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:
@@ -5673,13 +5673,14 @@ func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView
|
|||||||
}
|
}
|
||||||
|
|
||||||
rs := &router.Config{
|
rs := &router.Config{
|
||||||
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
|
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
|
||||||
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()),
|
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()),
|
||||||
SNATSubnetRoutes: !prefs.NoSNAT(),
|
SNATSubnetRoutes: !prefs.NoSNAT(),
|
||||||
StatefulFiltering: doStatefulFiltering,
|
StatefulFiltering: doStatefulFiltering,
|
||||||
NetfilterMode: prefs.NetfilterMode(),
|
NetfilterMode: prefs.NetfilterMode(),
|
||||||
Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold, prefs.RouteAll()),
|
Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold, prefs.RouteAll()),
|
||||||
NetfilterKind: netfilterKind,
|
NetfilterKind: netfilterKind,
|
||||||
|
RemoveCGNATDropRule: nm.HasCap(tailcfg.NodeAttrDisableLinuxCGNATDropRule),
|
||||||
}
|
}
|
||||||
|
|
||||||
if buildfeatures.HasSynology && distro.Get() == distro.Synology {
|
if buildfeatures.HasSynology && distro.Get() == distro.Synology {
|
||||||
|
|||||||
+8
-1
@@ -182,7 +182,8 @@ type CapabilityVersion int
|
|||||||
// - 133: 2026-02-17: client understands [NodeAttrForceRegisterMagicDNSIPv4Only]; MagicDNS IPv6 registered w/ OS by default
|
// - 133: 2026-02-17: client understands [NodeAttrForceRegisterMagicDNSIPv4Only]; MagicDNS IPv6 registered w/ OS by default
|
||||||
// - 134: 2026-03-09: Client understands [NodeAttrDisableAndroidBindToActiveNetwork]
|
// - 134: 2026-03-09: Client understands [NodeAttrDisableAndroidBindToActiveNetwork]
|
||||||
// - 135: 2026-03-30: Client understands [NodeAttrCacheNetworkMaps]
|
// - 135: 2026-03-30: Client understands [NodeAttrCacheNetworkMaps]
|
||||||
const CurrentCapabilityVersion CapabilityVersion = 135
|
// - 136: 2026-04-09: Client understands [NodeAttrDisableLinuxCGNATDropRule]
|
||||||
|
const CurrentCapabilityVersion CapabilityVersion = 136
|
||||||
|
|
||||||
// ID is an integer ID for a user, node, or login allocated by the
|
// ID is an integer ID for a user, node, or login allocated by the
|
||||||
// control plane.
|
// control plane.
|
||||||
@@ -2790,6 +2791,12 @@ const (
|
|||||||
// absent (or removed), a node that supports netmap caching will ignore and
|
// absent (or removed), a node that supports netmap caching will ignore and
|
||||||
// discard existing cached maps, and will not store any.
|
// discard existing cached maps, and will not store any.
|
||||||
NodeAttrCacheNetworkMaps NodeCapability = "cache-network-maps"
|
NodeAttrCacheNetworkMaps NodeCapability = "cache-network-maps"
|
||||||
|
|
||||||
|
// NodeAttrDisableLinuxCGNATDropRule tells Linux clients to not insert a
|
||||||
|
// blanket firewall DROP rule for inbound traffic from the CGNAT IP range
|
||||||
|
// that does not originate from the Tailscale network interface.
|
||||||
|
// This enables access to off-tailnet endpoints within that IP range.
|
||||||
|
NodeAttrDisableLinuxCGNATDropRule NodeCapability = "disable-linux-cgnat-drop-rule"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -197,6 +197,22 @@ func sameLAN(c *vnet.Config) *vnet.Node {
|
|||||||
return c.AddNode(nw)
|
return c.AddNode(nw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sameLANNoDropCGNAT(c *vnet.Config) *vnet.Node {
|
||||||
|
nw := c.FirstNetwork()
|
||||||
|
if nw == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !nw.CanTakeMoreNodes() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.AddNode(
|
||||||
|
nw,
|
||||||
|
tailcfg.NodeCapMap{
|
||||||
|
tailcfg.NodeAttrDisableLinuxCGNATDropRule: nil,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func one2one(c *vnet.Config) *vnet.Node {
|
func one2one(c *vnet.Config) *vnet.Node {
|
||||||
n := c.NumNodes() + 1
|
n := c.NumNodes() + 1
|
||||||
return c.AddNode(c.AddNetwork(
|
return c.AddNode(c.AddNetwork(
|
||||||
@@ -437,6 +453,11 @@ func (nt *natTest) setupTest(ctx context.Context, addNode ...addNodeFunc) (nodes
|
|||||||
return fmt.Errorf("%v status: %w", node, err)
|
return fmt.Errorf("%v status: %w", node, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if capMap := node.WantCapMap(); capMap != nil {
|
||||||
|
nt.tb.Logf("using capmap for %s: %+v", node.String(), capMap)
|
||||||
|
nt.vnet.ControlServer().SetNodeCapMap(st.Self.PublicKey, capMap)
|
||||||
|
}
|
||||||
|
|
||||||
if st.BackendState != "Running" {
|
if st.BackendState != "Running" {
|
||||||
return fmt.Errorf("%v state = %q", node, st.BackendState)
|
return fmt.Errorf("%v state = %q", node, st.BackendState)
|
||||||
}
|
}
|
||||||
@@ -788,11 +809,8 @@ func cgnatNoTailnet(c *vnet.Config) *vnet.Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNonTailscaleCGNATEndpoint(t *testing.T) {
|
func TestNonTailscaleCGNATEndpoint(t *testing.T) {
|
||||||
if !*knownBroken {
|
|
||||||
t.Skip("skipping known-broken test; set --known-broken to run; see https://github.com/tailscale/corp/issues/36270")
|
|
||||||
}
|
|
||||||
nt := newNatTest(t)
|
nt := newNatTest(t)
|
||||||
if !nt.runHostConnectivityTest(cgnatNoTailnet, sameLAN) {
|
if !nt.runHostConnectivityTest(cgnatNoTailnet, sameLANNoDropCGNAT) {
|
||||||
t.Fatalf("could not ping")
|
t.Fatalf("could not ping")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/google/gopacket/pcapgo"
|
"github.com/google/gopacket/pcapgo"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/must"
|
"tailscale.com/util/must"
|
||||||
"tailscale.com/util/set"
|
"tailscale.com/util/set"
|
||||||
@@ -137,6 +138,8 @@ func (c *Config) AddNode(opts ...any) *Node {
|
|||||||
}
|
}
|
||||||
case MAC:
|
case MAC:
|
||||||
n.mac = o
|
n.mac = o
|
||||||
|
case tailcfg.NodeCapMap:
|
||||||
|
n.capMap = o
|
||||||
default:
|
default:
|
||||||
if n.err == nil {
|
if n.err == nil {
|
||||||
n.err = fmt.Errorf("unknown AddNode option type %T", o)
|
n.err = fmt.Errorf("unknown AddNode option type %T", o)
|
||||||
@@ -225,6 +228,7 @@ type Node struct {
|
|||||||
preICMPPing bool
|
preICMPPing bool
|
||||||
verboseSyslog bool
|
verboseSyslog bool
|
||||||
dontJoinTailnet bool
|
dontJoinTailnet bool
|
||||||
|
capMap tailcfg.NodeCapMap
|
||||||
|
|
||||||
// TODO(bradfitz): this is halfway converted to supporting multiple NICs
|
// TODO(bradfitz): this is halfway converted to supporting multiple NICs
|
||||||
// but not done. We need a MAC-per-Network.
|
// but not done. We need a MAC-per-Network.
|
||||||
@@ -318,6 +322,12 @@ func (n *Node) ShouldJoinTailnet() bool {
|
|||||||
return !n.dontJoinTailnet
|
return !n.dontJoinTailnet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WantCapMap returns the [tailcfg.NodeCapMap] that control should send down to
|
||||||
|
// this node, if any.
|
||||||
|
func (n *Node) WantCapMap() tailcfg.NodeCapMap {
|
||||||
|
return n.capMap
|
||||||
|
}
|
||||||
|
|
||||||
// IsV6Only reports whether this node is only connected to IPv6 networks.
|
// IsV6Only reports whether this node is only connected to IPv6 networks.
|
||||||
func (n *Node) IsV6Only() bool {
|
func (n *Node) IsV6Only() bool {
|
||||||
for _, net := range n.nets {
|
for _, net := range n.nets {
|
||||||
|
|||||||
@@ -95,3 +95,5 @@ func (f *FakeNetfilterRunner) DeleteSvc(svc, tun string, targetIPs []netip.Addr,
|
|||||||
func (f *FakeNetfilterRunner) EnsurePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
|
func (f *FakeNetfilterRunner) EnsurePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (f *FakeNetfilterRunner) AddExternalCGNATRules(mode CGNATMode, tunname string) error { return nil }
|
||||||
|
func (f *FakeNetfilterRunner) DelExternalCGNATRules(mode CGNATMode, tunname string) error { return nil }
|
||||||
|
|||||||
@@ -214,23 +214,8 @@ func (i *iptablesRunner) AddBase(tunname string) error {
|
|||||||
// addBase4 adds some basic IPv4 processing rules to be
|
// addBase4 adds some basic IPv4 processing rules to be
|
||||||
// supplemented by later calls to other helpers.
|
// supplemented by later calls to other helpers.
|
||||||
func (i *iptablesRunner) addBase4(tunname string) error {
|
func (i *iptablesRunner) addBase4(tunname string) error {
|
||||||
// Only allow CGNAT range traffic to come from tailscale0. There
|
// Explicitly allow all inbound traffic to the tun interface
|
||||||
// is an exception carved out for ranges used by ChromeOS, for
|
args := []string{"-i", tunname, "-j", "ACCEPT"}
|
||||||
// which we fall out of the Tailscale chain.
|
|
||||||
//
|
|
||||||
// Note, this will definitely break nodes that end up using the
|
|
||||||
// CGNAT range for other purposes :(.
|
|
||||||
args := []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
|
|
||||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
|
||||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
|
||||||
}
|
|
||||||
args = []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
|
||||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
|
||||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly allow all other inbound traffic to the tun interface
|
|
||||||
args = []string{"-i", tunname, "-j", "ACCEPT"}
|
|
||||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||||
}
|
}
|
||||||
@@ -682,6 +667,67 @@ func (i *iptablesRunner) DelMagicsockPortRule(port uint16, network string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildExternalCGNATRules abstracts out logic for constructing firewall rules
|
||||||
|
// for handling non-Tailscale CGNAT traffic, since these rules need to be
|
||||||
|
// identical across [AddExternalCGNATRules] and [DelExternalCGNATRules].
|
||||||
|
func buildExternalCGNATRules(mode CGNATMode, tunname string) ([][]string, error) {
|
||||||
|
switch mode {
|
||||||
|
case CGNATModeDrop:
|
||||||
|
// Only allow CGNAT range traffic to come from the Tailscale interface.
|
||||||
|
// There is an exception carved out for ranges used by ChromeOS, for
|
||||||
|
// which we fall out of the Tailscale chain.
|
||||||
|
return [][]string{
|
||||||
|
{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"},
|
||||||
|
{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"},
|
||||||
|
}, nil
|
||||||
|
case CGNATModeReturn:
|
||||||
|
// Fall out of the Tailscale chain for CGNAT traffic that doesn't
|
||||||
|
// originate from the Tailscale interface.
|
||||||
|
return [][]string{
|
||||||
|
{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "RETURN"},
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported mode %q", mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddExternalCGNATRules adds rules to the ts-input chain to deal with
|
||||||
|
// traffic from the CGNAT range that arrives on non-Tailscale network
|
||||||
|
// interfaces.
|
||||||
|
func (i *iptablesRunner) AddExternalCGNATRules(mode CGNATMode, tunname string) error {
|
||||||
|
rules, err := buildExternalCGNATRules(mode, tunname)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("build cgnat mode rule: %v", err)
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
if err := i.ipt4.Append("filter", "ts-input", rule...); err != nil {
|
||||||
|
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", rule, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelExternalCGNATRules removes the rules created by AddExternalCGNATRules,
|
||||||
|
// if they exist.
|
||||||
|
func (i *iptablesRunner) DelExternalCGNATRules(mode CGNATMode, tunname string) error {
|
||||||
|
rules, err := buildExternalCGNATRules(mode, tunname)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("build cgnat mode rule: %v", err)
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
if found, err := i.ipt4.Exists("filter", "ts-input", rule...); err != nil {
|
||||||
|
return fmt.Errorf("checking for %v in v4/filter/ts-input: %w", rule, err)
|
||||||
|
} else if !found {
|
||||||
|
// Don't need to delete a rule that isn't there.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := i.ipt4.Delete("filter", "ts-input", rule...); err != nil {
|
||||||
|
return fmt.Errorf("deleting %v in v4/filter/ts-input: %w", rule, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// delTSHook deletes hook in a chain that jumps to a ts-chain. If the hook does not
|
// delTSHook deletes hook in a chain that jumps to a ts-chain. If the hook does not
|
||||||
// exist, it's a no-op since the desired state is already achieved but we log the
|
// exist, it's a no-op since the desired state is already achieved but we log the
|
||||||
// error because error code from the iptables module resists unwrapping.
|
// error because error code from the iptables module resists unwrapping.
|
||||||
|
|||||||
@@ -126,8 +126,6 @@ func TestAddAndDeleteBase(t *testing.T) {
|
|||||||
|
|
||||||
// Check that the rules were created.
|
// Check that the rules were created.
|
||||||
tsRulesV4 := []fakeRule{ // table/chain/rule
|
tsRulesV4 := []fakeRule{ // table/chain/rule
|
||||||
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}},
|
|
||||||
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}},
|
|
||||||
{"filter", "ts-forward", []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}},
|
{"filter", "ts-forward", []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,3 +502,56 @@ func TestAddAndDelConnmarkSaveRule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddAndDelCGNATRules(t *testing.T) {
|
||||||
|
iptr := newFakeIPTablesRunner()
|
||||||
|
tunname := "tun0"
|
||||||
|
|
||||||
|
// We need the chains to exist so we can add rules into them.
|
||||||
|
if err := iptr.AddChains(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
mode CGNATMode
|
||||||
|
wantRules []fakeRule
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
CGNATModeDrop, []fakeRule{
|
||||||
|
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}},
|
||||||
|
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CGNATModeReturn, []fakeRule{
|
||||||
|
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "RETURN"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if err := iptr.AddExternalCGNATRules(tt.mode, tunname); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tr := range tt.wantRules {
|
||||||
|
if exists, err := iptr.ipt4.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||||
|
t.Fatalf("mode %q: error checking for rule: %v", tt.mode, err)
|
||||||
|
} else if !exists {
|
||||||
|
t.Errorf("mode %q: rule %s/%s/%s doesn't exist", tt.mode, tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := iptr.DelExternalCGNATRules(tt.mode, tunname); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tr := range tt.wantRules {
|
||||||
|
if exists, err := iptr.ipt4.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||||
|
t.Fatalf("mode %q: error checking for rule: %v", tt.mode, err)
|
||||||
|
} else if exists {
|
||||||
|
t.Errorf("mode %q: rule %s/%s/%s not deleted", tt.mode, tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ const (
|
|||||||
FirewallModeNfTables FirewallMode = "nftables"
|
FirewallModeNfTables FirewallMode = "nftables"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CGNATMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CGNATModeDrop CGNATMode = "DROP"
|
||||||
|
CGNATModeReturn CGNATMode = "RETURN"
|
||||||
|
)
|
||||||
|
|
||||||
// The following bits are added to packet marks for Tailscale use.
|
// The following bits are added to packet marks for Tailscale use.
|
||||||
//
|
//
|
||||||
// We tried to pick bits sufficiently out of the way that it's
|
// We tried to pick bits sufficiently out of the way that it's
|
||||||
|
|||||||
@@ -593,6 +593,15 @@ type NetfilterRunner interface {
|
|||||||
// DelMagicsockPortRule removes the rule created by AddMagicsockPortRule,
|
// DelMagicsockPortRule removes the rule created by AddMagicsockPortRule,
|
||||||
// if it exists.
|
// if it exists.
|
||||||
DelMagicsockPortRule(port uint16, network string) error
|
DelMagicsockPortRule(port uint16, network string) error
|
||||||
|
|
||||||
|
// AddExternalCGNATRules adds rules to the ts-input chain to deal with
|
||||||
|
// traffic from the CGNAT range that arrives on non-Tailscale network
|
||||||
|
// interfaces.
|
||||||
|
AddExternalCGNATRules(mode CGNATMode, tunname string) error
|
||||||
|
|
||||||
|
// DelExternalCGNATRules removes the rules created by AddExternalCGNATRules,
|
||||||
|
// if they exist.
|
||||||
|
DelExternalCGNATRules(mode CGNATMode, tunname string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a NetfilterRunner, auto-detecting whether to use
|
// New creates a NetfilterRunner, auto-detecting whether to use
|
||||||
@@ -1221,6 +1230,27 @@ func addReturnChromeOSVMRangeRule(c *nftables.Conn, table *nftables.Table, chain
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delReturnChromeOSVMRangeRule deletes the rule created by addReturnChromeOSVMRangeRule,
|
||||||
|
// if it exists.
|
||||||
|
func delReturnChromeOSVMRangeRule(c *nftables.Conn, table *nftables.Table, chain *nftables.Chain, tunname string) error {
|
||||||
|
rule, err := createRangeRule(table, chain, tunname, tsaddr.ChromeOSVMRange(), expr.VerdictReturn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create rule: %w", err)
|
||||||
|
}
|
||||||
|
rule, err = findRule(c, rule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find rule: %v", err)
|
||||||
|
}
|
||||||
|
if rule == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = c.DelRule(rule)
|
||||||
|
if err := c.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("flush del rule: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// addDropCGNATRangeRule adds a rule to drop if the source IP is in the
|
// addDropCGNATRangeRule adds a rule to drop if the source IP is in the
|
||||||
// CGNAT range.
|
// CGNAT range.
|
||||||
func addDropCGNATRangeRule(c *nftables.Conn, table *nftables.Table, chain *nftables.Chain, tunname string) error {
|
func addDropCGNATRangeRule(c *nftables.Conn, table *nftables.Table, chain *nftables.Chain, tunname string) error {
|
||||||
@@ -1235,6 +1265,62 @@ func addDropCGNATRangeRule(c *nftables.Conn, table *nftables.Table, chain *nftab
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delDropCGNATRangeRule deletes the rule created by addDropCGNATRangeRule,
|
||||||
|
// if it exists.
|
||||||
|
func delDropCGNATRangeRule(c *nftables.Conn, table *nftables.Table, chain *nftables.Chain, tunname string) error {
|
||||||
|
rule, err := createRangeRule(table, chain, tunname, tsaddr.CGNATRange(), expr.VerdictDrop)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create rule: %w", err)
|
||||||
|
}
|
||||||
|
rule, err = findRule(c, rule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find rule: %v", err)
|
||||||
|
}
|
||||||
|
if rule == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = c.DelRule(rule)
|
||||||
|
if err := c.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("flush del rule: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addReturnCGNATRangeRule adds a rule to return if the source IP is in the
|
||||||
|
// CGNAT range.
|
||||||
|
func addReturnCGNATRangeRule(c *nftables.Conn, table *nftables.Table, chain *nftables.Chain, tunname string) error {
|
||||||
|
rule, err := createRangeRule(table, chain, tunname, tsaddr.CGNATRange(), expr.VerdictReturn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create rule: %w", err)
|
||||||
|
}
|
||||||
|
_ = c.AddRule(rule)
|
||||||
|
if err = c.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("add rule: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// delReturnCGNATRangeRule deletes the rule created by addReturnCGNATRangeRule,
|
||||||
|
// if it exists.
|
||||||
|
func delReturnCGNATRangeRule(c *nftables.Conn, table *nftables.Table, chain *nftables.Chain, tunname string) error {
|
||||||
|
rule, err := createRangeRule(table, chain, tunname, tsaddr.CGNATRange(), expr.VerdictReturn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create rule: %w", err)
|
||||||
|
}
|
||||||
|
rule, err = findRule(c, rule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find rule: %v", err)
|
||||||
|
}
|
||||||
|
if rule == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = c.DelRule(rule)
|
||||||
|
if err := c.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("flush del rule: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// createSetSubnetRouteMarkRule creates a rule to set the subnet route
|
// createSetSubnetRouteMarkRule creates a rule to set the subnet route
|
||||||
// mark if the packet is from the given interface.
|
// mark if the packet is from the given interface.
|
||||||
func createSetSubnetRouteMarkRule(table *nftables.Table, chain *nftables.Chain, tunname string) (*nftables.Rule, error) {
|
func createSetSubnetRouteMarkRule(table *nftables.Table, chain *nftables.Chain, tunname string) (*nftables.Rule, error) {
|
||||||
@@ -1502,6 +1588,67 @@ func (n *nftablesRunner) DelMagicsockPortRule(port uint16, network string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddExternalCGNATRules adds rules to the ts-input chain to deal with
|
||||||
|
// traffic from the CGNAT range that arrives on non-Tailscale network
|
||||||
|
// interfaces.
|
||||||
|
func (n *nftablesRunner) AddExternalCGNATRules(mode CGNATMode, tunname string) error {
|
||||||
|
conn := n.conn
|
||||||
|
|
||||||
|
inputChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameInput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get input chain v4: %v", err)
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case CGNATModeDrop:
|
||||||
|
if err = addReturnChromeOSVMRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
||||||
|
return fmt.Errorf("add return chromeos vm range rule v4: %w", err)
|
||||||
|
}
|
||||||
|
if err = addDropCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
||||||
|
return fmt.Errorf("add drop cgnat range rule v4: %w", err)
|
||||||
|
}
|
||||||
|
case CGNATModeReturn:
|
||||||
|
if err = addReturnCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
||||||
|
return fmt.Errorf("add return cgnat range rule v4: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported cgnat mode %q", mode)
|
||||||
|
}
|
||||||
|
if err = conn.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("flush cgnat rules v4: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelExternalCGNATRules removes the rules created by AddExternalCGNATRules,
|
||||||
|
// if they exist.
|
||||||
|
func (n *nftablesRunner) DelExternalCGNATRules(mode CGNATMode, tunname string) error {
|
||||||
|
conn := n.conn
|
||||||
|
|
||||||
|
inputChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameInput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get input chain v4: %v", err)
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case CGNATModeDrop:
|
||||||
|
if err = delReturnChromeOSVMRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
||||||
|
return fmt.Errorf("del return chromeos vm range rule v4: %w", err)
|
||||||
|
}
|
||||||
|
if err = delDropCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
||||||
|
return fmt.Errorf("del drop cgnat range rule v4: %w", err)
|
||||||
|
}
|
||||||
|
case CGNATModeReturn:
|
||||||
|
if err = delReturnCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
||||||
|
return fmt.Errorf("del return cgnat range rule v4: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported mode %q", mode)
|
||||||
|
}
|
||||||
|
if err = conn.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("flush cgnat rules v4: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// createAcceptIncomingPacketRule creates a rule to accept incoming packets to
|
// createAcceptIncomingPacketRule creates a rule to accept incoming packets to
|
||||||
// the given interface.
|
// the given interface.
|
||||||
func createAcceptIncomingPacketRule(table *nftables.Table, chain *nftables.Chain, tunname string) *nftables.Rule {
|
func createAcceptIncomingPacketRule(table *nftables.Table, chain *nftables.Chain, tunname string) *nftables.Rule {
|
||||||
@@ -1555,12 +1702,6 @@ func (n *nftablesRunner) addBase4(tunname string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get input chain v4: %v", err)
|
return fmt.Errorf("get input chain v4: %v", err)
|
||||||
}
|
}
|
||||||
if err = addReturnChromeOSVMRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
|
||||||
return fmt.Errorf("add return chromeos vm range rule v4: %w", err)
|
|
||||||
}
|
|
||||||
if err = addDropCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
|
||||||
return fmt.Errorf("add drop cgnat range rule v4: %w", err)
|
|
||||||
}
|
|
||||||
if err = addAcceptIncomingPacketRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
if err = addAcceptIncomingPacketRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
|
||||||
return fmt.Errorf("add accept incoming packet rule v4: %w", err)
|
return fmt.Errorf("add accept incoming packet rule v4: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -633,7 +633,7 @@ func TestAddAndDelNetfilterChains(t *testing.T) {
|
|||||||
func getTsChains(
|
func getTsChains(
|
||||||
conn *nftables.Conn,
|
conn *nftables.Conn,
|
||||||
proto nftables.TableFamily) (*nftables.Chain, *nftables.Chain, *nftables.Chain, error) {
|
proto nftables.TableFamily) (*nftables.Chain, *nftables.Chain, *nftables.Chain, error) {
|
||||||
chains, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
chains, err := conn.ListChainsOfTableFamily(proto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, fmt.Errorf("list chains failed: %w", err)
|
return nil, nil, nil, fmt.Errorf("list chains failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -658,17 +658,7 @@ func findV4BaseRules(
|
|||||||
forwChain *nftables.Chain,
|
forwChain *nftables.Chain,
|
||||||
tunname string) ([]*nftables.Rule, error) {
|
tunname string) ([]*nftables.Rule, error) {
|
||||||
want := []*nftables.Rule{}
|
want := []*nftables.Rule{}
|
||||||
rule, err := createRangeRule(inpChain.Table, inpChain, tunname, tsaddr.ChromeOSVMRange(), expr.VerdictReturn)
|
rule, err := createDropOutgoingPacketFromCGNATRangeRuleWithTunname(forwChain.Table, forwChain, tunname)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create rule: %w", err)
|
|
||||||
}
|
|
||||||
want = append(want, rule)
|
|
||||||
rule, err = createRangeRule(inpChain.Table, inpChain, tunname, tsaddr.CGNATRange(), expr.VerdictDrop)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create rule: %w", err)
|
|
||||||
}
|
|
||||||
want = append(want, rule)
|
|
||||||
rule, err = createDropOutgoingPacketFromCGNATRangeRuleWithTunname(forwChain.Table, forwChain, tunname)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create rule: %w", err)
|
return nil, fmt.Errorf("create rule: %w", err)
|
||||||
}
|
}
|
||||||
@@ -745,7 +735,7 @@ func TestNFTAddAndDelNetfilterBase(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("getTsChains() failed: %v", err)
|
t.Fatalf("getTsChains() failed: %v", err)
|
||||||
}
|
}
|
||||||
checkChainRules(t, conn, inputV4, 3)
|
checkChainRules(t, conn, inputV4, 1)
|
||||||
checkChainRules(t, conn, forwardV4, 4)
|
checkChainRules(t, conn, forwardV4, 4)
|
||||||
checkChainRules(t, conn, postroutingV4, 0)
|
checkChainRules(t, conn, postroutingV4, 0)
|
||||||
|
|
||||||
@@ -763,8 +753,8 @@ func TestNFTAddAndDelNetfilterBase(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("getTsChains() failed: %v", err)
|
t.Fatalf("getTsChains() failed: %v", err)
|
||||||
}
|
}
|
||||||
checkChainRules(t, conn, inputV6, 3)
|
checkChainRules(t, conn, inputV6, 1)
|
||||||
checkChainRules(t, conn, forwardV6, 4)
|
checkChainRules(t, conn, forwardV6, 3)
|
||||||
checkChainRules(t, conn, postroutingV6, 0)
|
checkChainRules(t, conn, postroutingV6, 0)
|
||||||
|
|
||||||
_, err = findCommonBaseRules(conn, forwardV6, "testTunn")
|
_, err = findCommonBaseRules(conn, forwardV6, "testTunn")
|
||||||
@@ -783,6 +773,92 @@ func TestNFTAddAndDelNetfilterBase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findCGNATRules(
|
||||||
|
conn *nftables.Conn,
|
||||||
|
inpChain *nftables.Chain,
|
||||||
|
mode CGNATMode,
|
||||||
|
tunname string,
|
||||||
|
) error {
|
||||||
|
want := []*nftables.Rule{}
|
||||||
|
switch mode {
|
||||||
|
case CGNATModeDrop:
|
||||||
|
rule, err := createRangeRule(inpChain.Table, inpChain, tunname, tsaddr.ChromeOSVMRange(), expr.VerdictReturn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create rule: %w", err)
|
||||||
|
}
|
||||||
|
want = append(want, rule)
|
||||||
|
rule, err = createRangeRule(inpChain.Table, inpChain, tunname, tsaddr.CGNATRange(), expr.VerdictDrop)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create rule: %w", err)
|
||||||
|
}
|
||||||
|
want = append(want, rule)
|
||||||
|
case CGNATModeReturn:
|
||||||
|
rule, err := createRangeRule(inpChain.Table, inpChain, tunname, tsaddr.CGNATRange(), expr.VerdictReturn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create rule: %w", err)
|
||||||
|
}
|
||||||
|
want = append(want, rule)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown mode %q", mode)
|
||||||
|
}
|
||||||
|
for _, rule := range want {
|
||||||
|
_, err := findRule(conn, rule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find rule: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNFTAddAndDelCGNATRules(t *testing.T) {
|
||||||
|
modes := []CGNATMode{CGNATModeDrop, CGNATModeReturn}
|
||||||
|
for _, mode := range modes {
|
||||||
|
t.Run(string(mode), func(t *testing.T) {
|
||||||
|
conn := newSysConn(t)
|
||||||
|
|
||||||
|
runner := newFakeNftablesRunnerWithConn(t, conn, false)
|
||||||
|
|
||||||
|
if err := runner.AddChains(); err != nil {
|
||||||
|
t.Fatalf("AddChains() failed: %v", err)
|
||||||
|
}
|
||||||
|
defer runner.DelChains()
|
||||||
|
|
||||||
|
inputV4, _, _, err := getTsChains(conn, nftables.TableFamilyIPv4)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getTsChains() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkChainRules(t, conn, inputV4, 0)
|
||||||
|
|
||||||
|
tunname := "tun0"
|
||||||
|
|
||||||
|
if err := runner.AddExternalCGNATRules(mode, tunname); err != nil {
|
||||||
|
t.Fatalf("add rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case CGNATModeDrop:
|
||||||
|
checkChainRules(t, conn, inputV4, 2)
|
||||||
|
case CGNATModeReturn:
|
||||||
|
checkChainRules(t, conn, inputV4, 1)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unknown mode %q", mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := findCGNATRules(conn, inputV4, mode, tunname); err != nil {
|
||||||
|
t.Fatalf("find rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runner.DelExternalCGNATRules(mode, tunname); err != nil {
|
||||||
|
t.Fatalf("delete rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that all the rules have been deleted (0 remaining).
|
||||||
|
checkChainRules(t, conn, inputV4, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func findLoopBackRule(conn *nftables.Conn, proto nftables.TableFamily, table *nftables.Table, chain *nftables.Chain, addr netip.Addr) (*nftables.Rule, error) {
|
func findLoopBackRule(conn *nftables.Conn, proto nftables.TableFamily, table *nftables.Table, chain *nftables.Chain, addr netip.Addr) (*nftables.Rule, error) {
|
||||||
matchingAddr := addr.AsSlice()
|
matchingAddr := addr.AsSlice()
|
||||||
saddrExpr, err := newLoadSaddrExpr(proto, 1)
|
saddrExpr, err := newLoadSaddrExpr(proto, 1)
|
||||||
@@ -845,16 +921,16 @@ func TestNFTAddAndDelLoopbackRule(t *testing.T) {
|
|||||||
|
|
||||||
runner.AddBase("testTunn")
|
runner.AddBase("testTunn")
|
||||||
defer runner.DelBase()
|
defer runner.DelBase()
|
||||||
checkChainRules(t, conn, inputV4, 3)
|
checkChainRules(t, conn, inputV4, 1)
|
||||||
checkChainRules(t, conn, inputV6, 3)
|
checkChainRules(t, conn, inputV6, 1)
|
||||||
|
|
||||||
addr := netip.MustParseAddr("192.168.0.2")
|
addr := netip.MustParseAddr("192.168.0.2")
|
||||||
addrV6 := netip.MustParseAddr("2001:db8::2")
|
addrV6 := netip.MustParseAddr("2001:db8::2")
|
||||||
runner.AddLoopbackRule(addr)
|
runner.AddLoopbackRule(addr)
|
||||||
runner.AddLoopbackRule(addrV6)
|
runner.AddLoopbackRule(addrV6)
|
||||||
|
|
||||||
checkChainRules(t, conn, inputV4, 4)
|
checkChainRules(t, conn, inputV4, 2)
|
||||||
checkChainRules(t, conn, inputV6, 4)
|
checkChainRules(t, conn, inputV6, 2)
|
||||||
|
|
||||||
existingLoopBackRule, err := findLoopBackRule(conn, nftables.TableFamilyIPv4, runner.nft4.Filter, inputV4, addr)
|
existingLoopBackRule, err := findLoopBackRule(conn, nftables.TableFamilyIPv4, runner.nft4.Filter, inputV4, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -877,8 +953,8 @@ func TestNFTAddAndDelLoopbackRule(t *testing.T) {
|
|||||||
runner.DelLoopbackRule(addr)
|
runner.DelLoopbackRule(addr)
|
||||||
runner.DelLoopbackRule(addrV6)
|
runner.DelLoopbackRule(addrV6)
|
||||||
|
|
||||||
checkChainRules(t, conn, inputV4, 3)
|
checkChainRules(t, conn, inputV4, 1)
|
||||||
checkChainRules(t, conn, inputV6, 3)
|
checkChainRules(t, conn, inputV6, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNFTAddAndDelHookRule(t *testing.T) {
|
func TestNFTAddAndDelHookRule(t *testing.T) {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ type linuxRouter struct {
|
|||||||
connmarkEnabled bool // whether connmark rules are currently enabled
|
connmarkEnabled bool // whether connmark rules are currently enabled
|
||||||
netfilterMode preftype.NetfilterMode
|
netfilterMode preftype.NetfilterMode
|
||||||
netfilterKind string
|
netfilterKind string
|
||||||
|
cgnatMode linuxfw.CGNATMode
|
||||||
magicsockPortV4 uint16
|
magicsockPortV4 uint16
|
||||||
magicsockPortV6 uint16
|
magicsockPortV6 uint16
|
||||||
}
|
}
|
||||||
@@ -521,9 +522,50 @@ func (r *linuxRouter) Set(cfg *router.Config) error {
|
|||||||
r.enableIPForwarding()
|
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...)
|
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{
|
var dockerStatefulFilteringWarnable = health.Register(&health.Warnable{
|
||||||
Code: "docker-stateful-filtering",
|
Code: "docker-stateful-filtering",
|
||||||
Title: "Docker with 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -717,11 +717,11 @@ func (n *fakeIPTablesRunner) DeleteDNATRuleForSvc(svcName string, origDst, dst n
|
|||||||
return errors.New("not implemented")
|
return errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type iptRule struct{ chain, rule string }
|
||||||
|
|
||||||
func (n *fakeIPTablesRunner) addBase4(tunname string) error {
|
func (n *fakeIPTablesRunner) addBase4(tunname string) error {
|
||||||
curIPT := n.ipt4
|
curIPT := n.ipt4
|
||||||
newRules := []struct{ chain, rule string }{
|
newRules := []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())},
|
|
||||||
{"filter/ts-forward", fmt.Sprintf("-i %s -j MARK --set-mark %s/%s", tunname, tsconst.LinuxSubnetRouteMark, tsconst.LinuxFwmarkMask)},
|
{"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("-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())},
|
{"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 {
|
func (n *fakeIPTablesRunner) addBase6(tunname string) error {
|
||||||
curIPT := n.ipt6
|
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("-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("-m mark --mark %s/%s -j ACCEPT", tsconst.LinuxSubnetRouteMark, tsconst.LinuxFwmarkMask)},
|
||||||
{"filter/ts-forward", fmt.Sprintf("-o %s -j ACCEPT", tunname)},
|
{"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 {
|
func (n *fakeIPTablesRunner) AddHooks() error {
|
||||||
newRules := []struct{ chain, rule string }{
|
newRules := []iptRule{
|
||||||
{"filter/INPUT", "-j ts-input"},
|
{"filter/INPUT", "-j ts-input"},
|
||||||
{"filter/FORWARD", "-j ts-forward"},
|
{"filter/FORWARD", "-j ts-forward"},
|
||||||
{"nat/POSTROUTING", "-j ts-postrouting"},
|
{"nat/POSTROUTING", "-j ts-postrouting"},
|
||||||
@@ -778,7 +778,7 @@ func (n *fakeIPTablesRunner) AddHooks() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *fakeIPTablesRunner) DelHooks(logf logger.Logf) error {
|
func (n *fakeIPTablesRunner) DelHooks(logf logger.Logf) error {
|
||||||
delRules := []struct{ chain, rule string }{
|
delRules := []iptRule{
|
||||||
{"filter/INPUT", "-j ts-input"},
|
{"filter/INPUT", "-j ts-input"},
|
||||||
{"filter/FORWARD", "-j ts-forward"},
|
{"filter/FORWARD", "-j ts-forward"},
|
||||||
{"nat/POSTROUTING", "-j ts-postrouting"},
|
{"nat/POSTROUTING", "-j ts-postrouting"},
|
||||||
@@ -953,6 +953,48 @@ func (n *fakeIPTablesRunner) DelConnmarkSaveRule() error {
|
|||||||
return nil
|
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) HasIPV6() bool { return true }
|
||||||
func (n *fakeIPTablesRunner) HasIPV6NAT() bool { return true }
|
func (n *fakeIPTablesRunner) HasIPV6NAT() bool { return true }
|
||||||
func (n *fakeIPTablesRunner) HasIPV6Filter() bool { return true }
|
func (n *fakeIPTablesRunner) HasIPV6Filter() bool { return true }
|
||||||
|
|||||||
@@ -132,10 +132,11 @@ type Config struct {
|
|||||||
SubnetRoutes []netip.Prefix
|
SubnetRoutes []netip.Prefix
|
||||||
|
|
||||||
// Linux-only things below, ignored on other platforms.
|
// Linux-only things below, ignored on other platforms.
|
||||||
SNATSubnetRoutes bool // SNAT traffic to local subnets
|
SNATSubnetRoutes bool // SNAT traffic to local subnets
|
||||||
StatefulFiltering bool // Apply stateful filtering to inbound connections
|
StatefulFiltering bool // Apply stateful filtering to inbound connections
|
||||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||||
NetfilterKind string // what kind of netfilter to use ("nftables", "iptables", or "" to auto-detect)
|
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 {
|
func (a *Config) Equal(b *Config) bool {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func TestConfigEqual(t *testing.T) {
|
|||||||
testedFields := []string{
|
testedFields := []string{
|
||||||
"LocalAddrs", "Routes", "LocalRoutes", "NewMTU",
|
"LocalAddrs", "Routes", "LocalRoutes", "NewMTU",
|
||||||
"SubnetRoutes", "SNATSubnetRoutes", "StatefulFiltering",
|
"SubnetRoutes", "SNATSubnetRoutes", "StatefulFiltering",
|
||||||
"NetfilterMode", "NetfilterKind",
|
"NetfilterMode", "NetfilterKind", "RemoveCGNATDropRule",
|
||||||
}
|
}
|
||||||
configType := reflect.TypeFor[Config]()
|
configType := reflect.TypeFor[Config]()
|
||||||
configFields := []string{}
|
configFields := []string{}
|
||||||
|
|||||||
Reference in New Issue
Block a user