mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-12 05:37:32 +00:00
util/linuxfw: decoupling IPTables logic from linux router
This change is introducing new netfilterRunner interface and moving iptables manipulation to a lower leveled iptables runner. For #391 Signed-off-by: KevinLiang10 <kevinliang@tailscale.com>
This commit is contained in:

committed by
KevinLiang10

parent
9c64e015e5
commit
243ce6ccc1
475
util/linuxfw/iptables_runner.go
Normal file
475
util/linuxfw/iptables_runner.go
Normal file
@@ -0,0 +1,475 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
type iptablesInterface interface {
|
||||
// Adding this interface for testing purposes so we can mock out
|
||||
// the iptables library, in reality this is a wrapper to *iptables.IPTables.
|
||||
Insert(table, chain string, pos int, args ...string) error
|
||||
Append(table, chain string, args ...string) error
|
||||
Exists(table, chain string, args ...string) (bool, error)
|
||||
Delete(table, chain string, args ...string) error
|
||||
ClearChain(table, chain string) error
|
||||
NewChain(table, chain string) error
|
||||
DeleteChain(table, chain string) error
|
||||
}
|
||||
|
||||
type iptablesRunner struct {
|
||||
ipt4 iptablesInterface
|
||||
ipt6 iptablesInterface
|
||||
|
||||
v6Available bool
|
||||
v6NATAvailable bool
|
||||
}
|
||||
|
||||
// NewIPTablesRunner constructs a NetfilterRunner that programs iptables rules.
|
||||
// If the underlying iptables library fails to initialize, that error is
|
||||
// returned. The runner probes for IPv6 support once at initialization time and
|
||||
// if not found, no IPv6 rules will be modified for the lifetime of the runner.
|
||||
func NewIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
|
||||
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supportsV6, supportsV6NAT := false, false
|
||||
v6err := checkIPv6(logf)
|
||||
if v6err != nil {
|
||||
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
||||
} else {
|
||||
supportsV6 = true
|
||||
supportsV6NAT = supportsV6 && checkSupportsV6NAT()
|
||||
logf("v6nat = %v", supportsV6NAT)
|
||||
}
|
||||
|
||||
var ipt6 *iptables.IPTables
|
||||
if supportsV6 {
|
||||
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil
|
||||
}
|
||||
|
||||
// HasIPV6 returns true if the system supports IPv6.
|
||||
func (i *iptablesRunner) HasIPV6() bool {
|
||||
return i.v6Available
|
||||
}
|
||||
|
||||
// HasIPV6NAT returns true if the system supports IPv6 NAT.
|
||||
func (i *iptablesRunner) HasIPV6NAT() bool {
|
||||
return i.v6NATAvailable
|
||||
}
|
||||
|
||||
func isErrChainNotExist(err error) bool {
|
||||
return errCode(err) == 1
|
||||
}
|
||||
|
||||
// getIPTByAddr returns the iptablesInterface with correct IP family
|
||||
// that we will be using for the given address.
|
||||
func (i *iptablesRunner) getIPTByAddr(addr netip.Addr) iptablesInterface {
|
||||
nf := i.ipt4
|
||||
if addr.Is6() {
|
||||
nf = i.ipt6
|
||||
}
|
||||
return nf
|
||||
}
|
||||
|
||||
// AddLoopbackRule adds an iptables rule to permit loopback traffic to
|
||||
// a local Tailscale IP.
|
||||
func (i *iptablesRunner) AddLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("adding loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tsChain returns the name of the tailscale sub-chain corresponding
|
||||
// to the given "parent" chain (e.g. INPUT, FORWARD, ...).
|
||||
func tsChain(chain string) string {
|
||||
return "ts-" + strings.ToLower(chain)
|
||||
}
|
||||
|
||||
// DelLoopbackRule removes the iptables rule permitting loopback
|
||||
// traffic to a Tailscale IP.
|
||||
func (i *iptablesRunner) DelLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTables gets the available iptablesInterface in iptables runner.
|
||||
func (i *iptablesRunner) getTables() []iptablesInterface {
|
||||
if i.HasIPV6() {
|
||||
return []iptablesInterface{i.ipt4, i.ipt6}
|
||||
}
|
||||
return []iptablesInterface{i.ipt4}
|
||||
}
|
||||
|
||||
// getNATTables gets the available iptablesInterface in iptables runner.
|
||||
// If the system does not support IPv6 NAT, only the IPv4 iptablesInterface
|
||||
// is returned.
|
||||
func (i *iptablesRunner) getNATTables() []iptablesInterface {
|
||||
if i.HasIPV6NAT() {
|
||||
return i.getTables()
|
||||
}
|
||||
return []iptablesInterface{i.ipt4}
|
||||
}
|
||||
|
||||
// AddHooks inserts calls to tailscale's netfilter chains in
|
||||
// the relevant main netfilter chains. The tailscale chains must
|
||||
// already exist. If they do not, an error is returned.
|
||||
func (i *iptablesRunner) AddHooks() error {
|
||||
// divert inserts a jump to the tailscale chain in the given table/chain.
|
||||
// If the jump already exists, it is a no-op.
|
||||
divert := func(ipt iptablesInterface, table, chain string) error {
|
||||
tsChain := tsChain(chain)
|
||||
|
||||
args := []string{"-j", tsChain}
|
||||
exists, err := ipt.Exists(table, chain, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
if err := ipt.Insert(table, chain, 1, args...); err != nil {
|
||||
return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := divert(ipt, "filter", "INPUT"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := divert(ipt, "filter", "FORWARD"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := divert(ipt, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddChains creates custom Tailscale chains in netfilter via iptables
|
||||
// if the ts-chain doesn't already exist.
|
||||
func (i *iptablesRunner) AddChains() error {
|
||||
// create creates a chain in the given table if it doesn't already exist.
|
||||
// If the chain already exists, it is a no-op.
|
||||
create := func(ipt iptablesInterface, table, chain string) error {
|
||||
err := ipt.ClearChain(table, chain)
|
||||
if isErrChainNotExist(err) {
|
||||
// nonexistent chain. let's create it!
|
||||
return ipt.NewChain(table, chain)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := create(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := create(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := create(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBase adds some basic processing rules to be supplemented by
|
||||
// later calls to other helpers.
|
||||
func (i *iptablesRunner) AddBase(tunname string) error {
|
||||
if err := i.addBase4(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
if i.HasIPV6() {
|
||||
if err := i.addBase6(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addBase4 adds some basic IPv6 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (i *iptablesRunner) addBase4(tunname string) error {
|
||||
// Only allow CGNAT range traffic to come from tailscale0. There
|
||||
// is an exception carved out for ranges used by ChromeOS, for
|
||||
// which we fall out of the Tailscale chain.
|
||||
//
|
||||
// Note, this will definitely break nodes that end up using the
|
||||
// CGNAT range for other purposes :(.
|
||||
args := []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
args = []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
// Forward all traffic from the Tailscale interface, and drop
|
||||
// traffic to the tailscale interface by default. We use packet
|
||||
// marks here so both filter/FORWARD and nat/POSTROUTING can match
|
||||
// on these packets of interest.
|
||||
//
|
||||
// In particular, we only want to apply SNAT rules in
|
||||
// nat/POSTROUTING to packets that originated from the Tailscale
|
||||
// interface, but we can't match on the inbound interface in
|
||||
// POSTROUTING. So instead, we match on the inbound interface in
|
||||
// filter/FORWARD, and set a packet mark that nat/POSTROUTING can
|
||||
// use to effectively run that same test again.
|
||||
args = []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addBase6 adds some basic IPv4 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (i *iptablesRunner) addBase6(tunname string) error {
|
||||
// TODO: only allow traffic from Tailscale's ULA range to come
|
||||
// from tailscale0.
|
||||
|
||||
args := []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
// TODO: drop forwarded traffic to tailscale0 from tailscale's ULA
|
||||
// (see corresponding IPv4 CGNAT rule).
|
||||
args = []string{"-o", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelChains removes the custom Tailscale chains from netfilter via iptables.
|
||||
func (i *iptablesRunner) DelChains() error {
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := delChain(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := delChain(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := delChain(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelBase empties but does not remove custom Tailscale chains from
|
||||
// netfilter via iptables.
|
||||
func (i *iptablesRunner) DelBase() error {
|
||||
del := func(ipt iptablesInterface, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if isErrChainNotExist(err) {
|
||||
// nonexistent chain. That's fine, since it's
|
||||
// the desired state anyway.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := del(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := del(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelHooks deletes the calls to tailscale's netfilter chains
|
||||
// in the relevant main netfilter chains.
|
||||
func (i *iptablesRunner) DelHooks(logf logger.Logf) error {
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSNATRule adds a netfilter rule to SNAT traffic destined for
|
||||
// local subnets.
|
||||
func (i *iptablesRunner) AddSNATRule() error {
|
||||
args := []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := ipt.Append("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelSNATRule removes the netfilter rule to SNAT traffic destined for
|
||||
// local subnets. An error is returned if the rule does not exist.
|
||||
func (i *iptablesRunner) DelSNATRule() error {
|
||||
args := []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := ipt.Delete("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("deleting %v in nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPTablesCleanup removes all Tailscale added iptables rules.
|
||||
// Any errors that occur are logged to the provided logf.
|
||||
func IPTablesCleanup(logf logger.Logf) {
|
||||
err := clearRules(iptables.ProtocolIPv4, logf)
|
||||
if err != nil {
|
||||
logf("linuxfw: clear iptables: %v", err)
|
||||
}
|
||||
|
||||
err = clearRules(iptables.ProtocolIPv6, logf)
|
||||
if err != nil {
|
||||
logf("linuxfw: clear ip6tables: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delTSHook deletes hook in a chain that jumps to a ts-chain. If the hook does not
|
||||
// exist, it's a no-op since the desired state is already achieved but we log the
|
||||
// error because error code from the iptables module resists unwrapping.
|
||||
func delTSHook(ipt iptablesInterface, table, chain string, logf logger.Logf) error {
|
||||
tsChain := tsChain(chain)
|
||||
args := []string{"-j", tsChain}
|
||||
if err := ipt.Delete(table, chain, args...); err != nil {
|
||||
// TODO(apenwarr): check for errCode(1) here.
|
||||
// Unfortunately the error code from the iptables
|
||||
// module resists unwrapping, unlike with other
|
||||
// calls. So we have to assume if Delete fails,
|
||||
// it's because there is no such rule.
|
||||
logf("deleting %v in %s/%s: %v", args, table, chain, err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// delChain flushs and deletes a chain. If the chain does not exist, it's a no-op
|
||||
// since the desired state is already achieved. otherwise, it returns an error.
|
||||
func delChain(ipt iptablesInterface, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if isErrChainNotExist(err) {
|
||||
// nonexistent chain. nothing to do.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
if err := ipt.DeleteChain(table, chain); err != nil {
|
||||
return fmt.Errorf("deleting %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearRules clears all the iptables rules created by Tailscale
|
||||
// for the given protocol. If error occurs, it's logged but not returned.
|
||||
func clearRules(proto iptables.Protocol, logf logger.Logf) error {
|
||||
ipt, err := iptables.NewWithProtocol(proto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := delChain(ipt, "filter", "ts-input"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delChain(ipt, "filter", "ts-forward"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := delChain(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return multierr.New(errs...)
|
||||
}
|
420
util/linuxfw/iptables_runner_test.go
Normal file
420
util/linuxfw/iptables_runner_test.go
Normal file
@@ -0,0 +1,420 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
var errExec = errors.New("execution failed")
|
||||
|
||||
type fakeIPTables struct {
|
||||
t *testing.T
|
||||
n map[string][]string
|
||||
}
|
||||
|
||||
type fakeRule struct {
|
||||
table, chain string
|
||||
args []string
|
||||
}
|
||||
|
||||
func newIPTables(t *testing.T) *fakeIPTables {
|
||||
return &fakeIPTables{
|
||||
t: t,
|
||||
n: map[string][]string{
|
||||
"filter/INPUT": nil,
|
||||
"filter/OUTPUT": nil,
|
||||
"filter/FORWARD": nil,
|
||||
"nat/PREROUTING": nil,
|
||||
"nat/OUTPUT": nil,
|
||||
"nat/POSTROUTING": nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Insert(table, chain string, pos int, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if pos > len(rules)+1 {
|
||||
n.t.Errorf("bad position %d in %s", pos, k)
|
||||
return errExec
|
||||
}
|
||||
rules = append(rules, "")
|
||||
copy(rules[pos:], rules[pos-1:])
|
||||
rules[pos-1] = strings.Join(args, " ")
|
||||
n.n[k] = rules
|
||||
} else {
|
||||
n.t.Errorf("unknown table/chain %s", k)
|
||||
return errExec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Append(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
return n.Insert(table, chain, len(n.n[k])+1, args...)
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Exists(table, chain string, args ...string) (bool, error) {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for _, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
} else {
|
||||
n.t.Logf("unknown table/chain %s", k)
|
||||
return false, errExec
|
||||
}
|
||||
}
|
||||
|
||||
func hasChain(n *fakeIPTables, table, chain string) bool {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Delete(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for i, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
rules = append(rules[:i], rules[i+1:]...)
|
||||
n.n[k] = rules
|
||||
return nil
|
||||
}
|
||||
}
|
||||
n.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
|
||||
return errExec
|
||||
} else {
|
||||
n.t.Errorf("unknown table/chain %s", k)
|
||||
return errExec
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) ClearChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
} else {
|
||||
n.t.Logf("note: ClearChain: unknown table/chain %s", k)
|
||||
return errors.New("exitcode:1")
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) NewChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
n.t.Errorf("table/chain %s already exists", k)
|
||||
return errExec
|
||||
}
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) DeleteChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if len(rules) != 0 {
|
||||
n.t.Errorf("%s is not empty", k)
|
||||
return errExec
|
||||
}
|
||||
delete(n.n, k)
|
||||
return nil
|
||||
} else {
|
||||
n.t.Errorf("%s does not exist", k)
|
||||
return errExec
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeIPTablesRunner(t *testing.T) *iptablesRunner {
|
||||
ipt4 := newIPTables(t)
|
||||
ipt6 := newIPTables(t)
|
||||
|
||||
iptr := &iptablesRunner{ipt4, ipt6, true, true}
|
||||
return iptr
|
||||
}
|
||||
|
||||
func TestAddAndDeleteChains(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
err := iptr.AddChains()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the chains were created.
|
||||
tsChains := []struct{ table, chain string }{ // table/chain
|
||||
{"filter", "ts-input"},
|
||||
{"filter", "ts-forward"},
|
||||
{"nat", "ts-postrouting"},
|
||||
}
|
||||
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tc := range tsChains {
|
||||
// Exists returns error if the chain doesn't exist.
|
||||
if _, err := proto.Exists(tc.table, tc.chain); err != nil {
|
||||
t.Errorf("chain %s/%s doesn't exist", tc.table, tc.chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = iptr.DelChains()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the chains were deleted.
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tc := range tsChains {
|
||||
if _, err = proto.Exists(tc.table, tc.chain); err == nil {
|
||||
t.Errorf("chain %s/%s still exists", tc.table, tc.chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddAndDeleteHooks(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
// don't need to test what happens if the chains don't exist, because
|
||||
// this is handled by fake iptables, in realife iptables would return error.
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer iptr.DelChains()
|
||||
|
||||
if err := iptr.AddHooks(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were created.
|
||||
tsRules := []fakeRule{ // table/chain/rule
|
||||
{"filter", "INPUT", []string{"-j", "ts-input"}},
|
||||
{"filter", "FORWARD", []string{"-j", "ts-forward"}},
|
||||
{"nat", "POSTROUTING", []string{"-j", "ts-postrouting"}},
|
||||
}
|
||||
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tr := range tsRules {
|
||||
if exists, err := proto.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exists {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
// check if the rule is at front of the chain
|
||||
if proto.(*fakeIPTables).n[tr.table+"/"+tr.chain][0] != strings.Join(tr.args, " ") {
|
||||
t.Errorf("v4 rule %s/%s/%s is not at the top", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.DelHooks(t.Logf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were deleted.
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tr := range tsRules {
|
||||
if exists, err := proto.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exists {
|
||||
t.Errorf("rule %s/%s/%s still exists", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.AddHooks(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddAndDeleteBase(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
tunname := "tun0"
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := iptr.AddBase(tunname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were created.
|
||||
tsRulesV4 := []fakeRule{ // table/chain/rule
|
||||
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}},
|
||||
{"filter", "ts-input", []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}},
|
||||
{"filter", "ts-forward", []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}},
|
||||
}
|
||||
|
||||
tsRulesCommon := []fakeRule{ // table/chain/rule
|
||||
{"filter", "ts-forward", []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}},
|
||||
{"filter", "ts-forward", []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}},
|
||||
{"filter", "ts-forward", []string{"-o", tunname, "-j", "ACCEPT"}},
|
||||
}
|
||||
|
||||
// check that the rules were created for ipt4
|
||||
for _, tr := range append(tsRulesV4, tsRulesCommon...) {
|
||||
if exists, err := iptr.ipt4.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exists {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
// check that the rules were created for ipt6
|
||||
for _, tr := range tsRulesCommon {
|
||||
if exists, err := iptr.ipt6.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exists {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.DelBase(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were deleted.
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
for _, tr := range append(tsRulesV4, tsRulesCommon...) {
|
||||
if exists, err := proto.Exists(tr.table, tr.chain, tr.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exists {
|
||||
t.Errorf("rule %s/%s/%s still exists", tr.table, tr.chain, strings.Join(tr.args, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.DelChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddAndDelLoopbackRule(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
// We don't need to test for malformed addresses, AddLoopbackRule
|
||||
// takes in a netip.Addr, which is already valid.
|
||||
fakeAddrV4 := netip.MustParseAddr("192.168.0.2")
|
||||
fakeAddrV6 := netip.MustParseAddr("2001:db8::2")
|
||||
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := iptr.AddLoopbackRule(fakeAddrV4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := iptr.AddLoopbackRule(fakeAddrV6); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were created.
|
||||
tsRulesV4 := fakeRule{ // table/chain/rule
|
||||
"filter", "ts-input", []string{"-i", "lo", "-s", fakeAddrV4.String(), "-j", "ACCEPT"}}
|
||||
|
||||
tsRulesV6 := fakeRule{ // table/chain/rule
|
||||
"filter", "ts-input", []string{"-i", "lo", "-s", fakeAddrV6.String(), "-j", "ACCEPT"}}
|
||||
|
||||
// check that the rules were created for ipt4 and ipt6
|
||||
if exist, err := iptr.ipt4.Exists(tsRulesV4.table, tsRulesV4.chain, tsRulesV4.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exist {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tsRulesV4.table, tsRulesV4.chain, strings.Join(tsRulesV4.args, " "))
|
||||
}
|
||||
if exist, err := iptr.ipt6.Exists(tsRulesV6.table, tsRulesV6.chain, tsRulesV6.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exist {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", tsRulesV6.table, tsRulesV6.chain, strings.Join(tsRulesV6.args, " "))
|
||||
}
|
||||
|
||||
// check that the rule is at the top
|
||||
chain := "filter/ts-input"
|
||||
if iptr.ipt4.(*fakeIPTables).n[chain][0] != strings.Join(tsRulesV4.args, " ") {
|
||||
t.Errorf("v4 rule %s/%s/%s is not at the top", tsRulesV4.table, tsRulesV4.chain, strings.Join(tsRulesV4.args, " "))
|
||||
}
|
||||
if iptr.ipt6.(*fakeIPTables).n[chain][0] != strings.Join(tsRulesV6.args, " ") {
|
||||
t.Errorf("v6 rule %s/%s/%s is not at the top", tsRulesV6.table, tsRulesV6.chain, strings.Join(tsRulesV6.args, " "))
|
||||
}
|
||||
|
||||
// delete the rules
|
||||
if err := iptr.DelLoopbackRule(fakeAddrV4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := iptr.DelLoopbackRule(fakeAddrV6); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rules were deleted.
|
||||
if exist, err := iptr.ipt4.Exists(tsRulesV4.table, tsRulesV4.chain, tsRulesV4.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exist {
|
||||
t.Errorf("rule %s/%s/%s still exists", tsRulesV4.table, tsRulesV4.chain, strings.Join(tsRulesV4.args, " "))
|
||||
}
|
||||
|
||||
if exist, err := iptr.ipt6.Exists(tsRulesV6.table, tsRulesV6.chain, tsRulesV6.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exist {
|
||||
t.Errorf("rule %s/%s/%s still exists", tsRulesV6.table, tsRulesV6.chain, strings.Join(tsRulesV6.args, " "))
|
||||
}
|
||||
|
||||
if err := iptr.DelChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddAndDelSNATRule(t *testing.T) {
|
||||
iptr := newFakeIPTablesRunner(t)
|
||||
|
||||
if err := iptr.AddChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rule := fakeRule{ // table/chain/rule
|
||||
"nat", "ts-postrouting", []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"},
|
||||
}
|
||||
|
||||
// Add SNAT rule
|
||||
if err := iptr.AddSNATRule(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rule was created for ipt4 and ipt6
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
if exist, err := proto.Exists(rule.table, rule.chain, rule.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !exist {
|
||||
t.Errorf("rule %s/%s/%s doesn't exist", rule.table, rule.chain, strings.Join(rule.args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
// Delete SNAT rule
|
||||
if err := iptr.DelSNATRule(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that the rule was deleted for ipt4 and ipt6
|
||||
for _, proto := range []iptablesInterface{iptr.ipt4, iptr.ipt6} {
|
||||
if exist, err := proto.Exists(rule.table, rule.chain, rule.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exist {
|
||||
t.Errorf("rule %s/%s/%s still exists", rule.table, rule.chain, strings.Join(rule.args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
if err := iptr.DelChains(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
@@ -2,10 +2,179 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package linuxfw returns the kind of firewall being used by the kernel.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
// ErrUnsupported is the error returned from all functions on non-Linux
|
||||
// platforms.
|
||||
var ErrUnsupported = errors.New("unsupported")
|
||||
"github.com/tailscale/netlink"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// The following bits are added to packet marks for Tailscale use.
|
||||
//
|
||||
// We tried to pick bits sufficiently out of the way that it's
|
||||
// unlikely to collide with existing uses. We have 4 bytes of mark
|
||||
// bits to play with. We leave the lower byte alone on the assumption
|
||||
// that sysadmins would use those. Kubernetes uses a few bits in the
|
||||
// second byte, so we steer clear of that too.
|
||||
//
|
||||
// Empirically, most of the documentation on packet marks on the
|
||||
// internet gives the impression that the marks are 16 bits
|
||||
// wide. Based on this, we theorize that the upper two bytes are
|
||||
// relatively unused in the wild, and so we consume bits 16:23 (the
|
||||
// third byte).
|
||||
//
|
||||
// The constants are in the iptables/iproute2 string format for
|
||||
// matching and setting the bits, so they can be directly embedded in
|
||||
// commands.
|
||||
const (
|
||||
// The mask for reading/writing the 'firewall mask' bits on a packet.
|
||||
// See the comment on the const block on why we only use the third byte.
|
||||
//
|
||||
// We claim bits 16:23 entirely. For now we only use the lower four
|
||||
// bits, leaving the higher 4 bits for future use.
|
||||
TailscaleFwmarkMask = "0xff0000"
|
||||
TailscaleFwmarkMaskNeg = "0xff00ffff"
|
||||
TailscaleFwmarkMaskNum = 0xff0000
|
||||
|
||||
// Packet is from Tailscale and to a subnet route destination, so
|
||||
// is allowed to be routed through this machine.
|
||||
TailscaleSubnetRouteMark = "0x40000"
|
||||
TailscaleSubnetRouteMarkNum = 0x40000
|
||||
// This one is same value but padded to even number of digit, so
|
||||
// hex decoding can work correctly.
|
||||
TailscaleSubnetRouteMarkHexStr = "0x040000"
|
||||
|
||||
// Packet was originated by tailscaled itself, and must not be
|
||||
// routed over the Tailscale network.
|
||||
TailscaleBypassMark = "0x80000"
|
||||
TailscaleBypassMarkNum = 0x80000
|
||||
)
|
||||
|
||||
// errCode extracts and returns the process exit code from err, or
|
||||
// zero if err is nil.
|
||||
func errCode(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
var e *exec.ExitError
|
||||
if ok := errors.As(err, &e); ok {
|
||||
return e.ExitCode()
|
||||
}
|
||||
s := err.Error()
|
||||
if strings.HasPrefix(s, "exitcode:") {
|
||||
code, err := strconv.Atoi(s[9:])
|
||||
if err == nil {
|
||||
return code
|
||||
}
|
||||
}
|
||||
return -42
|
||||
}
|
||||
|
||||
// checkIPv6 checks whether the system appears to have a working IPv6
|
||||
// network stack. It returns an error explaining what looks wrong or
|
||||
// missing. It does not check that IPv6 is currently functional or
|
||||
// that there's a global address, just that the system would support
|
||||
// IPv6 if it were on an IPv6 network.
|
||||
func checkIPv6(logf logger.Logf) error {
|
||||
_, err := os.Stat("/proc/sys/net/ipv6")
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
bs, err := os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_ipv6")
|
||||
if err != nil {
|
||||
// Be conservative if we can't find the IPv6 configuration knob.
|
||||
return err
|
||||
}
|
||||
disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_ipv6 has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_ipv6 is set")
|
||||
}
|
||||
|
||||
// Older kernels don't support IPv6 policy routing. Some kernels
|
||||
// support policy routing but don't have this knob, so absence of
|
||||
// the knob is not fatal.
|
||||
bs, err = os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy")
|
||||
if err == nil {
|
||||
disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_policy has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_policy is set")
|
||||
}
|
||||
}
|
||||
|
||||
if err := CheckIPRuleSupportsV6(logf); err != nil {
|
||||
return fmt.Errorf("kernel doesn't support IPv6 policy routing: %w", err)
|
||||
}
|
||||
|
||||
// Some distros ship ip6tables separately from iptables.
|
||||
if _, err := exec.LookPath("ip6tables"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkSupportsV6NAT returns whether the system has a "nat" table in the
|
||||
// IPv6 netfilter stack.
|
||||
//
|
||||
// The nat table was added after the initial release of ipv6
|
||||
// netfilter, so some older distros ship a kernel that can't NAT IPv6
|
||||
// traffic.
|
||||
func checkSupportsV6NAT() bool {
|
||||
bs, err := os.ReadFile("/proc/net/ip6_tables_names")
|
||||
if err != nil {
|
||||
// Can't read the file. Assume SNAT works.
|
||||
return true
|
||||
}
|
||||
if bytes.Contains(bs, []byte("nat\n")) {
|
||||
return true
|
||||
}
|
||||
// In nftables mode, that proc file will be empty. Try another thing:
|
||||
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckIPRuleSupportsV6(logf logger.Logf) error {
|
||||
// First try just a read-only operation to ideally avoid
|
||||
// having to modify any state.
|
||||
if rules, err := netlink.RuleList(netlink.FAMILY_V6); err != nil {
|
||||
return fmt.Errorf("querying IPv6 policy routing rules: %w", err)
|
||||
} else {
|
||||
if len(rules) > 0 {
|
||||
logf("[v1] kernel supports IPv6 policy routing (found %d rules)", len(rules))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to actually create & delete one as a test.
|
||||
rule := netlink.NewRule()
|
||||
rule.Priority = 1234
|
||||
rule.Mark = TailscaleBypassMarkNum
|
||||
rule.Table = 52
|
||||
rule.Family = netlink.FAMILY_V6
|
||||
// First delete the rule unconditionally, and don't check for
|
||||
// errors. This is just cleaning up anything that might be already
|
||||
// there.
|
||||
netlink.RuleDel(rule)
|
||||
// And clean up on exit.
|
||||
defer netlink.RuleDel(rule)
|
||||
return netlink.RuleAdd(rule)
|
||||
}
|
||||
|
@@ -9,9 +9,15 @@
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// ErrUnsupported is the error returned from all functions on non-Linux
|
||||
// platforms.
|
||||
var ErrUnsupported = errors.New("linuxfw:unsupported")
|
||||
|
||||
// DebugNetfilter is not supported on non-Linux platforms.
|
||||
func DebugNetfilter(logf logger.Logf) error {
|
||||
return ErrUnsupported
|
||||
|
Reference in New Issue
Block a user