1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//! 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 `POST`s
//! `/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`](crate::funnel::FunnelManager) holds the node's `TlsAcceptor` and an
//! `mpsc::Sender` sink ([`FunnelIngressSink`](crate::funnel::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`](crate::funnel::FunnelAccepted) over a [`FunnelAcceptedReceiver`](crate::funnel::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).
use Arc;
use TcpStream as OverlayStream;
use ;
use TlsAcceptor;
use crateAsyncReadWrite;
/// Bound on hijacked-but-not-yet-TLS-terminated ingress connections queued to the pump task, and on
/// TLS-terminated connections queued to the embedder. A relay flood back-pressures the peerAPI
/// `/v0/ingress` handler (which then drops, fail-closed) rather than buffering without limit. Each
/// queued conn pins an overlay TCP socket (~512 KiB rx+tx buffers — see `tcp_buffer_size` in
/// AGENTS.md), so the cap is deliberately modest.
const MAX_INGRESS_INFLIGHT: usize = 256;
/// 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.
///
/// `stream` is the overlay peerAPI connection *after* the `HTTP/1.1 101 Switching Protocols` reply —
/// raw bytes from here on are the public client's TLS handshake + records. `target` is the
/// `Tailscale-Ingress-Target` (`host:port` the public client hit) and `src` the
/// `Tailscale-Ingress-Src` (the public client's `host:port`, informational), both parsed from the
/// POST headers.
/// The sink the peerAPI `/v0/ingress` handler pushes hijacked [`IngressConn`]s 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`.
pub type FunnelIngressSink = Sender;
/// 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.
pub type FunnelIngressSlot = ;
/// 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`).
///
/// `stream` is the decrypted stream (the overlay stream wrapped in `tokio_rustls`'s server
/// `TlsStream`, boxed so the type is target-agnostic). `target`/`src` carry the ingress headers
/// through so an embedder can route on the hit `host:port` and log the public client.
/// Receiver side of the Funnel ingress hand-back channel (mirrors a `net.Listener`'s accept queue).
/// `Device::listen_funnel` returns one; await [`recv`](mpsc::Receiver::recv) to take the next
/// TLS-terminated public connection. Dropping it (or dropping the [`FunnelManager`]) tears the
/// listener down.
pub type FunnelAcceptedReceiver = Receiver;
/// 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`].
///
/// Built by `Device::listen_funnel` after the [`funnel_access`](ts_control::funnel_access) gate and
/// cert path pass. Holds the sink end so the manager keeps the channel (and thus the route) alive;
/// dropping the manager closes the sink and stops the pump. Registered on the device (mirroring
/// `serve: Mutex<Option<ServeManager>>`) so its lifetime is tied to the `Device`.
/// TLS-terminate each hijacked ingress stream and hand the decrypted stream to the embedder.
///
/// One handshake per connection, spawned so a slow handshake on one public client can't head-of-line
/// block another. A handshake failure drops the connection (fail-closed, logged). Ends when
/// `ingress_rx` closes (sink dropped) or `accept_tx` closes (embedder dropped the receiver).
async
/// Terminate TLS on one hijacked ingress stream and forward the decrypted stream. Anti-leak: TLS is
/// terminated on the overlay stream (never a host socket); no plaintext downgrade — a handshake
/// failure drops the connection.
async
/// Assert that `S` is an `AsyncRead + AsyncWrite` so callers know the decrypted stream is drivable.