mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-21 12:28:39 +00:00
linuxfw,wgengine/route,ipn: add c2n and nodeattrs to control linux netfilter
Updates tailscale/corp#14029. Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
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)
|
||||
|
17
ipn/prefs.go
17
ipn/prefs.go
@ -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…
x
Reference in New Issue
Block a user