mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-19 19:38:40 +00:00
various: implement stateful firewalling on Linux (#12025)
Updates https://github.com/tailscale/corp/issues/19623 Change-Id: I7980e1fb736e234e66fa000d488066466c96ec85 Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Co-authored-by: Andrew Dunham <andrew@du.nham.ca>
This commit is contained in:
parent
5ef178fdca
commit
c28f5767bf
@ -655,12 +655,13 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
|||||||
goos: "linux",
|
goos: "linux",
|
||||||
args: upArgsFromOSArgs("linux"),
|
args: upArgsFromOSArgs("linux"),
|
||||||
want: &ipn.Prefs{
|
want: &ipn.Prefs{
|
||||||
ControlURL: ipn.DefaultControlURL,
|
ControlURL: ipn.DefaultControlURL,
|
||||||
WantRunning: true,
|
WantRunning: true,
|
||||||
NoSNAT: false,
|
NoSNAT: false,
|
||||||
NetfilterMode: preftype.NetfilterOn,
|
NoStatefulFiltering: "false",
|
||||||
CorpDNS: true,
|
NetfilterMode: preftype.NetfilterOn,
|
||||||
AllowSingleHosts: true,
|
CorpDNS: true,
|
||||||
|
AllowSingleHosts: true,
|
||||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||||
Check: true,
|
Check: true,
|
||||||
},
|
},
|
||||||
@ -694,7 +695,8 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
|||||||
netip.MustParsePrefix("0.0.0.0/0"),
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
netip.MustParsePrefix("::/0"),
|
netip.MustParsePrefix("::/0"),
|
||||||
},
|
},
|
||||||
NetfilterMode: preftype.NetfilterOn,
|
NoStatefulFiltering: "false",
|
||||||
|
NetfilterMode: preftype.NetfilterOn,
|
||||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||||
Check: true,
|
Check: true,
|
||||||
},
|
},
|
||||||
@ -781,9 +783,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
|
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
|
||||||
want: &ipn.Prefs{
|
want: &ipn.Prefs{
|
||||||
WantRunning: true,
|
WantRunning: true,
|
||||||
NetfilterMode: preftype.NetfilterNoDivert,
|
NetfilterMode: preftype.NetfilterNoDivert,
|
||||||
NoSNAT: true,
|
NoSNAT: true,
|
||||||
|
NoStatefulFiltering: "true",
|
||||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||||
Check: true,
|
Check: true,
|
||||||
},
|
},
|
||||||
@ -797,9 +800,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantWarn: "netfilter=off; configure iptables yourself.",
|
wantWarn: "netfilter=off; configure iptables yourself.",
|
||||||
want: &ipn.Prefs{
|
want: &ipn.Prefs{
|
||||||
WantRunning: true,
|
WantRunning: true,
|
||||||
NetfilterMode: preftype.NetfilterOff,
|
NetfilterMode: preftype.NetfilterOff,
|
||||||
NoSNAT: true,
|
NoSNAT: true,
|
||||||
|
NoStatefulFiltering: "true",
|
||||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||||
Check: true,
|
Check: true,
|
||||||
},
|
},
|
||||||
@ -813,8 +817,9 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
|||||||
netfilterMode: "off",
|
netfilterMode: "off",
|
||||||
},
|
},
|
||||||
want: &ipn.Prefs{
|
want: &ipn.Prefs{
|
||||||
WantRunning: true,
|
WantRunning: true,
|
||||||
NoSNAT: true,
|
NoSNAT: true,
|
||||||
|
NoStatefulFiltering: "true",
|
||||||
AdvertiseRoutes: []netip.Prefix{
|
AdvertiseRoutes: []netip.Prefix{
|
||||||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
|
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
|
||||||
},
|
},
|
||||||
@ -831,8 +836,9 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
|||||||
netfilterMode: "off",
|
netfilterMode: "off",
|
||||||
},
|
},
|
||||||
want: &ipn.Prefs{
|
want: &ipn.Prefs{
|
||||||
WantRunning: true,
|
WantRunning: true,
|
||||||
NoSNAT: true,
|
NoSNAT: true,
|
||||||
|
NoStatefulFiltering: "true",
|
||||||
AdvertiseRoutes: []netip.Prefix{
|
AdvertiseRoutes: []netip.Prefix{
|
||||||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112"),
|
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112"),
|
||||||
},
|
},
|
||||||
@ -1031,6 +1037,7 @@ func TestUpdatePrefs(t *testing.T) {
|
|||||||
HostnameSet: true,
|
HostnameSet: true,
|
||||||
NetfilterModeSet: true,
|
NetfilterModeSet: true,
|
||||||
NoSNATSet: true,
|
NoSNATSet: true,
|
||||||
|
NoStatefulFilteringSet: true,
|
||||||
OperatorUserSet: true,
|
OperatorUserSet: true,
|
||||||
RouteAllSet: true,
|
RouteAllSet: true,
|
||||||
RunSSHSet: true,
|
RunSSHSet: true,
|
||||||
|
@ -121,6 +121,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
|||||||
switch goos {
|
switch goos {
|
||||||
case "linux":
|
case "linux":
|
||||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||||
|
upf.BoolVar(&upArgs.statefulFiltering, "stateful-filtering", true, "apply stateful filtering to forwarded packets (subnet routers, exit nodes, etc.)")
|
||||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||||
case "windows":
|
case "windows":
|
||||||
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||||
@ -168,6 +169,7 @@ type upArgsT struct {
|
|||||||
advertiseTags string
|
advertiseTags string
|
||||||
advertiseConnector bool
|
advertiseConnector bool
|
||||||
snat bool
|
snat bool
|
||||||
|
statefulFiltering bool
|
||||||
netfilterMode string
|
netfilterMode string
|
||||||
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
||||||
hostname string
|
hostname string
|
||||||
@ -291,6 +293,9 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
|||||||
if goos == "linux" {
|
if goos == "linux" {
|
||||||
prefs.NoSNAT = !upArgs.snat
|
prefs.NoSNAT = !upArgs.snat
|
||||||
|
|
||||||
|
// Backfills for NoStatefulFiltering occur when loading a profile; just set it explicitly here.
|
||||||
|
prefs.NoStatefulFiltering.Set(!upArgs.statefulFiltering)
|
||||||
|
|
||||||
switch upArgs.netfilterMode {
|
switch upArgs.netfilterMode {
|
||||||
case "on":
|
case "on":
|
||||||
prefs.NetfilterMode = preftype.NetfilterOn
|
prefs.NetfilterMode = preftype.NetfilterOn
|
||||||
@ -711,6 +716,7 @@ func init() {
|
|||||||
addPrefFlagMapping("netfilter-mode", "NetfilterMode")
|
addPrefFlagMapping("netfilter-mode", "NetfilterMode")
|
||||||
addPrefFlagMapping("shields-up", "ShieldsUp")
|
addPrefFlagMapping("shields-up", "ShieldsUp")
|
||||||
addPrefFlagMapping("snat-subnet-routes", "NoSNAT")
|
addPrefFlagMapping("snat-subnet-routes", "NoSNAT")
|
||||||
|
addPrefFlagMapping("stateful-filtering", "NoStatefulFiltering")
|
||||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||||
addPrefFlagMapping("operator", "OperatorUser")
|
addPrefFlagMapping("operator", "OperatorUser")
|
||||||
@ -895,7 +901,7 @@ func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, env upCheckEnv) {
|
|||||||
|
|
||||||
func flagAppliesToOS(flag, goos string) bool {
|
func flagAppliesToOS(flag, goos string) bool {
|
||||||
switch flag {
|
switch flag {
|
||||||
case "netfilter-mode", "snat-subnet-routes":
|
case "netfilter-mode", "snat-subnet-routes", "stateful-filtering":
|
||||||
return goos == "linux"
|
return goos == "linux"
|
||||||
case "unattended":
|
case "unattended":
|
||||||
return goos == "windows"
|
return goos == "windows"
|
||||||
@ -970,6 +976,16 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
|||||||
set(prefs.AppConnector.Advertise)
|
set(prefs.AppConnector.Advertise)
|
||||||
case "snat-subnet-routes":
|
case "snat-subnet-routes":
|
||||||
set(!prefs.NoSNAT)
|
set(!prefs.NoSNAT)
|
||||||
|
case "stateful-filtering":
|
||||||
|
// We only set the stateful-filtering flag to false if
|
||||||
|
// the pref (negated!) is explicitly set to true; unset
|
||||||
|
// or false is treated as enabled.
|
||||||
|
val, ok := prefs.NoStatefulFiltering.Get()
|
||||||
|
if ok && val {
|
||||||
|
set(false)
|
||||||
|
} else {
|
||||||
|
set(true)
|
||||||
|
}
|
||||||
case "netfilter-mode":
|
case "netfilter-mode":
|
||||||
set(prefs.NetfilterMode.String())
|
set(prefs.NetfilterMode.String())
|
||||||
case "unattended":
|
case "unattended":
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/opt"
|
||||||
"tailscale.com/types/persist"
|
"tailscale.com/types/persist"
|
||||||
"tailscale.com/types/preftype"
|
"tailscale.com/types/preftype"
|
||||||
)
|
)
|
||||||
@ -57,6 +58,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
|||||||
Egg bool
|
Egg bool
|
||||||
AdvertiseRoutes []netip.Prefix
|
AdvertiseRoutes []netip.Prefix
|
||||||
NoSNAT bool
|
NoSNAT bool
|
||||||
|
NoStatefulFiltering opt.Bool
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
OperatorUser string
|
OperatorUser string
|
||||||
ProfileName string
|
ProfileName string
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/opt"
|
||||||
"tailscale.com/types/persist"
|
"tailscale.com/types/persist"
|
||||||
"tailscale.com/types/preftype"
|
"tailscale.com/types/preftype"
|
||||||
"tailscale.com/types/views"
|
"tailscale.com/types/views"
|
||||||
@ -86,6 +87,7 @@ func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] {
|
|||||||
return views.SliceOf(v.ж.AdvertiseRoutes)
|
return views.SliceOf(v.ж.AdvertiseRoutes)
|
||||||
}
|
}
|
||||||
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
|
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
|
||||||
|
func (v PrefsView) NoStatefulFiltering() opt.Bool { return v.ж.NoStatefulFiltering }
|
||||||
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
|
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
|
||||||
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
|
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
|
||||||
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
|
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
|
||||||
@ -120,6 +122,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
|||||||
Egg bool
|
Egg bool
|
||||||
AdvertiseRoutes []netip.Prefix
|
AdvertiseRoutes []netip.Prefix
|
||||||
NoSNAT bool
|
NoSNAT bool
|
||||||
|
NoStatefulFiltering opt.Bool
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
OperatorUser string
|
OperatorUser string
|
||||||
ProfileName string
|
ProfileName string
|
||||||
|
@ -4153,13 +4153,33 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs ipn.PrefsView, oneC
|
|||||||
netfilterKind = prefs.NetfilterKind()
|
netfilterKind = prefs.NetfilterKind()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var doStatefulFiltering bool
|
||||||
|
if v, ok := prefs.NoStatefulFiltering().Get(); !ok {
|
||||||
|
// The stateful filtering preference isn't explicitly set; this is
|
||||||
|
// unexpected since we expect it to be set during the profile
|
||||||
|
// backfill, but to be safe let's enable stateful filtering
|
||||||
|
// absent further information.
|
||||||
|
doStatefulFiltering = true
|
||||||
|
b.logf("[unexpected] NoStatefulFiltering preference not set; enabling stateful filtering")
|
||||||
|
} else if v {
|
||||||
|
// The preferences explicitly say "no stateful filtering", so
|
||||||
|
// we don't do it.
|
||||||
|
doStatefulFiltering = false
|
||||||
|
} else {
|
||||||
|
// The preferences explicitly "do stateful filtering" is turned
|
||||||
|
// off, or to expand the double negative, to do stateful
|
||||||
|
// filtering. Do so.
|
||||||
|
doStatefulFiltering = true
|
||||||
|
}
|
||||||
|
|
||||||
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(),
|
||||||
NetfilterMode: prefs.NetfilterMode(),
|
StatefulFiltering: doStatefulFiltering,
|
||||||
Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold),
|
NetfilterMode: prefs.NetfilterMode(),
|
||||||
NetfilterKind: netfilterKind,
|
Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold),
|
||||||
|
NetfilterKind: netfilterKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
if distro.Get() == distro.Synology {
|
if distro.Get() == distro.Synology {
|
||||||
|
@ -371,12 +371,39 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
|
|||||||
// https://github.com/tailscale/tailscale/pull/11814/commits/1613b18f8280c2bce786980532d012c9f0454fa2#diff-314ba0d799f70c8998940903efb541e511f352b39a9eeeae8d475c921d66c2ac
|
// https://github.com/tailscale/tailscale/pull/11814/commits/1613b18f8280c2bce786980532d012c9f0454fa2#diff-314ba0d799f70c8998940903efb541e511f352b39a9eeeae8d475c921d66c2ac
|
||||||
// prefs could set AutoUpdate.Apply=true via EditPrefs or tailnet
|
// prefs could set AutoUpdate.Apply=true via EditPrefs or tailnet
|
||||||
// auto-update defaults. After that change, such value is "invalid" and
|
// auto-update defaults. After that change, such value is "invalid" and
|
||||||
// cause any EditPrefs calls to fail (other than disabling autp-updates).
|
// cause any EditPrefs calls to fail (other than disabling auto-updates).
|
||||||
//
|
//
|
||||||
// Reset AutoUpdate.Apply if we detect such invalid prefs.
|
// Reset AutoUpdate.Apply if we detect such invalid prefs.
|
||||||
if savedPrefs.AutoUpdate.Apply.EqualBool(true) && !clientupdate.CanAutoUpdate() {
|
if savedPrefs.AutoUpdate.Apply.EqualBool(true) && !clientupdate.CanAutoUpdate() {
|
||||||
savedPrefs.AutoUpdate.Apply.Clear()
|
savedPrefs.AutoUpdate.Apply.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backfill a missing NoStatefulFiltering field based on the value of
|
||||||
|
// the NoSNAT field; we want to apply stateful filtering in all cases
|
||||||
|
// *except* where the user has disabled SNAT.
|
||||||
|
//
|
||||||
|
// Only backfill if the user hasn't set a value for
|
||||||
|
// NoStatefulFiltering, however.
|
||||||
|
_, haveNoStateful := savedPrefs.NoStatefulFiltering.Get()
|
||||||
|
if !haveNoStateful {
|
||||||
|
if savedPrefs.NoSNAT {
|
||||||
|
pm.logf("backfilling NoStatefulFiltering field to true because NoSNAT is set")
|
||||||
|
|
||||||
|
// No SNAT: no stateful filtering
|
||||||
|
savedPrefs.NoStatefulFiltering.Set(true)
|
||||||
|
} else {
|
||||||
|
pm.logf("backfilling NoStatefulFiltering field to false because NoSNAT is not set")
|
||||||
|
|
||||||
|
// SNAT (default): apply stateful filtering
|
||||||
|
savedPrefs.NoStatefulFiltering.Set(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to the preferences store now that we've updated it.
|
||||||
|
if err := pm.writePrefsToStore(key, savedPrefs.View()); err != nil {
|
||||||
|
return ipn.PrefsView{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return savedPrefs.View(), nil
|
return savedPrefs.View(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,6 +495,7 @@ var defaultPrefs = func() ipn.PrefsView {
|
|||||||
prefs := ipn.NewPrefs()
|
prefs := ipn.NewPrefs()
|
||||||
prefs.LoggedOut = true
|
prefs.LoggedOut = true
|
||||||
prefs.WantRunning = false
|
prefs.WantRunning = false
|
||||||
|
prefs.NoStatefulFiltering = "false"
|
||||||
|
|
||||||
return prefs.View()
|
return prefs.View()
|
||||||
}()
|
}()
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -12,12 +13,14 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"tailscale.com/clientupdate"
|
"tailscale.com/clientupdate"
|
||||||
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/store/mem"
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/types/opt"
|
||||||
"tailscale.com/types/persist"
|
"tailscale.com/types/persist"
|
||||||
"tailscale.com/util/must"
|
"tailscale.com/util/must"
|
||||||
)
|
)
|
||||||
@ -600,3 +603,86 @@ func TestProfileManagementWindows(t *testing.T) {
|
|||||||
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid)
|
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProfileBackfillStatefulFiltering(t *testing.T) {
|
||||||
|
envknob.Setenv("TS_DEBUG_PROFILES", "true")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
noSNAT bool
|
||||||
|
noStateful opt.Bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
// Default: NoSNAT is false, NoStatefulFiltering is false, so
|
||||||
|
// we want it to stay false.
|
||||||
|
{false, "false", false},
|
||||||
|
|
||||||
|
// NoSNAT being set to true and NoStatefulFiltering being false
|
||||||
|
// should result in NoStatefulFiltering still being false,
|
||||||
|
// since it was explicitly set.
|
||||||
|
{true, "false", false},
|
||||||
|
|
||||||
|
// If NoSNAT is false, and NoStatefulFiltering is unset, we
|
||||||
|
// backfill it to 'false'.
|
||||||
|
{false, "", false},
|
||||||
|
|
||||||
|
// If NoSNAT is true, and NoStatefulFiltering is unset, we
|
||||||
|
// backfill to 'true' to not break users of NoSNAT.
|
||||||
|
//
|
||||||
|
// In other words: if the user is not using SNAT, they almost
|
||||||
|
// certainly also don't want to use stateful filtering.
|
||||||
|
{true, "", true},
|
||||||
|
|
||||||
|
// However, if the user specifies both NoSNAT and stateful
|
||||||
|
// filtering, don't change that.
|
||||||
|
{true, "true", true},
|
||||||
|
{false, "true", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("noSNAT=%v,noStateful=%q", tt.noSNAT, tt.noStateful), func(t *testing.T) {
|
||||||
|
prefs := ipn.NewPrefs()
|
||||||
|
prefs.Persist = &persist.Persist{
|
||||||
|
NodeID: tailcfg.StableNodeID("node1"),
|
||||||
|
UserProfile: tailcfg.UserProfile{
|
||||||
|
ID: tailcfg.UserID(1),
|
||||||
|
LoginName: "user1@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.NoSNAT = tt.noSNAT
|
||||||
|
prefs.NoStatefulFiltering = tt.noStateful
|
||||||
|
|
||||||
|
// Make enough of a state store to load the prefs.
|
||||||
|
const profileName = "profile1"
|
||||||
|
bn := must.Get(json.Marshal(map[string]any{
|
||||||
|
string(ipn.CurrentProfileStateKey): []byte(profileName),
|
||||||
|
string(ipn.KnownProfilesStateKey): must.Get(json.Marshal(map[ipn.ProfileID]*ipn.LoginProfile{
|
||||||
|
profileName: {
|
||||||
|
ID: "profile1-id",
|
||||||
|
Key: profileName,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
profileName: prefs.ToBytes(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
store := new(mem.Store)
|
||||||
|
err := store.LoadFromJSON([]byte(bn))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ht := new(health.Tracker)
|
||||||
|
pm, err := newProfileManagerWithGOOS(store, t.Logf, ht, "linux")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current profile and verify that we backfilled our
|
||||||
|
// StatefulFiltering boolean.
|
||||||
|
pf := pm.CurrentPrefs()
|
||||||
|
if !pf.NoStatefulFiltering().EqualBool(tt.want) {
|
||||||
|
t.Fatalf("got NoStatefulFiltering=%v, want %v", pf.NoStatefulFiltering(), tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
24
ipn/prefs.go
24
ipn/prefs.go
@ -203,6 +203,21 @@ type Prefs struct {
|
|||||||
// Linux-only.
|
// Linux-only.
|
||||||
NoSNAT bool
|
NoSNAT bool
|
||||||
|
|
||||||
|
// NoStatefulFiltering specifies whether to apply stateful filtering
|
||||||
|
// when advertising routes in AdvertiseRoutes. The default is to apply
|
||||||
|
// stateful filtering.
|
||||||
|
//
|
||||||
|
// To allow inbound connections from advertised routes, both NoSNAT and
|
||||||
|
// NoStatefulFiltering must be true.
|
||||||
|
//
|
||||||
|
// This is an opt.Bool because it was added after NoSNAT, but is backfilled
|
||||||
|
// based on the value of that parameter. We need to treat it as a tristate:
|
||||||
|
// true, false, or unset, and backfill based on that value. See
|
||||||
|
// ipn/ipnlocal for more details on the backfill.
|
||||||
|
//
|
||||||
|
// Linux-only.
|
||||||
|
NoStatefulFiltering opt.Bool `json:",omitempty"`
|
||||||
|
|
||||||
// NetfilterMode specifies how much to manage netfilter rules for
|
// NetfilterMode specifies how much to manage netfilter rules for
|
||||||
// Tailscale, if at all.
|
// Tailscale, if at all.
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
@ -302,6 +317,7 @@ type MaskedPrefs struct {
|
|||||||
EggSet bool `json:",omitempty"`
|
EggSet bool `json:",omitempty"`
|
||||||
AdvertiseRoutesSet bool `json:",omitempty"`
|
AdvertiseRoutesSet bool `json:",omitempty"`
|
||||||
NoSNATSet bool `json:",omitempty"`
|
NoSNATSet bool `json:",omitempty"`
|
||||||
|
NoStatefulFilteringSet bool `json:",omitempty"`
|
||||||
NetfilterModeSet bool `json:",omitempty"`
|
NetfilterModeSet bool `json:",omitempty"`
|
||||||
OperatorUserSet bool `json:",omitempty"`
|
OperatorUserSet bool `json:",omitempty"`
|
||||||
ProfileNameSet bool `json:",omitempty"`
|
ProfileNameSet bool `json:",omitempty"`
|
||||||
@ -501,6 +517,13 @@ func (p *Prefs) pretty(goos string) string {
|
|||||||
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
|
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
|
||||||
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
|
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
|
||||||
}
|
}
|
||||||
|
if len(p.AdvertiseRoutes) > 0 || p.NoStatefulFiltering.EqualBool(true) {
|
||||||
|
// Only print if we're advertising any routes, or the user has
|
||||||
|
// turned off stateful filtering (NoStatefulFiltering=true ⇒
|
||||||
|
// StatefulFiltering=false).
|
||||||
|
bb, _ := p.NoStatefulFiltering.Get()
|
||||||
|
fmt.Fprintf(&sb, "statefulFiltering=%v ", !bb)
|
||||||
|
}
|
||||||
if len(p.AdvertiseTags) > 0 {
|
if len(p.AdvertiseTags) > 0 {
|
||||||
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
|
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
|
||||||
}
|
}
|
||||||
@ -569,6 +592,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
|||||||
p.NotepadURLs == p2.NotepadURLs &&
|
p.NotepadURLs == p2.NotepadURLs &&
|
||||||
p.ShieldsUp == p2.ShieldsUp &&
|
p.ShieldsUp == p2.ShieldsUp &&
|
||||||
p.NoSNAT == p2.NoSNAT &&
|
p.NoSNAT == p2.NoSNAT &&
|
||||||
|
p.NoStatefulFiltering == p2.NoStatefulFiltering &&
|
||||||
p.NetfilterMode == p2.NetfilterMode &&
|
p.NetfilterMode == p2.NetfilterMode &&
|
||||||
p.OperatorUser == p2.OperatorUser &&
|
p.OperatorUser == p2.OperatorUser &&
|
||||||
p.Hostname == p2.Hostname &&
|
p.Hostname == p2.Hostname &&
|
||||||
|
@ -56,6 +56,7 @@ func TestPrefsEqual(t *testing.T) {
|
|||||||
"Egg",
|
"Egg",
|
||||||
"AdvertiseRoutes",
|
"AdvertiseRoutes",
|
||||||
"NoSNAT",
|
"NoSNAT",
|
||||||
|
"NoStatefulFiltering",
|
||||||
"NetfilterMode",
|
"NetfilterMode",
|
||||||
"OperatorUser",
|
"OperatorUser",
|
||||||
"ProfileName",
|
"ProfileName",
|
||||||
|
@ -133,7 +133,8 @@ type CapabilityVersion int
|
|||||||
// - 90: 2024-04-03: Client understands PeerCapabilityTaildrive.
|
// - 90: 2024-04-03: Client understands PeerCapabilityTaildrive.
|
||||||
// - 91: 2024-04-24: Client understands PeerCapabilityTaildriveSharer.
|
// - 91: 2024-04-24: Client understands PeerCapabilityTaildriveSharer.
|
||||||
// - 92: 2024-05-06: Client understands NodeAttrUserDialUseRoutes.
|
// - 92: 2024-05-06: Client understands NodeAttrUserDialUseRoutes.
|
||||||
const CurrentCapabilityVersion CapabilityVersion = 92
|
// - 93: 2024-05-06: added support for stateful firewalling.
|
||||||
|
const CurrentCapabilityVersion CapabilityVersion = 93
|
||||||
|
|
||||||
type StableID string
|
type StableID string
|
||||||
|
|
||||||
|
@ -85,6 +85,15 @@ func (n *fakeIPTables) Delete(table, chain string, args ...string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *fakeIPTables) List(table, chain string) ([]string, error) {
|
||||||
|
k := table + "/" + chain
|
||||||
|
if rules, ok := n.n[k]; ok {
|
||||||
|
return rules, nil
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("unknown table/chain %s", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (n *fakeIPTables) ClearChain(table, chain string) error {
|
func (n *fakeIPTables) ClearChain(table, chain string) error {
|
||||||
k := table + "/" + chain
|
k := table + "/" + chain
|
||||||
if _, ok := n.n[k]; ok {
|
if _, ok := n.n[k]; ok {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ type iptablesInterface interface {
|
|||||||
Append(table, chain string, args ...string) error
|
Append(table, chain string, args ...string) error
|
||||||
Exists(table, chain string, args ...string) (bool, error)
|
Exists(table, chain string, args ...string) (bool, error)
|
||||||
Delete(table, chain string, args ...string) error
|
Delete(table, chain string, args ...string) error
|
||||||
|
List(table, chain string) ([]string, error)
|
||||||
ClearChain(table, chain string) error
|
ClearChain(table, chain string) error
|
||||||
NewChain(table, chain string) error
|
NewChain(table, chain string) error
|
||||||
DeleteChain(table, chain string) error
|
DeleteChain(table, chain string) error
|
||||||
@ -530,6 +532,67 @@ func (i *iptablesRunner) DelSNATRule() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statefulRuleArgs(tunname string) []string {
|
||||||
|
return []string{"-o", tunname, "-m", "conntrack", "!", "--ctstate", "ESTABLISHED,RELATED", "-j", "DROP"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStatefulRule adds a netfilter rule for stateful packet filtering using
|
||||||
|
// conntrack.
|
||||||
|
func (i *iptablesRunner) AddStatefulRule(tunname string) error {
|
||||||
|
// Drop packets that are destined for the tailscale interface if
|
||||||
|
// they're a new connection, per conntrack, to prevent hosts on the
|
||||||
|
// same subnet from being able to use this device as a way to forward
|
||||||
|
// packets on to the Tailscale network.
|
||||||
|
//
|
||||||
|
// The conntrack states are:
|
||||||
|
// NEW A packet which creates a new connection.
|
||||||
|
// ESTABLISHED A packet which belongs to an existing connection
|
||||||
|
// (i.e., a reply packet, or outgoing packet on a
|
||||||
|
// connection which has seen replies).
|
||||||
|
// RELATED A packet which is related to, but not part of, an
|
||||||
|
// existing connection, such as an ICMP error.
|
||||||
|
// INVALID A packet which could not be identified for some
|
||||||
|
// reason: this includes running out of memory and ICMP
|
||||||
|
// errors which don't correspond to any known
|
||||||
|
// connection. Generally these packets should be
|
||||||
|
// dropped.
|
||||||
|
//
|
||||||
|
// We drop NEW packets to prevent connections from coming "into"
|
||||||
|
// Tailscale from other hosts on the same network segment; we drop
|
||||||
|
// INVALID packets as well.
|
||||||
|
args := statefulRuleArgs(tunname)
|
||||||
|
for _, ipt := range i.getTables() {
|
||||||
|
// First, find the final "accept" rule.
|
||||||
|
rules, err := ipt.List("filter", "ts-forward")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listing rules in filter/ts-forward: %w", err)
|
||||||
|
}
|
||||||
|
want := fmt.Sprintf("-A %s -o %s -j ACCEPT", "ts-forward", tunname)
|
||||||
|
|
||||||
|
pos := slices.Index(rules, want)
|
||||||
|
if pos < 0 {
|
||||||
|
return fmt.Errorf("couldn't find final ACCEPT rule in filter/ts-forward")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ipt.Insert("filter", "ts-forward", pos, args...); err != nil {
|
||||||
|
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelStatefulRule removes the netfilter rule for stateful packet filtering
|
||||||
|
// using conntrack.
|
||||||
|
func (i *iptablesRunner) DelStatefulRule(tunname string) error {
|
||||||
|
args := statefulRuleArgs(tunname)
|
||||||
|
for _, ipt := range i.getTables() {
|
||||||
|
if err := ipt.Delete("filter", "ts-forward", args...); err != nil {
|
||||||
|
return fmt.Errorf("deleting %v in filter/ts-forward: %w", args, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// buildMagicsockPortRule generates the string slice containing the arguments
|
// buildMagicsockPortRule generates the string slice containing the arguments
|
||||||
// to describe a rule accepting traffic on a particular port to iptables. It is
|
// to describe a rule accepting traffic on a particular port to iptables. It is
|
||||||
// separated out here to avoid repetition in AddMagicsockPortRule and
|
// separated out here to avoid repetition in AddMagicsockPortRule and
|
||||||
|
@ -514,6 +514,14 @@ type NetfilterRunner interface {
|
|||||||
// DelSNATRule removes the rule added by AddSNATRule.
|
// DelSNATRule removes the rule added by AddSNATRule.
|
||||||
DelSNATRule() error
|
DelSNATRule() error
|
||||||
|
|
||||||
|
// AddStatefulRule adds a netfilter rule for stateful packet filtering
|
||||||
|
// using conntrack.
|
||||||
|
AddStatefulRule(tunname string) error
|
||||||
|
|
||||||
|
// DelStatefulRule removes a netfilter rule for stateful packet filtering
|
||||||
|
// using conntrack.
|
||||||
|
DelStatefulRule(tunname string) error
|
||||||
|
|
||||||
// HasIPV6 reports true if the system supports IPv6.
|
// HasIPV6 reports true if the system supports IPv6.
|
||||||
HasIPV6() bool
|
HasIPV6() bool
|
||||||
|
|
||||||
@ -1748,6 +1756,194 @@ func (n *nftablesRunner) DelSNATRule() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nativeUint32(v uint32) []byte {
|
||||||
|
b := make([]byte, 4)
|
||||||
|
binary.NativeEndian.PutUint32(b, v)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeStatefulRuleExprs(tunname string) []expr.Any {
|
||||||
|
return []expr.Any{
|
||||||
|
// Check if the output interface is the Tailscale interface by
|
||||||
|
// first loding the OIFNAME into register 1 and comparing it
|
||||||
|
// against our tunname.
|
||||||
|
//
|
||||||
|
// 'cmp' implicitly breaks from a rule if a comparison fails,
|
||||||
|
// so if we continue past this rule we know that the packet is
|
||||||
|
// going to our TUN.
|
||||||
|
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpNeq,
|
||||||
|
Register: 1,
|
||||||
|
Data: []byte(tunname),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Store the conntrack state in register 1
|
||||||
|
&expr.Ct{
|
||||||
|
Register: 1,
|
||||||
|
Key: expr.CtKeySTATE,
|
||||||
|
},
|
||||||
|
// Mask the state in register 1 to "hide" the ESTABLISHED and
|
||||||
|
// RELATED bits (which are expected and fine); if there are any
|
||||||
|
// other bits, we want them to remain.
|
||||||
|
//
|
||||||
|
// This operation is, in the kernel:
|
||||||
|
// dst[i] = (src[i] & mask[i]) ^ xor[i]
|
||||||
|
//
|
||||||
|
// So, we can mask by setting the inverse of the bits we want
|
||||||
|
// to remove; i.e. ESTABLISHED = 0b00000010, RELATED =
|
||||||
|
// 0b00000100, so, if we assume an 8-bit state (in reality,
|
||||||
|
// it's 32-bit), we can mask with 0b11111001 to clear those
|
||||||
|
// bits and keep everything else (e.g. the INVALID bit which is
|
||||||
|
// 0b00000001).
|
||||||
|
//
|
||||||
|
// TODO(andrew-d): for now, let's also allow
|
||||||
|
// CtStateBitUNTRACKED, which is a state for packets that are not
|
||||||
|
// tracked (marked so explicitly with an iptables rule using
|
||||||
|
// --notrack); we should figure out if we want to allow this or not.
|
||||||
|
&expr.Bitwise{
|
||||||
|
SourceRegister: 1,
|
||||||
|
DestRegister: 1,
|
||||||
|
Len: 4,
|
||||||
|
Mask: nativeUint32(^(0 |
|
||||||
|
expr.CtStateBitESTABLISHED |
|
||||||
|
expr.CtStateBitRELATED |
|
||||||
|
expr.CtStateBitUNTRACKED)),
|
||||||
|
|
||||||
|
// Xor is unused but must be specified
|
||||||
|
Xor: nativeUint32(0),
|
||||||
|
},
|
||||||
|
// Compare against the expected state (0, i.e. no bits set
|
||||||
|
// other than maybe ESTABLISHED and RELATED). We want this
|
||||||
|
// comparison to fail if there are no bits set, so that this
|
||||||
|
// rule's evaluation stops and we don't fall through to the
|
||||||
|
// "Drop" verdict.
|
||||||
|
//
|
||||||
|
// For example, if the state is ESTABLISHED (and we want to
|
||||||
|
// break from this rule/accept this packet):
|
||||||
|
// state = ESTABLISHED
|
||||||
|
// register1 = 0b0 (since the bitwise operation cleared the ESTABLISHED bit)
|
||||||
|
//
|
||||||
|
// compare register1 (0b0) != 0: false
|
||||||
|
// -> comparison implicitly breaks
|
||||||
|
// -> continue to the next rule
|
||||||
|
//
|
||||||
|
// For example, if the state is NEW (and we want to continue to
|
||||||
|
// the next expression and thus drop this packet):
|
||||||
|
// state = NEW
|
||||||
|
// register1 = 0b1000
|
||||||
|
//
|
||||||
|
// compare register1 (0b1000) != 0: true
|
||||||
|
// -> comparison continues to next expr
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpNeq,
|
||||||
|
Register: 1,
|
||||||
|
Data: []byte{0, 0, 0, 0},
|
||||||
|
},
|
||||||
|
// If we get here, we know that this packet is going to our TUN
|
||||||
|
// device, and has a conntrack state set other than ESTABLISHED
|
||||||
|
// or RELATED. We thus count and drop the packet.
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Verdict{Kind: expr.VerdictDrop},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(andrew-d): iptables-nft writes a rule that dumps as:
|
||||||
|
//
|
||||||
|
// match name conntrack rev 3
|
||||||
|
//
|
||||||
|
// I think this is using expr.Match against the following struct
|
||||||
|
// (xt_conntrack_mtinfo3):
|
||||||
|
//
|
||||||
|
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/netfilter/xt_conntrack.h#L64-L77
|
||||||
|
//
|
||||||
|
// We could probably do something similar here, but I'm not sure if
|
||||||
|
// there's any advantage. Below is an example Match statement if we
|
||||||
|
// decide to do that, based on dumping the rule that iptables-nft
|
||||||
|
// generates:
|
||||||
|
//
|
||||||
|
// _ = expr.Match{
|
||||||
|
// Name: "conntrack",
|
||||||
|
// Rev: 3,
|
||||||
|
// Info: &xt.ConntrackMtinfo3{
|
||||||
|
// ConntrackMtinfo2: xt.ConntrackMtinfo2{
|
||||||
|
// ConntrackMtinfoBase: xt.ConntrackMtinfoBase{
|
||||||
|
// MatchFlags: xt.ConntrackState,
|
||||||
|
// InvertFlags: xt.ConntrackState,
|
||||||
|
// },
|
||||||
|
// // Mask the state to remove ESTABLISHED and
|
||||||
|
// // RELATED before comparing.
|
||||||
|
// StateMask: expr.CtStateBitESTABLISHED | expr.CtStateBitRELATED,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStatefulRule adds a netfilter rule for stateful packet filtering using
|
||||||
|
// conntrack.
|
||||||
|
func (n *nftablesRunner) AddStatefulRule(tunname string) error {
|
||||||
|
conn := n.conn
|
||||||
|
|
||||||
|
exprs := makeStatefulRuleExprs(tunname)
|
||||||
|
for _, table := range n.getTables() {
|
||||||
|
chain, err := getChainFromTable(conn, table.Filter, chainNameForward)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get forward chain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, find the 'accept' rule that we want to insert our rule before.
|
||||||
|
acceptRule := createAcceptOutgoingPacketRule(table.Filter, chain, tunname)
|
||||||
|
rule, err := findRule(conn, acceptRule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find accept rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.InsertRule(&nftables.Rule{
|
||||||
|
Table: table.Filter,
|
||||||
|
Chain: chain,
|
||||||
|
Exprs: exprs,
|
||||||
|
|
||||||
|
// Specifying Position in an Insert operation means to
|
||||||
|
// insert this rule before the specified rule.
|
||||||
|
Position: rule.Handle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("flush add stateful rule: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelStatefulRule removes the netfilter rule for stateful packet filtering
|
||||||
|
// using conntrack.
|
||||||
|
func (n *nftablesRunner) DelStatefulRule(tunname string) error {
|
||||||
|
conn := n.conn
|
||||||
|
|
||||||
|
exprs := makeStatefulRuleExprs(tunname)
|
||||||
|
for _, table := range n.getTables() {
|
||||||
|
chain, err := getChainFromTable(conn, table.Filter, chainNameForward)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get forward chain: %w", err)
|
||||||
|
}
|
||||||
|
rule, err := findRule(conn, &nftables.Rule{
|
||||||
|
Table: table.Nat,
|
||||||
|
Chain: chain,
|
||||||
|
Exprs: exprs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find stateful rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule != nil {
|
||||||
|
conn.DelRule(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := conn.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("flush del stateful rule: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// cleanupChain removes a jump rule from hookChainName to tsChainName, and then
|
// cleanupChain removes a jump rule from hookChainName to tsChainName, and then
|
||||||
// the entire chain tsChainName. Errors are logged, but attempts to remove both
|
// the entire chain tsChainName. Errors are logged, but attempts to remove both
|
||||||
// the jump rule and chain continue even if one errors.
|
// the jump rule and chain continue even if one errors.
|
||||||
|
@ -88,9 +88,10 @@ 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
|
||||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
StatefulFiltering bool // Apply stateful filtering to inbound connections
|
||||||
NetfilterKind string // what kind of netfilter to use (nftables, iptables)
|
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||||
|
NetfilterKind string // what kind of netfilter to use (nftables, iptables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Config) Equal(b *Config) bool {
|
func (a *Config) Equal(b *Config) bool {
|
||||||
|
@ -38,17 +38,18 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type linuxRouter struct {
|
type linuxRouter struct {
|
||||||
closed atomic.Bool
|
closed atomic.Bool
|
||||||
logf func(fmt string, args ...any)
|
logf func(fmt string, args ...any)
|
||||||
tunname string
|
tunname string
|
||||||
netMon *netmon.Monitor
|
netMon *netmon.Monitor
|
||||||
unregNetMon func()
|
unregNetMon func()
|
||||||
addrs map[netip.Prefix]bool
|
addrs map[netip.Prefix]bool
|
||||||
routes map[netip.Prefix]bool
|
routes map[netip.Prefix]bool
|
||||||
localRoutes map[netip.Prefix]bool
|
localRoutes map[netip.Prefix]bool
|
||||||
snatSubnetRoutes bool
|
snatSubnetRoutes bool
|
||||||
netfilterMode preftype.NetfilterMode
|
statefulFiltering bool
|
||||||
netfilterKind string
|
netfilterMode preftype.NetfilterMode
|
||||||
|
netfilterKind string
|
||||||
|
|
||||||
// ruleRestorePending is whether a timer has been started to
|
// ruleRestorePending is whether a timer has been started to
|
||||||
// restore deleted ip rules.
|
// restore deleted ip rules.
|
||||||
@ -390,6 +391,7 @@ func (r *linuxRouter) Set(cfg *Config) error {
|
|||||||
}
|
}
|
||||||
r.addrs = newAddrs
|
r.addrs = newAddrs
|
||||||
|
|
||||||
|
// Ensure that the SNAT rule is added or removed as needed.
|
||||||
switch {
|
switch {
|
||||||
case cfg.SNATSubnetRoutes == r.snatSubnetRoutes:
|
case cfg.SNATSubnetRoutes == r.snatSubnetRoutes:
|
||||||
// state already correct, nothing to do.
|
// state already correct, nothing to do.
|
||||||
@ -404,6 +406,21 @@ func (r *linuxRouter) Set(cfg *Config) error {
|
|||||||
}
|
}
|
||||||
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
|
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
|
||||||
|
|
||||||
|
// As above, for stateful filtering
|
||||||
|
switch {
|
||||||
|
case cfg.StatefulFiltering == r.statefulFiltering:
|
||||||
|
// state already correct, nothing to do.
|
||||||
|
case cfg.StatefulFiltering:
|
||||||
|
if err := r.addStatefulRule(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := r.delStatefulRule(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.statefulFiltering = cfg.StatefulFiltering
|
||||||
|
|
||||||
// Issue 11405: enable IP forwarding on gokrazy.
|
// Issue 11405: enable IP forwarding on gokrazy.
|
||||||
advertisingRoutes := len(cfg.SubnetRoutes) > 0
|
advertisingRoutes := len(cfg.SubnetRoutes) > 0
|
||||||
if distro.Get() == distro.Gokrazy && advertisingRoutes {
|
if distro.Get() == distro.Gokrazy && advertisingRoutes {
|
||||||
@ -1327,6 +1344,26 @@ func (r *linuxRouter) delSNATRule() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addStatefulRule adds a netfilter rule to perform stateful filtering from
|
||||||
|
// subnets onto the tailnet.
|
||||||
|
func (r *linuxRouter) addStatefulRule() error {
|
||||||
|
if r.netfilterMode == netfilterOff {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.nfr.AddStatefulRule(r.tunname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delStatefulRule removes the netfilter rule to perform stateful filtering
|
||||||
|
// from subnets onto the tailnet.
|
||||||
|
func (r *linuxRouter) delStatefulRule() error {
|
||||||
|
if r.netfilterMode == netfilterOff {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.nfr.DelStatefulRule(r.tunname)
|
||||||
|
}
|
||||||
|
|
||||||
// cidrDiff calls add and del as needed to make the set of prefixes in
|
// cidrDiff calls add and del as needed to make the set of prefixes in
|
||||||
// old and new match. Returns a map reflecting the actual new state
|
// old and new match. Returns a map reflecting the actual new state
|
||||||
// (which may be somewhere in between old and new if some commands
|
// (which may be somewhere in between old and new if some commands
|
||||||
|
@ -94,11 +94,49 @@ ip route add 192.168.16.0/24 dev tailscale0 table 52` + basic,
|
|||||||
{
|
{
|
||||||
name: "addr and routes and subnet routes with netfilter",
|
name: "addr and routes and subnet routes with netfilter",
|
||||||
in: &Config{
|
in: &Config{
|
||||||
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
||||||
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
||||||
SubnetRoutes: mustCIDRs("200.0.0.0/8"),
|
SubnetRoutes: mustCIDRs("200.0.0.0/8"),
|
||||||
SNATSubnetRoutes: true,
|
SNATSubnetRoutes: true,
|
||||||
NetfilterMode: netfilterOn,
|
StatefulFiltering: true,
|
||||||
|
NetfilterMode: netfilterOn,
|
||||||
|
},
|
||||||
|
want: `
|
||||||
|
up
|
||||||
|
ip addr add 100.101.102.104/10 dev tailscale0
|
||||||
|
ip route add 10.0.0.0/8 dev tailscale0 table 52
|
||||||
|
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
|
||||||
|
`v4/filter/FORWARD -j ts-forward
|
||||||
|
v4/filter/INPUT -j ts-input
|
||||||
|
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000/0xff0000
|
||||||
|
v4/filter/ts-forward -m mark --mark 0x40000/0xff0000 -j ACCEPT
|
||||||
|
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
|
||||||
|
v4/filter/ts-forward -o tailscale0 -m conntrack ! --ctstate ESTABLISHED,RELATED -j DROP
|
||||||
|
v4/filter/ts-forward -o tailscale0 -j ACCEPT
|
||||||
|
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
|
||||||
|
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
|
||||||
|
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
|
||||||
|
v4/nat/POSTROUTING -j ts-postrouting
|
||||||
|
v4/nat/ts-postrouting -m mark --mark 0x40000/0xff0000 -j MASQUERADE
|
||||||
|
v6/filter/FORWARD -j ts-forward
|
||||||
|
v6/filter/INPUT -j ts-input
|
||||||
|
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000/0xff0000
|
||||||
|
v6/filter/ts-forward -m mark --mark 0x40000/0xff0000 -j ACCEPT
|
||||||
|
v6/filter/ts-forward -o tailscale0 -m conntrack ! --ctstate ESTABLISHED,RELATED -j DROP
|
||||||
|
v6/filter/ts-forward -o tailscale0 -j ACCEPT
|
||||||
|
v6/nat/POSTROUTING -j ts-postrouting
|
||||||
|
v6/nat/ts-postrouting -m mark --mark 0x40000/0xff0000 -j MASQUERADE
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "addr and routes and subnet routes with netfilter but no stateful filtering",
|
||||||
|
in: &Config{
|
||||||
|
LocalAddrs: mustCIDRs("100.101.102.104/10"),
|
||||||
|
Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"),
|
||||||
|
SubnetRoutes: mustCIDRs("200.0.0.0/8"),
|
||||||
|
SNATSubnetRoutes: true,
|
||||||
|
StatefulFiltering: false,
|
||||||
|
NetfilterMode: netfilterOn,
|
||||||
},
|
},
|
||||||
want: `
|
want: `
|
||||||
up
|
up
|
||||||
@ -411,6 +449,22 @@ func insertRule(n *fakeIPTablesRunner, curIPT map[string][]string, chain, newRul
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insertRuleAt(n *fakeIPTablesRunner, curIPT map[string][]string, chain string, pos int, newRule string) {
|
||||||
|
rules, ok := curIPT[chain]
|
||||||
|
if !ok {
|
||||||
|
n.t.Fatalf("no %s chain exists", chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the given position is after the end of the chain, error.
|
||||||
|
if pos > len(rules) {
|
||||||
|
n.t.Fatalf("position %d > len(chain %s) %d", pos, chain, len(chain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the rule at the given position
|
||||||
|
rules = slices.Insert(rules, pos, newRule)
|
||||||
|
curIPT[chain] = rules
|
||||||
|
}
|
||||||
|
|
||||||
func appendRule(n *fakeIPTablesRunner, curIPT map[string][]string, chain, newRule string) error {
|
func appendRule(n *fakeIPTablesRunner, curIPT map[string][]string, chain, newRule string) error {
|
||||||
// Get current rules for filter/ts-input chain with according IP version
|
// Get current rules for filter/ts-input chain with according IP version
|
||||||
curTSInputRules, ok := curIPT[chain]
|
curTSInputRules, ok := curIPT[chain]
|
||||||
@ -611,6 +665,33 @@ func (n *fakeIPTablesRunner) DelSNATRule() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *fakeIPTablesRunner) AddStatefulRule(tunname string) error {
|
||||||
|
newRule := fmt.Sprintf("-o %s -m conntrack ! --ctstate ESTABLISHED,RELATED -j DROP", tunname)
|
||||||
|
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||||
|
// Mimic the real runner and insert after the 'accept all' rule
|
||||||
|
wantRule := fmt.Sprintf("-o %s -j ACCEPT", tunname)
|
||||||
|
|
||||||
|
const chain = "filter/ts-forward"
|
||||||
|
pos := slices.Index(ipt[chain], wantRule)
|
||||||
|
if pos < 0 {
|
||||||
|
n.t.Fatalf("no rule %q in chain %s", wantRule, chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
insertRuleAt(n, ipt, chain, pos, newRule)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *fakeIPTablesRunner) DelStatefulRule(tunname string) error {
|
||||||
|
delRule := fmt.Sprintf("-o %s -m conntrack ! --ctstate ESTABLISHED,RELATED -j DROP", tunname)
|
||||||
|
for _, ipt := range []map[string][]string{n.ipt4, n.ipt6} {
|
||||||
|
if err := deleteRule(n, ipt, "filter/ts-forward", delRule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// buildMagicsockPortRule builds a fake rule to use in AddMagicsockPortRule and
|
// buildMagicsockPortRule builds a fake rule to use in AddMagicsockPortRule and
|
||||||
// DelMagicsockPortRule below.
|
// DelMagicsockPortRule below.
|
||||||
func buildMagicsockPortRule(port uint16) string {
|
func buildMagicsockPortRule(port uint16) string {
|
||||||
|
@ -23,8 +23,8 @@ func mustCIDRs(ss ...string) []netip.Prefix {
|
|||||||
func TestConfigEqual(t *testing.T) {
|
func TestConfigEqual(t *testing.T) {
|
||||||
testedFields := []string{
|
testedFields := []string{
|
||||||
"LocalAddrs", "Routes", "LocalRoutes", "NewMTU",
|
"LocalAddrs", "Routes", "LocalRoutes", "NewMTU",
|
||||||
"SubnetRoutes", "SNATSubnetRoutes", "NetfilterMode",
|
"SubnetRoutes", "SNATSubnetRoutes", "StatefulFiltering",
|
||||||
"NetfilterKind",
|
"NetfilterMode", "NetfilterKind",
|
||||||
}
|
}
|
||||||
configType := reflect.TypeFor[Config]()
|
configType := reflect.TypeFor[Config]()
|
||||||
configFields := []string{}
|
configFields := []string{}
|
||||||
@ -125,6 +125,16 @@ func TestConfigEqual(t *testing.T) {
|
|||||||
&Config{SNATSubnetRoutes: false},
|
&Config{SNATSubnetRoutes: false},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
&Config{StatefulFiltering: false},
|
||||||
|
&Config{StatefulFiltering: true},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Config{StatefulFiltering: false},
|
||||||
|
&Config{StatefulFiltering: false},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
&Config{NetfilterMode: preftype.NetfilterOff},
|
&Config{NetfilterMode: preftype.NetfilterOff},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user