linuxfw,wgengine/route,ipn: add c2n and nodeattrs to control linux netfilter

Updates tailscale/corp#14029.

Signed-off-by: Naman Sood <mail@nsood.in>
pull/10370/head
Naman Sood 12 months ago
parent 215f657a5e
commit 0a59754eda

@ -91,7 +91,7 @@ func newNetfilterRunner(logf logger.Logf) (linuxfw.NetfilterRunner, error) {
if defaultBool("TS_TEST_FAKE_NETFILTER", false) {
return linuxfw.NewFakeIPTablesRunner(), nil
}
return linuxfw.New(logf)
return linuxfw.New(logf, "")
}
func main() {

@ -813,6 +813,10 @@ func TestPrefFlagMapping(t *testing.T) {
case "RunWebClient":
// TODO(tailscale/corp#14335): Currently behind a feature flag.
continue
case "NetfilterKind":
// Handled by TS_DEBUG_FIREWALL_MODE env var, we don't want to have
// a CLI flag for this. The Pref is used by c2n.
continue
}
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
}

@ -56,6 +56,14 @@ type Knobs struct {
// SilentDisco is whether the node should suppress disco heartbeats to its
// peers.
SilentDisco atomic.Bool
// LinuxForceIPTables is whether the node should use iptables for Linux
// netfiltering, unless overridden by the user.
LinuxForceIPTables atomic.Bool
// LinuxForceNfTables is whether the node should use nftables for Linux
// netfiltering, unless overridden by the user.
LinuxForceNfTables atomic.Bool
}
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
@ -79,6 +87,8 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability,
peerMTUEnable = has(tailcfg.NodeAttrPeerMTUEnable)
dnsForwarderDisableTCPRetries = has(tailcfg.NodeAttrDNSForwarderDisableTCPRetries)
silentDisco = has(tailcfg.NodeAttrSilentDisco)
forceIPTables = has(tailcfg.NodeAttrLinuxMustUseIPTables)
forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables)
)
if has(tailcfg.NodeAttrOneCGNATEnable) {
@ -97,6 +107,8 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability,
k.PeerMTUEnable.Store(peerMTUEnable)
k.DisableDNSForwarderTCPRetries.Store(dnsForwarderDisableTCPRetries)
k.SilentDisco.Store(silentDisco)
k.LinuxForceIPTables.Store(forceIPTables)
k.LinuxForceNfTables.Store(forceNfTables)
}
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
@ -116,5 +128,7 @@ func (k *Knobs) AsDebugJSON() map[string]any {
"PeerMTUEnable": k.PeerMTUEnable.Load(),
"DisableDNSForwarderTCPRetries": k.DisableDNSForwarderTCPRetries.Load(),
"SilentDisco": k.SilentDisco.Load(),
"LinuxForceIPTables": k.LinuxForceIPTables.Load(),
"LinuxForceNfTables": k.LinuxForceNfTables.Load(),
}
}

@ -55,6 +55,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
AutoUpdate AutoUpdatePrefs
AppConnector AppConnectorPrefs
PostureChecking bool
NetfilterKind string
Persist *persist.Persist
}{})

@ -90,6 +90,7 @@ func (v PrefsView) ProfileName() string { return v.ж.ProfileN
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector }
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind }
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
@ -119,6 +120,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
AutoUpdate AutoUpdatePrefs
AppConnector AppConnectorPrefs
PostureChecking bool
NetfilterKind string
Persist *persist.Persist
}{})

@ -69,6 +69,9 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
// App Connectors.
req("GET /appconnector/routes"): handleC2NAppConnectorDomainRoutesGet,
// Linux netfilter.
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
}
type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request)
@ -222,6 +225,32 @@ func handleC2NAppConnectorDomainRoutesGet(b *LocalBackend, w http.ResponseWriter
json.NewEncoder(w).Encode(res)
}
func handleC2NSetNetfilterKind(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.logf("c2n: POST /netfilter-kind received")
if version.OS() != "linux" {
http.Error(w, "netfilter kind only settable on linux", http.StatusNotImplemented)
}
kind := r.FormValue("kind")
b.logf("c2n: switching netfilter to %s", kind)
_, err := b.EditPrefs(&ipn.MaskedPrefs{
NetfilterKindSet: true,
Prefs: ipn.Prefs{
NetfilterKind: kind,
},
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
b.authReconfig()
w.WriteHeader(http.StatusNoContent)
}
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.logf("c2n: GET /update received")

@ -271,6 +271,9 @@ type LocalBackend struct {
currentUser ipnauth.WindowsToken
selfUpdateProgress []ipnstate.UpdateProgress
lastSelfUpdateState ipnstate.SelfUpdateStatus
// capForcedNetfilter is the netfilter that control instructs Linux clients
// to use, unless overridden locally.
capForcedNetfilter string
// ServeConfig fields. (also guarded by mu)
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
@ -3901,12 +3904,21 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs ipn.PrefsView, oneC
singleRouteThreshold = 1
}
netfilterKind := b.capForcedNetfilter
if prefs.NetfilterKind() != "" {
if b.capForcedNetfilter != "" {
b.logf("nodeattr netfilter preference %s overridden by c2n pref %s", b.capForcedNetfilter, prefs.NetfilterKind())
}
netfilterKind = prefs.NetfilterKind()
}
rs := &router.Config{
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()),
SNATSubnetRoutes: !prefs.NoSNAT(),
NetfilterMode: prefs.NetfilterMode(),
Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold),
NetfilterKind: netfilterKind,
}
if distro.Get() == distro.Synology {
@ -4416,6 +4428,14 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
}
b.capFileSharing = fs
if hasCapability(nm, tailcfg.NodeAttrLinuxMustUseIPTables) {
b.capForcedNetfilter = "iptables"
} else if hasCapability(nm, tailcfg.NodeAttrLinuxMustUseNfTables) {
b.capForcedNetfilter = "nftables"
} else {
b.capForcedNetfilter = "" // empty string means client can auto-detect
}
b.MagicConn().SetSilentDisco(b.ControlKnobs().SilentDisco.Load())
b.setDebugLogsByCapabilityLocked(nm)

@ -45,6 +45,8 @@ func IsLoginServerSynonym(val any) bool {
}
// Prefs are the user modifiable settings of the Tailscale node agent.
// When you add a Pref to this struct, remember to add a corresponding
// field in MaskedPrefs, and check your field for equality in Prefs.Equals().
type Prefs struct {
// ControlURL is the URL of the control server to use.
//
@ -213,6 +215,11 @@ type Prefs struct {
// posture checks.
PostureChecking bool
// NetfilterKind specifies what netfilter implementation to use.
//
// Linux-only.
NetfilterKind string
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
@ -241,6 +248,9 @@ type AppConnectorPrefs struct {
}
// MaskedPrefs is a Prefs with an associated bitmask of which fields are set.
// Make sure that the bool you add here maintains the same ordering of fields
// as the Prefs struct, because the ApplyEdits() function below relies on this
// ordering to be the same.
type MaskedPrefs struct {
Prefs
@ -269,6 +279,7 @@ type MaskedPrefs struct {
AutoUpdateSet bool `json:",omitempty"`
AppConnectorSet bool `json:",omitempty"`
PostureCheckingSet bool `json:",omitempty"`
NetfilterKindSet bool `json:",omitempty"`
}
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
@ -409,6 +420,9 @@ func (p *Prefs) pretty(goos string) string {
if p.OperatorUser != "" {
fmt.Fprintf(&sb, "op=%q ", p.OperatorUser)
}
if p.NetfilterKind != "" {
fmt.Fprintf(&sb, "netfilterKind=%s ", p.NetfilterKind)
}
sb.WriteString(p.AutoUpdate.Pretty())
sb.WriteString(p.AppConnector.Pretty())
if p.Persist != nil {
@ -468,7 +482,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.ProfileName == p2.ProfileName &&
p.AutoUpdate == p2.AutoUpdate &&
p.AppConnector == p2.AppConnector &&
p.PostureChecking == p2.PostureChecking
p.PostureChecking == p2.PostureChecking &&
p.NetfilterKind == p2.NetfilterKind
}
func (au AutoUpdatePrefs) Pretty() string {

@ -60,6 +60,7 @@ func TestPrefsEqual(t *testing.T) {
"AutoUpdate",
"AppConnector",
"PostureChecking",
"NetfilterKind",
"Persist",
}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
@ -327,6 +328,16 @@ func TestPrefsEqual(t *testing.T) {
&Prefs{PostureChecking: false},
false,
},
{
&Prefs{NetfilterKind: "iptables"},
&Prefs{NetfilterKind: "iptables"},
true,
},
{
&Prefs{NetfilterKind: "nftables"},
&Prefs{NetfilterKind: ""},
false,
},
}
for i, tt := range tests {
got := tt.a.Equals(tt.b)
@ -545,6 +556,20 @@ func TestPrefsPretty(t *testing.T) {
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
},
{
Prefs{
NetfilterKind: "iptables",
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off netfilterKind=iptables update=off Persist=nil}`,
},
{
Prefs{
NetfilterKind: "",
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
},
}
for i, tt := range tests {
got := tt.p.pretty(tt.os)

@ -2171,6 +2171,16 @@ const (
// NodeAttrDNSForwarderDisableTCPRetries disables retrying truncated
// DNS queries over TCP if the response is truncated.
NodeAttrDNSForwarderDisableTCPRetries NodeCapability = "dns-forwarder-disable-tcp-retries"
// NodeAttrLinuxMustUseIPTables forces Linux clients to use iptables for
// netfilter management.
// This cannot be set simultaneously with NodeAttrLinuxMustUseNfTables.
NodeAttrLinuxMustUseIPTables NodeCapability = "linux-netfilter?v=iptables"
// NodeAttrLinuxMustUseNfTables forces Linux clients to use nftables for
// netfilter management.
// This cannot be set simultaneously with NodeAttrLinuxMustUseIPTables.
NodeAttrLinuxMustUseNfTables NodeCapability = "linux-netfilter?v=nftables"
)
// SetDNSRequest is a request to add a DNS record.

@ -12,7 +12,7 @@ import (
"tailscale.com/version/distro"
)
func detectFirewallMode(logf logger.Logf) FirewallMode {
func detectFirewallMode(logf logger.Logf, prefHint string) FirewallMode {
if distro.Get() == distro.Gokrazy {
// Reduce startup logging on gokrazy. There's no way to do iptables on
// gokrazy anyway.
@ -21,18 +21,24 @@ func detectFirewallMode(logf logger.Logf) FirewallMode {
return FirewallModeNfTables
}
envMode := envknob.String("TS_DEBUG_FIREWALL_MODE")
mode := envknob.String("TS_DEBUG_FIREWALL_MODE")
// If the envknob isn't set, fall back to the pref suggested by c2n or
// nodeattrs.
if mode == "" {
mode = prefHint
logf("using firewall mode pref %s", prefHint)
} else if prefHint != "" {
logf("TS_DEBUG_FIREWALL_MODE set, overriding firewall mode from %s to %s", prefHint, mode)
}
// We now use iptables as default and have "auto" and "nftables" as
// options for people to test further.
switch envMode {
switch mode {
case "auto":
return pickFirewallModeFromInstalledRules(logf, linuxFWDetector{})
case "nftables":
logf("envknob TS_DEBUG_FIREWALL_MODE=nftables set")
hostinfo.SetFirewallMode("nft-forced")
return FirewallModeNfTables
case "iptables":
logf("envknob TS_DEBUG_FIREWALL_MODE=iptables set")
hostinfo.SetFirewallMode("ipt-forced")
default:
logf("default choosing iptables")

@ -511,10 +511,13 @@ type NetfilterRunner interface {
ClampMSSToPMTU(tun string, addr netip.Addr) error
}
// New creates a NetfilterRunner using either nftables or iptables.
// As nftables is still experimental, iptables will be used unless TS_DEBUG_USE_NETLINK_NFTABLES is set.
func New(logf logger.Logf) (NetfilterRunner, error) {
mode := detectFirewallMode(logf)
// New creates a NetfilterRunner, auto-detecting whether to use
// nftables or iptables.
// As nftables is still experimental, iptables will be used unless
// either the TS_DEBUG_FIREWALL_MODE environment variable, or the prefHint
// parameter, is set to one of "nftables" or "auto".
func New(logf logger.Logf, prefHint string) (NetfilterRunner, error) {
mode := detectFirewallMode(logf, prefHint)
switch mode {
case FirewallModeIPTables:
return newIPTablesRunner(logf)

@ -76,6 +76,7 @@ type Config struct {
SubnetRoutes []netip.Prefix // subnets being advertised to other Tailscale nodes
SNATSubnetRoutes bool // SNAT traffic to local subnets
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 {

@ -47,6 +47,7 @@ type linuxRouter struct {
localRoutes map[netip.Prefix]bool
snatSubnetRoutes bool
netfilterMode preftype.NetfilterMode
netfilterKind string
// ruleRestorePending is whether a timer has been started to
// restore deleted ip rules.
@ -326,6 +327,21 @@ func (r *linuxRouter) Close() error {
return nil
}
// setupNetfilter initializes the NetfilterRunner in r.nfr. It expects r.nfr
// to be nil, or the current netfilter to be set to netfilterOff.
// kind should be either a linuxfw.FirewallMode, or the empty string for auto.
func (r *linuxRouter) setupNetfilter(kind string) error {
r.netfilterKind = kind
var err error
r.nfr, err = linuxfw.New(r.logf, r.netfilterKind)
if err != nil {
return fmt.Errorf("could not create new netfilter: %w", err)
}
return nil
}
// Set implements the Router interface.
func (r *linuxRouter) Set(cfg *Config) error {
var errs []error
@ -333,6 +349,18 @@ func (r *linuxRouter) Set(cfg *Config) error {
cfg = &shutdownConfig
}
if cfg.NetfilterKind != r.netfilterKind {
if err := r.setNetfilterMode(netfilterOff); err != nil {
err = fmt.Errorf("could not disable existing netfilter: %w", err)
errs = append(errs, err)
} else {
r.nfr = nil
if err := r.setupNetfilter(cfg.NetfilterKind); err != nil {
errs = append(errs, err)
}
}
}
if err := r.setNetfilterMode(cfg.NetfilterMode); err != nil {
errs = append(errs, err)
}
@ -383,7 +411,7 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error {
if r.nfr == nil {
var err error
r.nfr, err = linuxfw.New(r.logf)
r.nfr, err = linuxfw.New(r.logf, r.netfilterKind)
if err != nil {
return err
}

@ -23,6 +23,7 @@ func TestConfigEqual(t *testing.T) {
testedFields := []string{
"LocalAddrs", "Routes", "LocalRoutes", "NewMTU",
"SubnetRoutes", "SNATSubnetRoutes", "NetfilterMode",
"NetfilterKind",
}
configType := reflect.TypeOf(Config{})
configFields := []string{}

Loading…
Cancel
Save