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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -717,11 +717,11 @@ func (n *fakeIPTablesRunner) DeleteDNATRuleForSvc(svcName string, origDst, dst n
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
type iptRule struct{ chain, rule string }
|
||||
|
||||
func (n *fakeIPTablesRunner) addBase4(tunname string) error {
|
||||
curIPT := n.ipt4
|
||||
newRules := []struct{ chain, rule string }{
|
||||
{"filter/ts-input", fmt.Sprintf("! -i %s -s %s -j RETURN", tunname, tsaddr.ChromeOSVMRange().String())},
|
||||
{"filter/ts-input", fmt.Sprintf("! -i %s -s %s -j DROP", tunname, tsaddr.CGNATRange().String())},
|
||||
newRules := []iptRule{
|
||||
{"filter/ts-forward", fmt.Sprintf("-i %s -j MARK --set-mark %s/%s", tunname, tsconst.LinuxSubnetRouteMark, tsconst.LinuxFwmarkMask)},
|
||||
{"filter/ts-forward", fmt.Sprintf("-m mark --mark %s/%s -j ACCEPT", tsconst.LinuxSubnetRouteMark, tsconst.LinuxFwmarkMask)},
|
||||
{"filter/ts-forward", fmt.Sprintf("-o %s -s %s -j DROP", tunname, tsaddr.CGNATRange().String())},
|
||||
@@ -737,7 +737,7 @@ func (n *fakeIPTablesRunner) addBase4(tunname string) error {
|
||||
|
||||
func (n *fakeIPTablesRunner) addBase6(tunname string) error {
|
||||
curIPT := n.ipt6
|
||||
newRules := []struct{ chain, rule string }{
|
||||
newRules := []iptRule{
|
||||
{"filter/ts-forward", fmt.Sprintf("-i %s -j MARK --set-mark %s/%s", tunname, tsconst.LinuxSubnetRouteMark, tsconst.LinuxFwmarkMask)},
|
||||
{"filter/ts-forward", fmt.Sprintf("-m mark --mark %s/%s -j ACCEPT", tsconst.LinuxSubnetRouteMark, tsconst.LinuxFwmarkMask)},
|
||||
{"filter/ts-forward", fmt.Sprintf("-o %s -j ACCEPT", tunname)},
|
||||
@@ -762,7 +762,7 @@ func (n *fakeIPTablesRunner) DelLoopbackRule(addr netip.Addr) error {
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddHooks() error {
|
||||
newRules := []struct{ chain, rule string }{
|
||||
newRules := []iptRule{
|
||||
{"filter/INPUT", "-j ts-input"},
|
||||
{"filter/FORWARD", "-j ts-forward"},
|
||||
{"nat/POSTROUTING", "-j ts-postrouting"},
|
||||
@@ -778,7 +778,7 @@ func (n *fakeIPTablesRunner) AddHooks() error {
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) DelHooks(logf logger.Logf) error {
|
||||
delRules := []struct{ chain, rule string }{
|
||||
delRules := []iptRule{
|
||||
{"filter/INPUT", "-j ts-input"},
|
||||
{"filter/FORWARD", "-j ts-forward"},
|
||||
{"nat/POSTROUTING", "-j ts-postrouting"},
|
||||
@@ -953,6 +953,48 @@ func (n *fakeIPTablesRunner) DelConnmarkSaveRule() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildExternalCGNATRules(mode linuxfw.CGNATMode, tunname string) ([]iptRule, error) {
|
||||
switch mode {
|
||||
case linuxfw.CGNATModeDrop:
|
||||
return []iptRule{
|
||||
{"filter/ts-input", fmt.Sprintf("! -i %s -s %s -j RETURN", tunname, tsaddr.ChromeOSVMRange().String())},
|
||||
{"filter/ts-input", fmt.Sprintf("! -i %s -s %s -j DROP", tunname, tsaddr.CGNATRange().String())},
|
||||
}, nil
|
||||
case linuxfw.CGNATModeReturn:
|
||||
return []iptRule{
|
||||
{"filter/ts-input", fmt.Sprintf("! -i %s -s %s -j RETURN", tunname, tsaddr.CGNATRange().String())},
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported mode %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) AddExternalCGNATRules(mode linuxfw.CGNATMode, tunname string) error {
|
||||
rules, err := buildExternalCGNATRules(mode, tunname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if err := appendRule(n, n.ipt4, rule.chain, rule.rule); err != nil {
|
||||
return fmt.Errorf("add rule %q to chain %q: %w", rule.rule, rule.chain, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) DelExternalCGNATRules(mode linuxfw.CGNATMode, tunname string) error {
|
||||
rules, err := buildExternalCGNATRules(mode, tunname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if err := deleteRule(n, n.ipt4, rule.chain, rule.rule); err != nil {
|
||||
return fmt.Errorf("del rule %q to chain %q: %w", rule.rule, rule.chain, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTablesRunner) HasIPV6() bool { return true }
|
||||
func (n *fakeIPTablesRunner) HasIPV6NAT() bool { return true }
|
||||
func (n *fakeIPTablesRunner) HasIPV6Filter() bool { return true }
|
||||
|
||||
@@ -132,10 +132,11 @@ type Config struct {
|
||||
SubnetRoutes []netip.Prefix
|
||||
|
||||
// Linux-only things below, ignored on other platforms.
|
||||
SNATSubnetRoutes bool // SNAT traffic to local subnets
|
||||
StatefulFiltering bool // Apply stateful filtering to inbound connections
|
||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||
NetfilterKind string // what kind of netfilter to use ("nftables", "iptables", or "" to auto-detect)
|
||||
SNATSubnetRoutes bool // SNAT traffic to local subnets
|
||||
StatefulFiltering bool // Apply stateful filtering to inbound connections
|
||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||
NetfilterKind string // what kind of netfilter to use ("nftables", "iptables", or "" to auto-detect)
|
||||
RemoveCGNATDropRule bool // whether to remove the firewall rule to drop non-Tailscale inbound traffic from CGNAT IPs
|
||||
}
|
||||
|
||||
func (a *Config) Equal(b *Config) bool {
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestConfigEqual(t *testing.T) {
|
||||
testedFields := []string{
|
||||
"LocalAddrs", "Routes", "LocalRoutes", "NewMTU",
|
||||
"SubnetRoutes", "SNATSubnetRoutes", "StatefulFiltering",
|
||||
"NetfilterMode", "NetfilterKind",
|
||||
"NetfilterMode", "NetfilterKind", "RemoveCGNATDropRule",
|
||||
}
|
||||
configType := reflect.TypeFor[Config]()
|
||||
configFields := []string{}
|
||||
|
||||
Reference in New Issue
Block a user