net/tstun, wgengine/filter: track UDP flow state for injected packets

Outbound packets produced by netstack (used by tailscaled with
--tun userspace-networking, by tsnet, and by the SOCKS5/HTTP proxies)
enter the wrapper via InjectOutbound{,PacketBuffer} and take the
injectedRead path, which bypasses Filter.RunOut.

RunOut's side effect for UDP/SCTP is to insert the reverse-flow tuple
into the connection-tracking LRU so that Filter.RunIn admits inbound
replies that no explicit ACL rule covers. Skipping it on the injected
path meant a netstack-side dial of UDP would send fine but the reply
would be dropped as "no matching rule". The kernel-TUN path was
already fine because it goes through RunOut.

Fixes #14229
Fixes #20064

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I816ef55c493a12ff4f561cd89c095559b5c2743b
This commit is contained in:
Brad Fitzpatrick
2026-06-22 15:57:37 -07:00
committed by Brad Fitzpatrick
parent 568c0bda24
commit e0677ccc76
4 changed files with 252 additions and 4 deletions
+18 -2
View File
@@ -621,8 +621,25 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
return noVerdict, "no rules matched"
}
// runIn runs the output-specific part of the filter logic.
// runOut runs the output-specific part of the filter logic.
func (f *Filter) runOut(q *packet.Parsed) (r Response, why string) {
f.UpdateOutboundFlowState(q)
return Accept, "ok out"
}
// UpdateOutboundFlowState records reverse-flow connection-tracking state for
// the given outbound packet so that subsequent inbound replies on the same
// flow are admitted by [Filter.RunIn] without an explicit allow rule.
//
// Only UDP and SCTP packets are tracked; for other protocols this is a no-op.
//
// It is intended for callers that synthesize outbound packets and bypass
// [Filter.RunOut] (for example netstack's [InjectOutbound] path used by
// userspace networking, tsnet and the SOCKS5/HTTP proxies), so that reply
// packets matching an outbound UDP flow are not silently dropped as "no
// matching rule" by [Filter.RunIn]. See tailscale/tailscale#14229 and
// tailscale/tailscale#20064.
func (f *Filter) UpdateOutboundFlowState(q *packet.Parsed) {
switch q.IPProto {
case ipproto.UDP, ipproto.SCTP:
tuple := flowtrack.MakeTuple(q.IPProto, q.Dst, q.Src) // src/dst reversed
@@ -630,7 +647,6 @@ func (f *Filter) runOut(q *packet.Parsed) (r Response, why string) {
f.state.lru.Add(tuple, struct{}{})
f.state.mu.Unlock()
}
return Accept, "ok out"
}
// direction is whether a packet was flowing into this machine, or