From 33f2101cc3e05c8260bbc7c9f3f115892e3fde7e Mon Sep 17 00:00:00 2001 From: Sri Vishnu Date: Mon, 28 Sep 2026 22:23:39 +0530 Subject: [PATCH 1/5] Enhance README with verbose logging instructions Added instructions for verbose logging of TCP connections. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3082f93ef..b3ac573a4 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,10 @@ $ Or you can serve a local TCP port, forwarded to localhost: +Pass `--verbose` before `serve` to log each accepted TCP connection and UDP +flow to stderr, including its source address, authenticated peer key, and +destination. These logs can reveal client and service addresses. + ```sh $ tailcat serve 8080,8443 # or: tailcat serve all # 🐈 Server listening with new address: tcXXXXXXXXX From 0d3bed7343d16e7746e1af60681e88fc287b193e Mon Sep 17 00:00:00 2001 From: Sri Vishnu Date: Mon, 28 Sep 2026 22:24:20 +0530 Subject: [PATCH 2/5] feat: log accepted server connections and flows --- tailcat.go | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/tailcat.go b/tailcat.go index d110ffcf1..1a6058ef3 100644 --- a/tailcat.go +++ b/tailcat.go @@ -430,6 +430,11 @@ type Server struct { // If nil, log.Printf is used. Logf logger.Logf + // LogConnections reports accepted TCP connections and UDP flows, including + // their remote address, authenticated peer key, and destination. It is + // disabled by default; logs can reveal client and service addresses. + LogConnections bool + // Region, if non-nil, is the DERP region to use as the bootstrap // relay, without fetching any DERP map. Region *tailcfg.DERPRegion @@ -697,15 +702,35 @@ func (s *Server) startLocked(ctx context.Context) error { } ns.ProcessLocalIPs = true ns.ProcessSubnets = true + logIncoming := func(network string, c net.Conn) { + if !s.LogConnections { + return + } + peer, ok := s.PeerKey(c.RemoteAddr()) + if !ok { + logf("incoming %s from %v (unknown peer key) to %v (server key %v)", network, c.RemoteAddr(), c.LocalAddr(), priv.Public()) + return + } + logf("incoming %s from %v (peer key %v) to %v (server key %v)", network, c.RemoteAddr(), peer, c.LocalAddr(), priv.Public()) + } + wrapTCP := func(h func(net.Conn)) func(net.Conn) { + if h == nil || !s.LogConnections { + return h + } + return func(c net.Conn) { + logIncoming("TCP", c) + h(c) + } + } ns.GetTCPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) { if dst.Addr() == lb.addr { if ln := s.listenerForPort("tcp", dst.Port()); ln != nil { - return ln.handle, true + return wrapTCP(ln.handle), true } if s.OnTCP == nil { return nil, true // send RST } - return s.OnTCP(dst.Port()), true + return wrapTCP(s.OnTCP(dst.Port())), true } if s.OnTCPForward == nil { return nil, true // send RST @@ -716,7 +741,7 @@ func (s *Server) startLocked(ctx context.Context) error { copy(a4[:], d6[12:16]) dst = netip.AddrPortFrom(netip.AddrFrom4(a4), dst.Port()) } - return s.OnTCPForward(dst), true + return wrapTCP(s.OnTCPForward(dst)), true } ns.GetUDPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(nettype.ConnPacketConn), intercept bool) { var h func(ConnPacketConn) @@ -738,7 +763,11 @@ func (s *Server) startLocked(ctx context.Context) error { if h == nil { return nil, true } - return func(c nettype.ConnPacketConn) { h(newIdlePacketConn(c, s.udpIdleTimeout())) }, true + return func(c nettype.ConnPacketConn) { + flow := newIdlePacketConn(c, s.udpIdleTimeout()) + logIncoming("UDP", flow) + h(flow) + }, true } lb.ns = ns sys.Set(ns) From e4e1f7c577c70ff3be46a88db4cf1e9439a24772 Mon Sep 17 00:00:00 2001 From: Sri Vishnu Date: Mon, 28 Sep 2026 22:24:56 +0530 Subject: [PATCH 3/5] Enhance verbosity flag description and logging --- cmd/tailcat/tailcat.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/tailcat/tailcat.go b/cmd/tailcat/tailcat.go index 3f7495b32..3daf19a9a 100644 --- a/cmd/tailcat/tailcat.go +++ b/cmd/tailcat/tailcat.go @@ -95,7 +95,7 @@ func newRootCommand() *ff.Command { rootFS := ff.NewFlagSet("tailcat") flagServe = rootFS.StringLong("serve", "", "comma-separated list of port numbers, port ranges, or service names to serve; the same list the serve subcommand takes as arguments. Service names are: 'all' (serve all ports), 'exit-node' (run an exit node for all addresses), 'ssh' (public-key-authenticated SSH server; see serve's --ssh-authorized-keys flag), 'no-auth-ssh' (auth-free SSH server), 'files' (file server for SFTP clients; see serve's --files flag), 'exec' (run the command after -- for each connection, with the connection as its stdio), 'perf' (accept throughput tests from 'tailcat perf'). If empty, it accepts a single connection on any port, writes it to stdout, and exits.") flagKey = rootFS.StringLong("key", "", "'new' for an ephemeral key. If empty, the default saved key is used if it exists ('default' in server mode, 'client-default' in client modes; see genkey), else an ephemeral key. Otherwise the path to a *.private.json or a name like 'foo' to read it from $CONFIG/tailcat/keys/foo.private.json") - flagVerbose = rootFS.BoolLong("verbose", "be verbose") + flagVerbose = rootFS.BoolLong("verbose", "be verbose; in server mode, log accepted connections and flows") flagJSON = rootFS.BoolLong("json", "in server mode, write {\"listenAddr\": ...} JSON to stdout; with perf, write the results as JSON") flagDERPMapURL = rootFS.StringLong("derpmap-url", cmp.Or(os.Getenv("TAILCAT_DERPMAP_URL"), tailcat.DefaultDERPMapURL), "URL of the JSON DERP map used to resolve or auto-select a DERP region; its default can also be set with the TAILCAT_DERPMAP_URL environment variable") @@ -1395,7 +1395,7 @@ func server(logf logger.Logf, serveSpec string, execArgs []string) { ci.ServerDiscoPublic = tailcat.DiscoPublicForNode(priv) connStr := ci.Addr() - s := &tailcat.Server{Key: priv, PresharedKey: psk, DisablePresharedKey: !usePSK, Logf: logf, Region: reg} + s := &tailcat.Server{Key: priv, PresharedKey: psk, DisablePresharedKey: !usePSK, Logf: logf, LogConnections: *flagVerbose, Region: reg} sshServices := services.Contains("ssh") || services.Contains("no-auth-ssh") || services.Contains("files") if sshServices && !tailcat.SupportsSSHServer() { log.Fatalf("Tailscale SSH server not supported on %v", runtime.GOOS) From 8d59d233bcf110f22ce0412b8de0ea50bf01a31f Mon Sep 17 00:00:00 2001 From: Sri Vishnu Date: Mon, 28 Sep 2026 22:25:26 +0530 Subject: [PATCH 4/5] test: cover verbose and quiet TCP logging --- cmd/tailcat/serve_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/tailcat/serve_test.go b/cmd/tailcat/serve_test.go index 8bf3c53ff..d19cc55b8 100644 --- a/cmd/tailcat/serve_test.go +++ b/cmd/tailcat/serve_test.go @@ -90,6 +90,9 @@ func TestServeWithoutPSK(t *testing.T) { if got != payload { t.Errorf("server echoed %q; want %q", got, payload) } + if strings.Contains(serverStderr.String(), "incoming TCP from ") { + t.Errorf("server logged a connection without --verbose: %s", serverStderr.String()) + } } func TestServeRemembersSavedKeyWithoutPSK(t *testing.T) { @@ -197,6 +200,11 @@ func TestServePorts(t *testing.T) { if got != payload { t.Errorf("served port echoed %q; want %q", got, payload) } + if logs := serverStderr.String(); !strings.Contains(logs, "incoming TCP from ") || + !strings.Contains(logs, "(peer key ") || + !strings.Contains(logs, fmt.Sprintf(":%d (server key ", port)) { + t.Errorf("verbose server did not log the accepted connection and its peer/destination: %s", logs) + } // The packet filter silently drops SYNs to unserved ports (no // RST; see Server.ServedTCPPorts), so instead of waiting out the From 33dfb0c1db9314fb0dbb15738430fa6c1bda83e2 Mon Sep 17 00:00:00 2001 From: Sri Vishnu Date: Mon, 28 Sep 2026 22:25:57 +0530 Subject: [PATCH 5/5] Enable verbose logging for performance tests --- cmd/tailcat/perf_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/tailcat/perf_test.go b/cmd/tailcat/perf_test.go index 243a07421..93324a27e 100644 --- a/cmd/tailcat/perf_test.go +++ b/cmd/tailcat/perf_test.go @@ -21,7 +21,7 @@ import ( func TestPerf(t *testing.T) { t.Parallel() e := newTestEnv(t) - _, addr, serverStderr := e.startServer("serve", "perf") + _, addr, serverStderr := e.startServer("--verbose", "serve", "perf") waitForLog(t, serverStderr, "Accepting perf tests") perfCmd := func(t *testing.T, args ...string) []byte { @@ -50,6 +50,7 @@ func TestPerf(t *testing.T) { } } waitForLog(t, serverStderr, "# perf test from ") + waitForLog(t, serverStderr, "incoming TCP from ") if !strings.Contains(serverStderr.String(), "TCP client -> server ") { t.Errorf("server log missing test summary:\n%s", serverStderr.String()) } @@ -96,6 +97,9 @@ func TestPerf(t *testing.T) { if got.RTT == nil || got.RTT.Count == 0 { t.Errorf("no RTT samples in %+v", got.Result) } + if logs := serverStderr.String(); !strings.Contains(logs, "incoming UDP from ") || !strings.Contains(logs, "(peer key ") { + t.Errorf("verbose server did not log the UDP flow and peer key: %s", logs) + } }) }