util/linuxfw,wgengine/router: allow incoming CGNAT range traffic with nodeattr

Clients with the newly added node attribute
`"disable-linux-cgnat-drop-rule"` will not automatically drop inbound
traffic on non-Tailscale network interfaces with the source IP in the
CGNAT IP range. This is an initial proof-of-concept for enabling
connectivity with off-Tailnet CGNAT endpoints.

Fixes tailscale/corp#36270.

Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
Naman Sood
2026-04-14 16:45:06 -04:00
committed by GitHub
parent 5834058269
commit 6301a6ce4b
14 changed files with 527 additions and 69 deletions
+56
View File
@@ -89,6 +89,7 @@ type linuxRouter struct {
connmarkEnabled bool // whether connmark rules are currently enabled
netfilterMode preftype.NetfilterMode
netfilterKind string
cgnatMode linuxfw.CGNATMode
magicsockPortV4 uint16
magicsockPortV6 uint16
}
@@ -521,9 +522,50 @@ func (r *linuxRouter) Set(cfg *router.Config) error {
r.enableIPForwarding()
}
// Remove the rule to drop off-tailnet CGNAT traffic, if asked.
if netfilterOn || cfg.NetfilterMode == netfilterNoDivert {
var cgnatMode linuxfw.CGNATMode
if cfg.RemoveCGNATDropRule {
cgnatMode = linuxfw.CGNATModeReturn
} else {
cgnatMode = linuxfw.CGNATModeDrop
}
err := r.setCGNATDropModeLocked(cgnatMode)
if err != nil {
errs = append(errs, fmt.Errorf("set cgnat mode: %w", err))
}
}
return errors.Join(errs...)
}
// setCGNATDropModeLocked clears old rules and add new rules for the desired
// behavior for incoming non-Tailscale CGNAT packets.
// [linuxRouter.mu] must be held.
func (r *linuxRouter) setCGNATDropModeLocked(want linuxfw.CGNATMode) error {
if want == r.cgnatMode {
return nil
}
// r.cgnatMode is empty at initial startup, before this function has been
// called for the first time. In that case, we can skip deleting old
// rules, because there aren't any.
if r.cgnatMode != "" {
err := r.nfr.DelExternalCGNATRules(r.cgnatMode, r.tunname)
if err != nil {
return fmt.Errorf("clear old cgnat rules: %w", err)
}
}
err := r.nfr.AddExternalCGNATRules(want, r.tunname)
if err != nil {
// We currently have no rules set, so change the state to reflect that
// so we might try again on a future Router update.
r.cgnatMode = ""
return fmt.Errorf("add new cgnat rules: %w", err)
}
r.cgnatMode = want
return nil
}
var dockerStatefulFilteringWarnable = health.Register(&health.Warnable{
Code: "docker-stateful-filtering",
Title: "Docker with stateful filtering",
@@ -772,6 +814,20 @@ func (r *linuxRouter) setNetfilterModeLocked(mode preftype.NetfilterMode) error
}
}
// Re-add the CGNAT rules if we had any set.
// This does not call [linuxRouter.setCGNATDropModeLocked] because that
// function assumes that [linuxRouter.cgnatMode] accurately represents the
// current state in the firewall. This would not be true when we hit this
// code path, and is what we're fixing up here.
if r.cgnatMode != "" {
if err := r.nfr.AddExternalCGNATRules(r.cgnatMode, r.tunname); err != nil {
// We currently have no rules set, so change the state to reflect that
// so we might try again on a future Router update.
r.cgnatMode = ""
return fmt.Errorf("add cgnat rules: %w", err)
}
}
return nil
}