util/linuxfw: move detection logic
Just a refactor to consolidate the firewall detection logic in a single package so that it can be reused in a later commit by containerboot. Updates #9310 Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
@ -22,7 +22,6 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
@ -37,145 +36,6 @@ const (
|
||||
netfilterOn = preftype.NetfilterOn
|
||||
)
|
||||
|
||||
// netfilterRunner abstracts helpers to run netfilter commands. It is
|
||||
// implemented by linuxfw.IPTablesRunner and linuxfw.NfTablesRunner.
|
||||
type netfilterRunner interface {
|
||||
AddLoopbackRule(addr netip.Addr) error
|
||||
DelLoopbackRule(addr netip.Addr) error
|
||||
AddHooks() error
|
||||
DelHooks(logf logger.Logf) error
|
||||
AddChains() error
|
||||
DelChains() error
|
||||
AddBase(tunname string) error
|
||||
DelBase() error
|
||||
AddSNATRule() error
|
||||
DelSNATRule() error
|
||||
|
||||
HasIPV6() bool
|
||||
HasIPV6NAT() bool
|
||||
}
|
||||
|
||||
// tableDetector abstracts helpers to detect the firewall mode.
|
||||
// It is implemented for testing purposes.
|
||||
type tableDetector interface {
|
||||
iptDetect() (int, error)
|
||||
nftDetect() (int, error)
|
||||
}
|
||||
|
||||
type linuxFWDetector struct{}
|
||||
|
||||
// iptDetect returns the number of iptables rules in the current namespace.
|
||||
func (l *linuxFWDetector) iptDetect() (int, error) {
|
||||
return linuxfw.DetectIptables()
|
||||
}
|
||||
|
||||
// nftDetect returns the number of nftables rules in the current namespace.
|
||||
func (l *linuxFWDetector) nftDetect() (int, error) {
|
||||
return linuxfw.DetectNetfilter()
|
||||
}
|
||||
|
||||
// chooseFireWallMode returns the firewall mode to use based on the
|
||||
// environment and the system's capabilities.
|
||||
func chooseFireWallMode(logf logger.Logf, det tableDetector) linuxfw.FirewallMode {
|
||||
if distro.Get() == distro.Gokrazy {
|
||||
// Reduce startup logging on gokrazy. There's no way to do iptables on
|
||||
// gokrazy anyway.
|
||||
return linuxfw.FirewallModeNfTables
|
||||
}
|
||||
iptAva, nftAva := true, true
|
||||
iptRuleCount, err := det.iptDetect()
|
||||
if err != nil {
|
||||
logf("detect iptables rule: %v", err)
|
||||
iptAva = false
|
||||
}
|
||||
nftRuleCount, err := det.nftDetect()
|
||||
if err != nil {
|
||||
logf("detect nftables rule: %v", err)
|
||||
nftAva = false
|
||||
}
|
||||
logf("nftables rule count: %d, iptables rule count: %d", nftRuleCount, iptRuleCount)
|
||||
switch {
|
||||
case nftRuleCount > 0 && iptRuleCount == 0:
|
||||
logf("nftables is currently in use")
|
||||
hostinfo.SetFirewallMode("nft-inuse")
|
||||
return linuxfw.FirewallModeNfTables
|
||||
case iptRuleCount > 0 && nftRuleCount == 0:
|
||||
logf("iptables is currently in use")
|
||||
hostinfo.SetFirewallMode("ipt-inuse")
|
||||
return linuxfw.FirewallModeIPTables
|
||||
case nftAva:
|
||||
// if both iptables and nftables are available but
|
||||
// neither/both are currently used, use nftables.
|
||||
logf("nftables is available")
|
||||
hostinfo.SetFirewallMode("nft")
|
||||
return linuxfw.FirewallModeNfTables
|
||||
case iptAva:
|
||||
logf("iptables is available")
|
||||
hostinfo.SetFirewallMode("ipt")
|
||||
return linuxfw.FirewallModeIPTables
|
||||
default:
|
||||
// if neither iptables nor nftables are available, use iptablesRunner as a dummy
|
||||
// runner which exists but won't do anything. Creating iptablesRunner errors only
|
||||
// if the iptables command is missing or doesn’t support "--version", as long as it
|
||||
// can determine a version then it’ll carry on.
|
||||
hostinfo.SetFirewallMode("ipt-fb")
|
||||
return linuxfw.FirewallModeIPTables
|
||||
}
|
||||
}
|
||||
|
||||
// newNetfilterRunner 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 newNetfilterRunner(logf logger.Logf) (netfilterRunner, error) {
|
||||
tableDetector := &linuxFWDetector{}
|
||||
var mode linuxfw.FirewallMode
|
||||
|
||||
// We now use iptables as default and have "auto" and "nftables" as
|
||||
// options for people to test further.
|
||||
switch {
|
||||
case distro.Get() == distro.Gokrazy:
|
||||
// Reduce startup logging on gokrazy. There's no way to do iptables on
|
||||
// gokrazy anyway.
|
||||
logf("GoKrazy should use nftables.")
|
||||
hostinfo.SetFirewallMode("nft-gokrazy")
|
||||
mode = linuxfw.FirewallModeNfTables
|
||||
case envknob.String("TS_DEBUG_FIREWALL_MODE") == "nftables":
|
||||
logf("envknob TS_DEBUG_FIREWALL_MODE=nftables set")
|
||||
hostinfo.SetFirewallMode("nft-forced")
|
||||
mode = linuxfw.FirewallModeNfTables
|
||||
case envknob.String("TS_DEBUG_FIREWALL_MODE") == "auto":
|
||||
mode = chooseFireWallMode(logf, tableDetector)
|
||||
case envknob.String("TS_DEBUG_FIREWALL_MODE") == "iptables":
|
||||
logf("envknob TS_DEBUG_FIREWALL_MODE=iptables set")
|
||||
hostinfo.SetFirewallMode("ipt-forced")
|
||||
mode = linuxfw.FirewallModeIPTables
|
||||
default:
|
||||
logf("default choosing iptables")
|
||||
hostinfo.SetFirewallMode("ipt-default")
|
||||
mode = linuxfw.FirewallModeIPTables
|
||||
}
|
||||
|
||||
var nfr netfilterRunner
|
||||
var err error
|
||||
switch mode {
|
||||
case linuxfw.FirewallModeIPTables:
|
||||
logf("using iptables")
|
||||
nfr, err = linuxfw.NewIPTablesRunner(logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case linuxfw.FirewallModeNfTables:
|
||||
logf("using nftables")
|
||||
nfr, err = linuxfw.NewNfTablesRunner(logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown firewall mode: %v", mode)
|
||||
}
|
||||
|
||||
return nfr, nil
|
||||
}
|
||||
|
||||
type linuxRouter struct {
|
||||
closed atomic.Bool
|
||||
logf func(fmt string, args ...any)
|
||||
@ -200,7 +60,7 @@ type linuxRouter struct {
|
||||
// ipPolicyPrefBase is the base priority at which ip rules are installed.
|
||||
ipPolicyPrefBase int
|
||||
|
||||
nfr netfilterRunner
|
||||
nfr linuxfw.NetfilterRunner
|
||||
cmd commandRunner
|
||||
}
|
||||
|
||||
@ -210,7 +70,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Moni
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nfr, err := newNetfilterRunner(logf)
|
||||
nfr, err := linuxfw.New(logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -222,7 +82,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Moni
|
||||
return newUserspaceRouterAdvanced(logf, tunname, netMon, nfr, cmd)
|
||||
}
|
||||
|
||||
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, nfr netfilterRunner, cmd commandRunner) (Router, error) {
|
||||
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, nfr linuxfw.NetfilterRunner, cmd commandRunner) (Router, error) {
|
||||
r := &linuxRouter{
|
||||
logf: logf,
|
||||
tunname: tunname,
|
||||
|
@ -372,7 +372,7 @@ type fakeIPTablesRunner struct {
|
||||
//we always assume ipv6 and ipv6 nat are enabled when testing
|
||||
}
|
||||
|
||||
func newIPTablesRunner(t *testing.T) netfilterRunner {
|
||||
func newIPTablesRunner(t *testing.T) linuxfw.NetfilterRunner {
|
||||
return &fakeIPTablesRunner{
|
||||
t: t,
|
||||
ipt4: map[string][]string{
|
||||
@ -603,7 +603,7 @@ type fakeOS struct {
|
||||
rules []string
|
||||
//This test tests on the router level, so we will not bother
|
||||
//with using iptables or nftables, chose the simpler one.
|
||||
nfr netfilterRunner
|
||||
nfr linuxfw.NetfilterRunner
|
||||
}
|
||||
|
||||
func NewFakeOS(t *testing.T) *fakeOS {
|
||||
@ -1063,63 +1063,3 @@ func adjustFwmask(t *testing.T, s string) string {
|
||||
|
||||
return fwmaskAdjustRe.ReplaceAllString(s, "$1")
|
||||
}
|
||||
|
||||
type testFWDetector struct {
|
||||
iptRuleCount, nftRuleCount int
|
||||
iptErr, nftErr error
|
||||
}
|
||||
|
||||
func (t *testFWDetector) iptDetect() (int, error) {
|
||||
return t.iptRuleCount, t.iptErr
|
||||
}
|
||||
|
||||
func (t *testFWDetector) nftDetect() (int, error) {
|
||||
return t.nftRuleCount, t.nftErr
|
||||
}
|
||||
|
||||
func TestChooseFireWallMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
det *testFWDetector
|
||||
want linuxfw.FirewallMode
|
||||
}{
|
||||
{
|
||||
name: "using iptables legacy",
|
||||
det: &testFWDetector{iptRuleCount: 1},
|
||||
want: linuxfw.FirewallModeIPTables,
|
||||
},
|
||||
{
|
||||
name: "using nftables",
|
||||
det: &testFWDetector{nftRuleCount: 1},
|
||||
want: linuxfw.FirewallModeNfTables,
|
||||
},
|
||||
{
|
||||
name: "using both iptables and nftables",
|
||||
det: &testFWDetector{iptRuleCount: 2, nftRuleCount: 2},
|
||||
want: linuxfw.FirewallModeNfTables,
|
||||
},
|
||||
{
|
||||
name: "not using any firewall, both available",
|
||||
det: &testFWDetector{},
|
||||
want: linuxfw.FirewallModeNfTables,
|
||||
},
|
||||
{
|
||||
name: "not using any firewall, iptables available only",
|
||||
det: &testFWDetector{iptRuleCount: 1, nftErr: errors.New("nft error")},
|
||||
want: linuxfw.FirewallModeIPTables,
|
||||
},
|
||||
{
|
||||
name: "not using any firewall, nftables available only",
|
||||
det: &testFWDetector{iptErr: errors.New("iptables error"), nftRuleCount: 1},
|
||||
want: linuxfw.FirewallModeNfTables,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := chooseFireWallMode(t.Logf, tt.det)
|
||||
if got != tt.want {
|
||||
t.Errorf("chooseFireWallMode() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user