Skip to main content

Module funnel

Module funnel 

Source
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§

FunnelAccepted
A fully TLS-terminated Funnel ingress connection handed back to the embedder (the in-process stand-in for Go tsnet’s ListenFunnel-returned net.Listener).
FunnelManager
Owns the node’s Funnel ingress data path: the TlsAcceptor built from the node’s *.ts.net cert and the pump task that TLS-terminates each hijacked IngressConn.
IngressConn
A raw (not-yet-TLS-terminated) Funnel ingress connection the peerAPI /v0/ingress handler hijacked off the relay’s POST and handed to the FunnelManager’s sink.

Type Aliases§

FunnelAcceptedReceiver
Receiver side of the Funnel ingress hand-back channel (mirrors a net.Listener’s accept queue). Device::listen_funnel returns one; await recv to take the next TLS-terminated public connection. Dropping it (or dropping the FunnelManager) tears the listener down.
FunnelIngressSink
The sink the peerAPI /v0/ingress handler pushes hijacked IngressConns to. Cloneable; an mpsc::Sender so 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 (see FunnelIngressSlot) when the embedder calls Device::listen_funnel.
FunnelIngressSlot
The shared, runtime-lifetime slot the peerAPI server reads per connection to find the active FunnelIngressSink, and that Device::listen_funnel writes when it stands up a FunnelManager. None (the default) means no funnel listener is active, so the peerAPI /v0/ingress route fails closed (404) without hijacking. The peerAPI server (spawned at runtime start, before any listen_funnel) holds a clone of this Arc; installing a sink at listen_funnel time makes the route live without restarting the server.