Expand description
Client-side Funnel ingress termination (tsnet’s ListenFunnel data path).
Client-side Funnel ingress termination (tsnet’s ListenFunnel data path).
§The model (Go ipn/ipnlocal/serve.go’s handleIngress / TCPHandlerForFunnelFlow)
Public Funnel traffic does not reach this node directly. Tailscale operates a public ingress
relay (a tailnet peer, provisioned by control when a node advertises HostInfo.IngressEnabled)
plus the public DNS <node>.<tailnet>.ts.net:443 → relay mapping. A public client’s TLS bytes
arrive at the relay, which opens a connection to this node’s peerAPI and POSTs
/v0/ingress with the headers Tailscale-Ingress-Src (the public client host:port,
informational) and Tailscale-Ingress-Target (the host:port the client hit). The node replies
HTTP/1.1 101 Switching Protocols\r\n\r\n to hijack the connection into a raw bidirectional
stream that now carries the public client’s TLS handshake + records. The node then TLS-terminates
that stream with its own *.ts.net certificate (the Funnel hostname is the node’s MagicDNS
name) and serves the decrypted stream.
This module is the node-side half: the FunnelManager holds the node’s TlsAcceptor and an
mpsc::Sender sink (FunnelIngressSink) the peerAPI /v0/ingress handler pushes hijacked
raw streams to. A spawned pump task TLS-terminates each raw stream and yields the decrypted
FunnelAccepted over a FunnelAcceptedReceiver the embedder holds (the in-process stand-in
for Go tsnet’s ListenFunnel-returned net.Listener).
The relay + DNS legs are Tailscale infrastructure — present against real Tailscale SaaS (with a Funnel-enabled ACL), absent against a self-hosted control plane. So this code is correct and fully wired, but only ever fed when the node talks to real Tailscale.
§Anti-leak
The hijacked ingress stream arrives on the overlay peerAPI listener (the netstack
OverlayStream, never a host socket). TLS is terminated on that overlay stream and the
decrypted stream is handed to the embedder. Nothing here ever dials a host socket and nothing
routes through the ts_forwarder exit-egress path — Funnel ingress is purely inbound overlay
traffic, structurally separate from the exit-node anti-leak chokepoint. There is no plaintext
downgrade: if TLS termination fails, the connection is dropped (logged).
Structs§
- Funnel
Accepted - A fully TLS-terminated Funnel ingress connection handed back to the embedder (the in-process
stand-in for Go
tsnet’sListenFunnel-returnednet.Listener). - Funnel
Manager - Owns the node’s Funnel ingress data path: the
TlsAcceptorbuilt from the node’s*.ts.netcert and the pump task that TLS-terminates each hijackedIngressConn. - Ingress
Conn - A raw (not-yet-TLS-terminated) Funnel ingress connection the peerAPI
/v0/ingresshandler hijacked off the relay’s POST and handed to theFunnelManager’s sink.
Type Aliases§
- Funnel
Accepted Receiver - Receiver side of the Funnel ingress hand-back channel (mirrors a
net.Listener’s accept queue).Device::listen_funnelreturns one; awaitrecvto take the next TLS-terminated public connection. Dropping it (or dropping theFunnelManager) tears the listener down. - Funnel
Ingress Sink - The sink the peerAPI
/v0/ingresshandler pushes hijackedIngressConns to. Cloneable; anmpsc::Senderso the handler back-pressures (and then drops, fail-closed) when the pump can’t keep up. Installed into the peerAPI server via the shared slot (seeFunnelIngressSlot) when the embedder callsDevice::listen_funnel. - Funnel
Ingress Slot - The shared, runtime-lifetime slot the peerAPI server reads per connection to find the active
FunnelIngressSink, and thatDevice::listen_funnelwrites when it stands up aFunnelManager.None(the default) means no funnel listener is active, so the peerAPI/v0/ingressroute fails closed (404) without hijacking. The peerAPI server (spawned at runtime start, before anylisten_funnel) holds a clone of thisArc; installing a sink atlisten_funneltime makes the route live without restarting the server.