Commit 399807c
authored
feat(proxy): CIDR rules + HTTP Host header peeking on port 80 (#39)
* feat(proxy): cidr destinations + http host header peeking
Two related improvements that together let the policy engine
match hostname-based rules for IP-only CONNECT traffic on port
80, mirroring the SNI-peek path that already exists for TLS
ports.
CIDR destinations
A rule's destination is now treated as a CIDR when it contains
a `/`, and as a glob otherwise. compileRules calls
net.ParseCIDR for the CIDR branch and stores the resulting
IPNet on compiledRule alongside the existing optional Glob.
matchDestination dispatches between IP containment and glob
matching depending on which is non-nil. Invalid CIDR masks
fail at compile time rather than silently matching nothing.
A CIDR rule only matches destinations that parse as an IP, so
hostnames cannot accidentally match 0.0.0.0/0.
HTTP Host header peeking
New isPlainHTTPPort guard (80, 8080) and ctxKeyHTTPHostDeferred
extend the same defer-then-peek mechanism that SNI uses. When
the SOCKS5 CONNECT carries a bare IP and the verdict is not
already Allow / Deny, Resolve sets the HTTP-host-deferred flag.
handleConnect then sends CONNECT success early, peeks the
client's request bytes via peekHTTPHost, parses the request
line + headers via http.ReadRequest, extracts the Host value,
and re-evaluates policy against the hostname. Bytes consumed
during the peek are prepended to the relay reader so the
upstream sees the full request. The peek is bounded to 8 KiB
and bails out fast when the first byte is not in [A-Z], so
non-HTTP traffic on port 80 falls back cleanly.
Operator motivation: tailscaled's DERP latency probes hit
dozens of bare DERP IPs on port 80. With the new path, a
single *.tailscale.com rule covers them, instead of one
approval prompt per IP.
* fix(proxy): copilot review round 1 on PR #39
Three review comments addressed.
1. Reword isPlainHTTPPort comment to drop the "Telegram approval
prompts" phrasing. The helper is product-neutral and the doc
should match — operators reading the code may not be using the
Telegram channel at all.
2. Replace strings.Split(dest, ":")[0] with request.DestAddr.IP.String()
in both the SNI and HTTP-host policy-check paths. The split
approach mishandled IPv6 destinations because DestAddr.String()
emits IPv6 as "[::1]:80" and splitting on ":" yielded partial
values that corrupted logs and stored an incorrect key into the
reverse-DNS cache. The .IP field is already the parsed net.IP,
so .String() returns the address-only form regardless of family.
Same one-line bug existed in the pre-existing SNI helper; fixing
both at once because the PR introduced the second instance.
3. Reword peekHTTPHost docstring to say "plain HTTP (e.g. ports
80, 8080)" rather than "plain HTTP on port 80", matching what
the caller actually enables via isPlainHTTPPort.
* fix(proxy): spoofing guard for http host peek
Copilot round 2: the new HTTP-host peek path re-evaluated policy
using the client-supplied Host header, but the subsequent dial
still went to the original SOCKS5 destination IP. Without a
binding between Host and dest, an agent could connect to an
arbitrary IP:80 and claim Host: <permitted-name> to slip past
hostname-based policy. TLS catches this naturally because
SNI/cert mismatch fails the upstream handshake; plain HTTP has
no equivalent integrity check.
New hostResolvesToIP attests the Host -> dest binding before the
verdict is upgraded:
- Reverse-cache hit for the dest IP whose stored hostname equals
the Host claim is accepted as attested. The cache is populated
by the agent's own prior DNS query, which is the strongest
available signal that host -> dest is real.
- Otherwise a forward DNS lookup runs with a 2s timeout. The
dest IP must appear in the result set.
- Confirmed mismatch (DNS resolved but dest is not in the result)
and lookup failure both deny outright. Falling back to IP-based
Ask would surface the spoofed hostname in the broker prompt,
which is exactly the manipulation the attacker wanted.
Tests cover the cache-attestation happy path, cache-different-host
rejection, and nil-input guards.
* fix(proxy): copilot review round 3 on PR #39
Three review comments addressed.
1. Gate HTTP-host deferral on broker presence. The peek inside
handleConnect must send SOCKS5 RepSuccess before it can read
the request bytes, which means a deferred Ask-with-no-broker
would manifest as success-then-reset on the client side
instead of a clean RepHostUnreachable. Resolve now skips the
defer when no broker is configured, falling through to the
IP-based path so the Ask->Deny collapse happens before SOCKS5
success goes out, matching how go-socks5 reports failure for
the non-deferred Ask-without-broker case.
2. Reorder spoofing check after policy evaluation. Before, every
extracted Host header triggered a forward DNS lookup even
when policy would deny it, which leaked denied hostnames to
the resolver and added avoidable latency. Now the verdict is
computed first; Deny short-circuits without any lookup, and
Ask-without-broker also short-circuits. The DNS forward check
only runs for Allow and Ask-with-broker outcomes where the
verdict could result in keeping the connection.
3. Make the spoofing-guard tests hermetic. Split the cache
attestation into attestHostFromCache (pure cache, no network)
and add an injectable lookupIP field on Server so
hostResolvesToIP tests stub the resolver instead of hitting
the real one. The previous mismatch test indirectly performed
a real net.DefaultResolver.LookupIP, which is slow and flaky
in restricted CI environments. New tests cover lookup-match,
lookup-mismatch (spoofing), and lookup-error paths through
the stub plus the cache-only paths through the split helper.
* fix(proxy): deny http host peek failures instead of allowing through
Copilot round 4: when peekHTTPHost failed to recover a Host (binary
protocol on port 80, truncated headers, peek timeout, malformed
HTTP), the deferred path was returning allow=true with the buffered
bytes for relay. The deferral runs only for non-Allow / non-Deny
verdicts (i.e. Ask), so this silently upgraded Ask into an
unconditional allow for any port-80 traffic that didn't parse as
HTTP. Now the peek-failure branch denies and closes the
connection. We already sent SOCKS5 RepSuccess to enable the peek,
so the client observes a closed connection rather than a clean
SOCKS5 reject; that is acceptable because the alternative is a
real bypass and a non-HTTP probe on a deferred port is the
suspect pattern we want to block anyway.
* fix(proxy): preserve ask semantic on host peek failure + ipv6 host parse
Round 5 review correcting an over-correction from round 4 plus an
IPv6 parsing edge case.
1. Host peek failure no longer collapses Ask to Deny. Round 4
switched the bypass to outright Deny, but that converted any
non-HTTP traffic on a deferred port-80 connection into Deny
regardless of the original IP-level Ask verdict, contrary to
the "fall back cleanly" intent. Now the failure path attaches
a per-request RequestPolicyChecker bound to the IP destination
and returns allow=true with the buffered bytes; the dial step
calls CheckAndConsume which broker-prompts the operator for
the bare IP. Approval => connection proceeds; denial => dial
fails and the connection closes. This mirrors the non-deferred
Ask-with-broker handling and preserves the Ask semantic. The
deferral guard already requires broker != nil, so the checker
has somewhere to send the prompt.
2. extractHTTPHost now uses net.SplitHostPort to strip the port
from a Host header. The previous strings.LastIndex(":") logic
correctly handled bracketed IPv6 ("[::1]:80") and DNS hosts
("example.com:80") but mishandled bare IPv6 ("2001:db8::1"),
stripping the final hextet as if it were a port. SplitHostPort
errors on bare IPv6 ("too many colons in address"), and the
error path now falls back to the trimmed host unchanged. Added
a regression test for the bare-IPv6 input.1 parent 61f4c00 commit 399807c
6 files changed
Lines changed: 736 additions & 14 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
274 | 274 | | |
275 | 275 | | |
276 | 276 | | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
277 | 281 | | |
278 | 282 | | |
279 | 283 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
30 | 35 | | |
| 36 | + | |
31 | 37 | | |
32 | 38 | | |
33 | 39 | | |
34 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
35 | 59 | | |
36 | 60 | | |
37 | 61 | | |
| |||
256 | 280 | | |
257 | 281 | | |
258 | 282 | | |
259 | | - | |
260 | | - | |
261 | | - | |
262 | | - | |
263 | | - | |
264 | 283 | | |
265 | 284 | | |
266 | 285 | | |
| |||
272 | 291 | | |
273 | 292 | | |
274 | 293 | | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
275 | 312 | | |
276 | 313 | | |
277 | 314 | | |
| |||
313 | 350 | | |
314 | 351 | | |
315 | 352 | | |
316 | | - | |
| 353 | + | |
317 | 354 | | |
318 | 355 | | |
319 | 356 | | |
| |||
337 | 374 | | |
338 | 375 | | |
339 | 376 | | |
340 | | - | |
| 377 | + | |
341 | 378 | | |
342 | 379 | | |
343 | 380 | | |
| |||
361 | 398 | | |
362 | 399 | | |
363 | 400 | | |
364 | | - | |
| 401 | + | |
365 | 402 | | |
366 | 403 | | |
367 | 404 | | |
| |||
416 | 453 | | |
417 | 454 | | |
418 | 455 | | |
419 | | - | |
| 456 | + | |
420 | 457 | | |
421 | 458 | | |
422 | 459 | | |
| |||
478 | 515 | | |
479 | 516 | | |
480 | 517 | | |
481 | | - | |
| 518 | + | |
482 | 519 | | |
483 | 520 | | |
484 | 521 | | |
485 | 522 | | |
486 | 523 | | |
487 | 524 | | |
488 | 525 | | |
489 | | - | |
| 526 | + | |
490 | 527 | | |
491 | 528 | | |
492 | 529 | | |
| |||
495 | 532 | | |
496 | 533 | | |
497 | 534 | | |
498 | | - | |
| 535 | + | |
499 | 536 | | |
500 | 537 | | |
501 | 538 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2239 | 2239 | | |
2240 | 2240 | | |
2241 | 2241 | | |
| 2242 | + | |
| 2243 | + | |
| 2244 | + | |
| 2245 | + | |
| 2246 | + | |
| 2247 | + | |
| 2248 | + | |
| 2249 | + | |
| 2250 | + | |
| 2251 | + | |
| 2252 | + | |
| 2253 | + | |
| 2254 | + | |
| 2255 | + | |
| 2256 | + | |
| 2257 | + | |
| 2258 | + | |
| 2259 | + | |
| 2260 | + | |
| 2261 | + | |
| 2262 | + | |
| 2263 | + | |
| 2264 | + | |
| 2265 | + | |
| 2266 | + | |
| 2267 | + | |
| 2268 | + | |
| 2269 | + | |
| 2270 | + | |
| 2271 | + | |
| 2272 | + | |
| 2273 | + | |
| 2274 | + | |
| 2275 | + | |
| 2276 | + | |
| 2277 | + | |
| 2278 | + | |
| 2279 | + | |
| 2280 | + | |
| 2281 | + | |
| 2282 | + | |
| 2283 | + | |
| 2284 | + | |
| 2285 | + | |
| 2286 | + | |
| 2287 | + | |
| 2288 | + | |
| 2289 | + | |
| 2290 | + | |
| 2291 | + | |
| 2292 | + | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
| 2299 | + | |
| 2300 | + | |
| 2301 | + | |
| 2302 | + | |
| 2303 | + | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
| 2308 | + | |
| 2309 | + | |
| 2310 | + | |
| 2311 | + | |
| 2312 | + | |
| 2313 | + | |
| 2314 | + | |
| 2315 | + | |
| 2316 | + | |
| 2317 | + | |
| 2318 | + | |
| 2319 | + | |
| 2320 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 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 | + | |
0 commit comments