Tailport

2026-09-04 · page · source ↗ · source summary

A single Go binary that turns "expose my local dev server to the tailnet" from a memorized tailscale serve invocation into a one-keystroke toggle in a terminal UI. Tailport lists your machine's locally listening TCP ports, derives each port's actual reachability (localhost / LAN / tailnet / public), and drives Tailscale's own CLI to serve, funnel, or unpublish each port. It matters less for what it adds — a thin Bubble Tea layer over a CLI that already exists — than for the engineering discipline it applies to a tool with real, irreversible consequences: public internet exposure is never automatic, every destructive edge mutation is verified against a confirm the user actually saw, and the "reachability" model is built on the truth of socket binds rather than on what the user thinks a port does.


Architecture

Tailport is a monolith with a layered internal package structure, not a service. It never runs a daemon, never scans the network, and installs nothing system-wide: every effect it has is a tailscale serve/funnel mapping the user explicitly toggled. The only state it owns is a per-port registry in ~/.config/tailport/config.yaml.

The TUI (internal/ui/ui.go, 7.6k lines) is a Bubble Tea model whose list items are portItems. Each item resolves a 7-state reach() classification — localhost, tailnet, LAN, served, funnel, publish, stale, offline — and renders a "moon-phase" fill ramp (🌕/🌔/🌒/🌑 vs ○/◔/◉/●) so exposure level reads at a glance.

Key techniques

Design decisions

Comparison notes

Where Tunnet builds the whole mesh — QUIC transport, CRDT membership, ACL engine, serve, tunnel, SSH — tailport builds none of it: it treats Tailscale as the platform and only layers a TUI, a registry, and a publish path on top. The trade is scope for depth: tailport's genuinely novel code is the concurrent-safety and confirm-integrity machinery around Caddy, not the networking.

Portless solves the adjacent local-dev problem from the naming side — replace localhost:3000 with https://myapp.localhost — whereas tailport solves it from the exposure side: keep the port number, make it tailnet- (or publicly) reachable. The two are complementary; Portless even integrates Tailscale for LAN sharing.

Against Headscale (the self-hosted Tailscale control plane), tailport is a pure client: it talks only to the CLI, so its tailnet-serve path works against any control plane that CLI supports, while its funnel path depends on Tailscale's own HTTPS certs and Funnel node attribute.

#tool #project #networking #tailscale #tui #go


Sources: raw/tailport, Tailport: a TUI for exposing local ports across your tailnet via tailscale serve Last updated: 2026-09-04