Merge pull request #50646 from robmry/nftables_no_enable_ip_forwarding

nftables: never enable IP forwarding on the host
This commit is contained in:
Rob Murray
2025-08-11 09:44:00 +01:00
committed by GitHub
21 changed files with 225 additions and 190 deletions

View File

@@ -128,6 +128,16 @@ check_device() {
fi
}
check_sysctl() {
val=$(sysctl -n $1)
want=$2
if [ "$val" = "$want" ]; then
wrap_good "sysctl $1" "enabled"
else
wrap_bad "sysctl $1" "disabled"
fi
}
if [ ! -e "$CONFIG" ]; then
wrap_warning "warning: $CONFIG does not exist, searching other paths for kernel config ..."
for tryConfig in $possibleConfigs; do
@@ -343,6 +353,10 @@ if ! is_set EXT4_FS || ! is_set EXT4_FS_POSIX_ACL || ! is_set EXT4_FS_SECURITY;
fi
echo '- Network Drivers:'
echo " - \"$(wrap_color 'bridge' blue)\":"
check_sysctl net.ipv4.ip_forward 1 | sed 's/^/ - /'
check_sysctl net.ipv6.conf.all.forwarding 1 | sed 's/^/ - /'
check_sysctl net.ipv6.conf.default.forwarding 1 | sed 's/^/ - /'
echo " - \"$(wrap_color 'overlay' blue)\":"
check_flags VXLAN BRIDGE_VLAN_FILTERING | sed 's/^/ /'
echo ' Optional (for encrypted networks):'

View File

@@ -199,5 +199,12 @@ else
mount_directory /etc/ssl "--rbind"
fi
# When running with --firewall-backend=nftables, IP forwarding needs to be enabled
# because the daemon won't enable it. IP forwarding is harmless in the rootless
# netns, there's only a single external interface and only Docker uses the netns.
# So, always enable IPv4 and IPv6 forwarding.
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1
exec "$dockerd" "$@"
fi

View File

@@ -556,7 +556,7 @@ var newFirewaller = func(ctx context.Context, config firewaller.Config) (firewal
// cleaner can't clean up network or port-specific rules that may have been added
// to iptables built-in chains. So, if cleanup is needed, give the cleaner to
// the nftabler. Then, it'll use it to delete old rules as networks are restored.
fw.(firewaller.FirewallCleanerSetter).SetFirewallCleaner(iptabler.NewCleaner(ctx, config))
fw.SetFirewallCleaner(iptabler.NewCleaner(ctx, config))
return fw, nil
}
@@ -880,14 +880,28 @@ func (d *driver) createNetwork(ctx context.Context, config *networkConfiguration
config.EnableIPv4 && d.config.EnableIPForwarding,
"setupIPv4Forwarding",
func(*networkConfiguration, *bridgeInterface) error {
return setupIPv4Forwarding(d.firewaller, d.config.EnableIPTables && !d.config.DisableFilterForwardDrop)
ffd, ok := d.firewaller.(filterForwardDropper)
if !ok {
// The firewaller can't drop non-Docker forwarding. It's up to the user to enable
// forwarding on their host, and configure their firewall appropriately.
return checkIPv4Forwarding()
}
// Enable forwarding and set a default-drop forwarding policy if necessary.
return setupIPv4Forwarding(ffd, d.config.EnableIPTables && !d.config.DisableFilterForwardDrop)
},
},
{
config.EnableIPv6 && d.config.EnableIPForwarding,
"setupIPv6Forwarding",
func(*networkConfiguration, *bridgeInterface) error {
return setupIPv6Forwarding(d.firewaller, d.config.EnableIP6Tables && !d.config.DisableFilterForwardDrop)
ffd, ok := d.firewaller.(filterForwardDropper)
if !ok {
// The firewaller can't drop non-Docker forwarding. It's up to the user to enable
// forwarding on their host, and configure their firewall appropriately.
return checkIPv6Forwarding()
}
// Enable forwarding and set a default-drop forwarding policy if necessary.
return setupIPv6Forwarding(ffd, d.config.EnableIP6Tables && !d.config.DisableFilterForwardDrop)
},
},

View File

@@ -13,7 +13,7 @@ import (
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/internal/otelutil"
"github.com/moby/moby/v2/daemon/libnetwork/datastore"
"github.com/moby/moby/v2/daemon/libnetwork/drivers/bridge/internal/firewaller"
"github.com/moby/moby/v2/daemon/libnetwork/drivers/bridge/internal/nftabler"
"github.com/moby/moby/v2/daemon/libnetwork/portmapperapi"
"github.com/moby/moby/v2/daemon/libnetwork/types"
"go.opentelemetry.io/otel"
@@ -43,8 +43,8 @@ func (d *driver) initStore() error {
// If there's a firewall cleaner, it's done its job by cleaning up rules
// belonging to the restored networks. So, drop it.
if fcs, ok := d.firewaller.(firewaller.FirewallCleanerSetter); ok {
fcs.SetFirewallCleaner(nil)
if nft, ok := d.firewaller.(*nftabler.Nftabler); ok {
nft.SetFirewallCleaner(nil)
}
return nil

View File

@@ -76,9 +76,6 @@ type Firewaller interface {
// NewNetwork returns an object that can be used to add published ports and legacy
// links for a bridge network.
NewNetwork(ctx context.Context, nc NetworkConfig) (Network, error)
// FilterForwardDrop sets the default policy of the FORWARD chain in the filter
// table to DROP.
FilterForwardDrop(ctx context.Context, ipv IPVersion) error
}
// Network can be used to manipulate firewall rules for a bridge network.
@@ -110,12 +107,6 @@ type Network interface {
DelLink(ctx context.Context, parentIP, childIP netip.Addr, ports []types.TransportPort)
}
// FirewallCleanerSetter is an optional interface for a Firewaller.
type FirewallCleanerSetter interface {
// SetFirewallCleaner replaces the FirewallCleaner (possibly with 'nil').
SetFirewallCleaner(FirewallCleaner)
}
// FirewallCleaner is used to delete rules created by previous incarnations of
// the daemon. On startup, once a Firewaller implementation has been selected, if
// rules may have been left behind by a different Firewaller implementation, get

View File

@@ -15,7 +15,6 @@ import (
type StubFirewaller struct {
Config
Networks map[string]*StubFirewallerNetwork
FFD map[IPVersion]bool // filter forward drop
}
func NewStubFirewaller(config Config) *StubFirewaller {
@@ -24,7 +23,6 @@ func NewStubFirewaller(config Config) *StubFirewaller {
// A real Firewaller shouldn't hold on to its own networks, the bridge driver is doing that.
// But, for unit tests cross-checking the driver, this is useful.
Networks: make(map[string]*StubFirewallerNetwork),
FFD: make(map[IPVersion]bool),
}
}
@@ -41,11 +39,6 @@ func (fw *StubFirewaller) NewNetwork(_ context.Context, nc NetworkConfig) (Netwo
return nw, nil
}
func (fw *StubFirewaller) FilterForwardDrop(_ context.Context, ipv IPVersion) error {
fw.FFD[ipv] = true
return nil
}
type stubFirewallerLink struct {
parentIP netip.Addr
childIP netip.Addr

View File

@@ -17,7 +17,7 @@ type iptablesCleaner struct {
}
// NewCleaner checks for iptables rules left behind by an old daemon that was using
// the iptabler.
// the Iptabler.
//
// If there are old rules present, it deletes as much as possible straight away
// (user-defined chains and jumps from the built-in chains).
@@ -46,6 +46,13 @@ func NewCleaner(ctx context.Context, config firewaller.Config) firewaller.Firewa
_ = t.DeleteJumpRule(iptables.Filter, "FORWARD", DockerForwardChain)
_ = deleteLegacyTopLevelRules(ctx, t, ipv)
removeIPChains(ctx, ipv)
// The iptables chains will no longer have Docker's ACCEPT rules. So, if the
// filter-FORWARD chain has policy DROP (possibly set by Docker when it enabled
// IP forwarding), packets accepted by nftables chains will still be processed by
// iptables and dropped. It's the user's responsibility to sort that out.
if t.HasPolicy("filter", "FORWARD", iptables.Drop) {
log.G(ctx).WithField("ipv", ipv).Warn("Network traffic for published ports may be dropped, iptables chain FORWARD has policy DROP.")
}
return true
}
cleaned4 := clean(iptables.IPv4, config.IPv4)
@@ -62,7 +69,7 @@ func (ic iptablesCleaner) DelNetwork(ctx context.Context, nc firewaller.NetworkC
}
n := network{
config: nc,
ipt: &iptabler{config: ic.config},
ipt: &Iptabler{config: ic.config},
}
if ic.config.IPv4 && nc.Config4.Prefix.IsValid() {
_ = deleteLegacyFilterRules(iptables.IPv4, nc.IfName)
@@ -77,7 +84,7 @@ func (ic iptablesCleaner) DelNetwork(ctx context.Context, nc firewaller.NetworkC
func (ic iptablesCleaner) DelEndpoint(ctx context.Context, nc firewaller.NetworkConfig, epIPv4, epIPv6 netip.Addr) {
n := network{
config: nc,
ipt: &iptabler{config: ic.config},
ipt: &Iptabler{config: ic.config},
}
if n.ipt.config.IPv4 && epIPv4.IsValid() {
_ = n.filterDirectAccess(ctx, iptables.IPv4, n.config.Config4, epIPv4, false)
@@ -90,7 +97,7 @@ func (ic iptablesCleaner) DelEndpoint(ctx context.Context, nc firewaller.Network
func (ic iptablesCleaner) DelPorts(ctx context.Context, nc firewaller.NetworkConfig, pbs []types.PortBinding) {
n := network{
config: nc,
ipt: &iptabler{config: ic.config},
ipt: &Iptabler{config: ic.config},
}
_ = n.DelPorts(ctx, pbs)
}

View File

@@ -35,12 +35,12 @@ const (
isolationChain2 = "DOCKER-ISOLATION-STAGE-2"
)
type iptabler struct {
type Iptabler struct {
config firewaller.Config
}
func NewIptabler(ctx context.Context, config firewaller.Config) (firewaller.Firewaller, error) {
ipt := &iptabler{config: config}
func NewIptabler(ctx context.Context, config firewaller.Config) (*Iptabler, error) {
ipt := &Iptabler{config: config}
if ipt.config.IPv4 {
removeIPChains(ctx, iptables.IPv4)
@@ -91,7 +91,8 @@ func NewIptabler(ctx context.Context, config firewaller.Config) (firewaller.Fire
return ipt, nil
}
func (ipt *iptabler) FilterForwardDrop(ctx context.Context, ipv firewaller.IPVersion) error {
// FilterForwardDrop sets the default policy of the FORWARD chain in the filter table to DROP.
func (ipt *Iptabler) FilterForwardDrop(ctx context.Context, ipv firewaller.IPVersion) error {
var iptv iptables.IPVersion
switch ipv {
case firewaller.IPv4:

View File

@@ -22,11 +22,11 @@ type (
type network struct {
config firewaller.NetworkConfig
ipt *iptabler
ipt *Iptabler
cleanFuncs iptablesCleanFuncs
}
func (ipt *iptabler) NewNetwork(ctx context.Context, nc firewaller.NetworkConfig) (_ firewaller.Network, retErr error) {
func (ipt *Iptabler) NewNetwork(ctx context.Context, nc firewaller.NetworkConfig) (_ firewaller.Network, retErr error) {
n := &network{
ipt: ipt,
config: nc,

View File

@@ -11,7 +11,7 @@ import (
"github.com/moby/moby/v2/daemon/libnetwork/internal/nftables"
)
// Cleanup deletes all rules created by nftabler; it's intended to be used
// Cleanup deletes all rules created by Nftabler; it's intended to be used
// during startup, to clean up rules created by an old incarnation of the daemon
// after switching to a different Firewaller implementation.
func Cleanup(ctx context.Context, config firewaller.Config) {
@@ -31,6 +31,6 @@ func Cleanup(ctx context.Context, config firewaller.Config) {
}
}
func (nft *nftabler) SetFirewallCleaner(fc firewaller.FirewallCleaner) {
func (nft *Nftabler) SetFirewallCleaner(fc firewaller.FirewallCleaner) {
nft.cleaner = fc
}

View File

@@ -18,10 +18,10 @@ import (
type network struct {
config firewaller.NetworkConfig
cleaner func(ctx context.Context) error
fw *nftabler
fw *Nftabler
}
func (nft *nftabler) NewNetwork(ctx context.Context, nc firewaller.NetworkConfig) (_ firewaller.Network, retErr error) {
func (nft *Nftabler) NewNetwork(ctx context.Context, nc firewaller.NetworkConfig) (_ firewaller.Network, retErr error) {
n := &network{
fw: nft,
config: nc,

View File

@@ -45,15 +45,15 @@ const (
rawPreroutingPortsRuleGroup = iota + initialRuleGroup + 1
)
type nftabler struct {
type Nftabler struct {
config firewaller.Config
cleaner firewaller.FirewallCleaner
table4 nftables.TableRef
table6 nftables.TableRef
}
func NewNftabler(ctx context.Context, config firewaller.Config) (firewaller.Firewaller, error) {
nft := &nftabler{config: config}
func NewNftabler(ctx context.Context, config firewaller.Config) (*Nftabler, error) {
nft := &Nftabler{config: config}
if nft.config.IPv4 {
var err error
@@ -85,23 +85,8 @@ func NewNftabler(ctx context.Context, config firewaller.Config) (firewaller.Fire
return nft, nil
}
func (nft *nftabler) getTable(ipv firewaller.IPVersion) nftables.TableRef {
if ipv == firewaller.IPv4 {
return nft.table4
}
return nft.table6
}
func (nft *nftabler) FilterForwardDrop(ctx context.Context, ipv firewaller.IPVersion) error {
table := nft.getTable(ipv)
if err := table.Chain(ctx, forwardChain).SetPolicy("drop"); err != nil {
return err
}
return nftApply(ctx, table)
}
// init creates the bridge driver's nftables table for IPv4 or IPv6.
func (nft *nftabler) init(ctx context.Context, family nftables.Family) (nftables.TableRef, error) {
func (nft *Nftabler) init(ctx context.Context, family nftables.Family) (nftables.TableRef, error) {
// Instantiate the table.
table, err := nftables.NewTable(family, dockerTable)
if err != nil {

View File

@@ -4,6 +4,7 @@ package bridge
import (
"context"
"errors"
"fmt"
"os"
@@ -17,7 +18,24 @@ const (
ipv6ForwardConfAll = "/proc/sys/net/ipv6/conf/all/forwarding"
)
func setupIPv4Forwarding(fw firewaller.Firewaller, wantFilterForwardDrop bool) (retErr error) {
type filterForwardDropper interface {
FilterForwardDrop(context.Context, firewaller.IPVersion) error
}
func checkIPv4Forwarding() error {
enabled, err := getKernelBoolParam(ipv4ForwardConf)
if err != nil {
return fmt.Errorf("checking IPv4 forwarding: %w", err)
}
if enabled {
return nil
}
// It's the user's responsibility to enable forwarding and secure their host. Or,
// start docker with --ip-forward=false to disable this check.
return errors.New("IPv4 forwarding is disabled: check your host's firewalling and set sysctl net.ipv4.ip_forward=1, or disable this check using daemon option --ip-forward=false")
}
func setupIPv4Forwarding(ffd filterForwardDropper, wantFilterForwardDrop bool) (retErr error) {
changed, err := configureIPForwarding(ipv4ForwardConf, '1')
if err != nil {
return err
@@ -34,16 +52,35 @@ func setupIPv4Forwarding(fw firewaller.Firewaller, wantFilterForwardDrop bool) (
// When enabling ip_forward set the default policy on forward chain to drop.
if changed && wantFilterForwardDrop {
if err := fw.FilterForwardDrop(context.TODO(), firewaller.IPv4); err != nil {
if err := ffd.FilterForwardDrop(context.TODO(), firewaller.IPv4); err != nil {
return err
}
}
return nil
}
func setupIPv6Forwarding(fw firewaller.Firewaller, wantFilterForwardDrop bool) (retErr error) {
func checkIPv6Forwarding() error {
enabledDef, err := getKernelBoolParam(ipv6ForwardConfDefault)
if err != nil {
return fmt.Errorf("checking IPv6 default forwarding: %w", err)
}
enabledAll, err := getKernelBoolParam(ipv6ForwardConfAll)
if err != nil {
return fmt.Errorf("checking IPv6 global forwarding: %w", err)
}
if enabledDef && enabledAll {
return nil
}
// It's the user's responsibility to enable forwarding and secure their host. Or,
// start docker with --ip-forward=false to disable this check.
return errors.New("IPv6 global forwarding is disabled: check your host's firewalling and set sysctls net.ipv6.conf.all.forwarding=1 and net.ipv6.conf.default.forwarding=1, or disable this check using daemon option --ip-forward=false")
}
func setupIPv6Forwarding(ffd filterForwardDropper, wantFilterForwardDrop bool) (retErr error) {
// Set IPv6 default.forwarding, if needed.
// FIXME(robmry) - is it necessary to set this, setting "all" (below) does the job?
// Setting "all" (below) sets "default" as well, but need to check that "default" is
// set even if "all" is already set.
changedDef, err := configureIPForwarding(ipv6ForwardConfDefault, '1')
if err != nil {
return err
@@ -74,7 +111,7 @@ func setupIPv6Forwarding(fw firewaller.Firewaller, wantFilterForwardDrop bool) (
}
if (changedAll || changedDef) && wantFilterForwardDrop {
if err := fw.FilterForwardDrop(context.TODO(), firewaller.IPv6); err != nil {
if err := ffd.FilterForwardDrop(context.TODO(), firewaller.IPv6); err != nil {
return err
}
}

View File

@@ -14,19 +14,20 @@ import (
is "gotest.tools/v3/assert/cmp"
)
type ffdTestFirewaller struct {
ffd firewaller.IPVersion
}
// NewNetwork is part of interface [firewaller.Firewaller].
func (f *ffdTestFirewaller) NewNetwork(_ context.Context, _ firewaller.NetworkConfig) (firewaller.Network, error) {
return nil, nil
type ffDropper struct {
ffDrop4 bool
ffDrop6 bool
}
// FilterForwardDrop is part of interface [firewaller.Firewaller]. Just enough to check
// it was called with the expected IPVersion.
func (f *ffdTestFirewaller) FilterForwardDrop(_ context.Context, ipv firewaller.IPVersion) error {
f.ffd = ipv
func (f *ffDropper) FilterForwardDrop(_ context.Context, ipv firewaller.IPVersion) error {
switch ipv {
case firewaller.IPv4:
f.ffDrop4 = true
case firewaller.IPv6:
f.ffDrop6 = true
}
return nil
}
@@ -36,21 +37,16 @@ func TestSetupIPForwarding(t *testing.T) {
for _, wantFFD := range []bool{true, false} {
t.Run(fmt.Sprintf("wantFFD=%v", wantFFD), func(t *testing.T) {
// Disable IP Forwarding if enabled
_, err := configureIPForwarding(ipv4ForwardConf, '0')
assert.NilError(t, err)
setForwarding(t, '0')
// Set IP Forwarding
fw := &ffdTestFirewaller{}
err = setupIPv4Forwarding(fw, wantFFD)
ffd := &ffDropper{}
err := setupIPv4Forwarding(ffd, wantFFD)
assert.NilError(t, err)
// Check what the firewaller was told.
if wantFFD {
assert.Check(t, is.Equal(fw.ffd, firewaller.IPv4))
} else {
var noVer firewaller.IPVersion
assert.Check(t, is.Equal(fw.ffd, noVer))
}
assert.Check(t, is.Equal(ffd.ffDrop4, wantFFD))
assert.Check(t, !ffd.ffDrop6)
// Read new setting
procSetting, err := os.ReadFile(ipv4ForwardConf)
@@ -65,23 +61,15 @@ func TestSetupIP6Forwarding(t *testing.T) {
for _, wantFFD := range []bool{true, false} {
t.Run(fmt.Sprintf("wantFFD=%v", wantFFD), func(t *testing.T) {
_, err := configureIPForwarding(ipv6ForwardConfDefault, '0')
assert.NilError(t, err)
_, err = configureIPForwarding(ipv6ForwardConfAll, '0')
assert.NilError(t, err)
// Disable IP Forwarding if enabled
setForwarding(t, '0')
// Set IP Forwarding
fw := &ffdTestFirewaller{}
err = setupIPv6Forwarding(fw, wantFFD)
ffd := &ffDropper{}
err := setupIPv6Forwarding(ffd, wantFFD)
assert.NilError(t, err)
// Check what the firewaller was told.
if wantFFD {
assert.Check(t, is.Equal(fw.ffd, firewaller.IPv6))
} else {
var noVer firewaller.IPVersion
assert.Check(t, is.Equal(fw.ffd, noVer))
}
assert.Check(t, !ffd.ffDrop4)
assert.Check(t, is.Equal(ffd.ffDrop6, wantFFD))
// Read new setting
procSetting, err := os.ReadFile(ipv6ForwardConfDefault)
@@ -93,3 +81,30 @@ func TestSetupIP6Forwarding(t *testing.T) {
})
}
}
func TestCheckForwarding(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
setForwarding(t, '0')
err := checkIPv4Forwarding()
assert.Check(t, is.ErrorContains(err, "IPv4 forwarding is disabled"))
err = checkIPv6Forwarding()
assert.Check(t, is.ErrorContains(err, "IPv6 global forwarding is disabled"))
setForwarding(t, '1')
err = checkIPv4Forwarding()
assert.Check(t, err)
err = checkIPv6Forwarding()
assert.Check(t, err)
}
func setForwarding(t *testing.T, val byte) {
for _, sysctl := range []string{
ipv4ForwardConf,
ipv6ForwardConfDefault,
ipv6ForwardConfAll,
} {
err := os.WriteFile(sysctl, []byte{val, '\n'}, 0o644)
assert.NilError(t, err)
}
}

View File

@@ -3,6 +3,7 @@
package iptables
import (
"bytes"
"context"
"errors"
"fmt"
@@ -411,6 +412,16 @@ func (iptable IPTable) SetDefaultPolicy(table Table, chain string, policy Policy
return nil
}
// HasPolicy returns true if the chain exists and has the given policy.
func (iptable IPTable) HasPolicy(table Table, chain string, policy Policy) bool {
out, err := iptable.Raw("-t", string(table), "-L", chain)
if err != nil {
return false
}
firstLine, _, _ := bytes.Cut(out, []byte("\n"))
return strings.Contains(string(firstLine), "policy "+string(policy))
}
// AddReturnRule adds a return rule for the chain in the filter table
func (iptable IPTable) AddReturnRule(table Table, chain string) error {
if iptable.Exists(table, chain, "-j", "RETURN") {

View File

@@ -67,6 +67,13 @@ fi
dockerd="dockerd"
# When running with the nftables backend, dockerd will not enable IP forwarding (by default, it
# will error on network creation if forwarding is not enabled).
if [ "$DOCKER_FIREWALL_BACKEND" = "nftables" ]; then
sysctl -w net.ipv4.ip_forward=1 > /dev/null
sysctl -w net.ipv6.conf.all.forwarding=1 > /dev/null
fi
if [ -n "$DOCKER_ROOTLESS" ]; then
if [ -z "$TEST_SKIP_INTEGRATION_CLI" ]; then
echo >&2 '# DOCKER_ROOTLESS requires TEST_SKIP_INTEGRATION_CLI to be set'

View File

@@ -1,7 +1,6 @@
package networking
import (
"fmt"
"os/exec"
"regexp"
"strings"
@@ -9,45 +8,29 @@ import (
"github.com/moby/moby/v2/testutil/daemon"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)
const (
// The name of the bridge driver's nftables tables.
nftTable = "docker-bridges"
// The name of the filter-FORWARD chain in nftTable.
nftFFChain = "filter-FORWARD"
)
// Find the policy in, for example "Chain FORWARD (policy ACCEPT)".
var rePolicy = regexp.MustCompile("policy ([A-Za-z]+)")
// SetFilterForwardPolicies sets the default policy for the FORWARD chain in
// the filter tables for both IPv4 and IPv6. The original policy is restored
// using t.Cleanup().
// the iptables filter tables for both IPv4 and IPv6. The original policy is
// restored using t.Cleanup().
//
// There's only one filter-FORWARD policy, so this won't behave well if used by
// tests running in parallel in a single network namespace that expect different
// behaviour.
func SetFilterForwardPolicies(t *testing.T, firewallBackend string, policy string) {
t.Helper()
if strings.HasPrefix(firewallBackend, "iptables") {
setIptablesFFP(t, policy)
return
}
if strings.HasPrefix(firewallBackend, "nftables") {
setNftablesFFP(t, policy)
return
}
t.Fatalf("unknown firewall backend %s", firewallBackend)
}
func setIptablesFFP(t *testing.T, policy string) {
func SetFilterForwardPolicies(t *testing.T, policy string) {
t.Helper()
for _, iptablesCmd := range []string{"iptables", "ip6tables"} {
origPolicy, err := getChainPolicy(t, exec.Command(iptablesCmd, "-L", "FORWARD"))
assert.NilError(t, err, "failed to get iptables policy")
out, err := exec.Command(iptablesCmd, "-L", "FORWARD").Output()
assert.NilError(t, err, "failed to get %s policy", iptablesCmd)
opMatch := rePolicy.FindSubmatch(out)
assert.Assert(t, is.Len(opMatch, 2), "searching for policy: %w", err)
origPolicy := string(opMatch[1])
if origPolicy == policy {
continue
}
@@ -62,42 +45,6 @@ func setIptablesFFP(t *testing.T, policy string) {
}
}
func setNftablesFFP(t *testing.T, policy string) {
t.Helper()
policy = strings.ToLower(policy)
for _, family := range []string{"ip", "ip6"} {
origPolicy, err := getChainPolicy(t, exec.Command("nft", "list", "chain", family, nftTable, nftFFChain))
assert.NilError(t, err, "failed to get nftables policy")
if origPolicy == policy {
continue
}
cmd := func(p string) *exec.Cmd {
return exec.Command("nft", "add", "chain", family, nftTable, nftFFChain, "{", "policy", p, ";", "}")
}
if err := cmd(policy).Run(); err != nil {
t.Fatalf("Failed to set %s filter-FORWARD policy: %v", family, err)
}
t.Cleanup(func() {
if err := cmd(origPolicy).Run(); err != nil {
t.Logf("Failed to restore %s filter-FORWARD policy: %v", family, err)
}
})
}
}
func getChainPolicy(t *testing.T, cmd *exec.Cmd) (string, error) {
t.Helper()
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("getting policy: %w", err)
}
opMatch := rePolicy.FindSubmatch(out)
if len(opMatch) != 2 {
return "", fmt.Errorf("searching for policy: %w", err)
}
return string(opMatch[1]), nil
}
// FirewalldRunning returns true if "firewall-cmd --state" reports "running".
func FirewalldRunning() bool {
state, err := exec.Command("firewall-cmd", "--state").CombinedOutput()

View File

@@ -77,6 +77,8 @@ func (l3 *L3Segment) AddHost(t *testing.T, hostname, nsName, ifname string, addr
l3.bridge.MustRun(t, "ip", "link", "set", hostname, "up", "master", l3.bridge.Iface)
host.MustRun(t, "ip", "link", "set", host.Iface, "up")
host.MustRun(t, "ip", "link", "set", "lo", "up")
host.MustRun(t, "sysctl", "-w", "net.ipv4.ip_forward=1")
host.MustRun(t, "sysctl", "-w", "net.ipv6.conf.all.forwarding=1")
for _, addr := range addrs {
host.MustRun(t, "ip", "addr", "add", addr.String(), "dev", host.Iface, "nodad")

View File

@@ -246,6 +246,8 @@ func TestIPRangeAt64BitLimit(t *testing.T) {
func TestFilterForwardPolicy(t *testing.T) {
skip.If(t, testEnv.IsRootless, "rootless has its own netns")
skip.If(t, networking.FirewalldRunning(), "can't use firewalld in host netns to add rules in L3Segment")
skip.If(t, strings.HasPrefix(testEnv.FirewallBackendDriver(), "nftables"), "no policy is set for nftables")
ctx := setupTest(t)
// Set up a netns for each test to avoid sysctl and iptables pollution.
@@ -305,30 +307,17 @@ func TestFilterForwardPolicy(t *testing.T) {
)
host := l3.Hosts[hostname]
getFwdPolicy := func(usingNftables bool, fam string) string {
getFwdPolicy := func(cmd string) string {
t.Helper()
if usingNftables {
out := host.MustRun(t, "nft", "list chain "+fam+" docker-bridges filter-FORWARD")
if strings.Contains(out, "policy accept") {
return "ACCEPT"
}
if strings.Contains(out, "policy drop") {
return "DROP"
}
t.Fatalf("Failed to determine nftables filter-FORWARD policy: %s", out)
return ""
} else {
cmd := fam + "tables"
out := host.MustRun(t, cmd, "-S", "FORWARD")
if strings.HasPrefix(out, "-P FORWARD ACCEPT") {
return "ACCEPT"
}
if strings.HasPrefix(out, "-P FORWARD DROP") {
return "DROP"
}
t.Fatalf("Failed to determine %s FORWARD policy: %s", cmd, out)
return ""
out := host.MustRun(t, cmd, "-S", "FORWARD")
if strings.HasPrefix(out, "-P FORWARD ACCEPT") {
return "ACCEPT"
}
if strings.HasPrefix(out, "-P FORWARD DROP") {
return "DROP"
}
t.Fatalf("Failed to determine %s FORWARD policy: %s", cmd, out)
return ""
}
type sysctls struct{ v4, v6def, v6all string }
@@ -354,22 +343,21 @@ func TestFilterForwardPolicy(t *testing.T) {
d.StartWithBusybox(ctx, t, tc.daemonArgs...)
t.Cleanup(func() { d.Stop(t) })
})
usingNftables := d.FirewallBackendDriver(t) == "nftables"
c := d.NewClientT(t)
t.Cleanup(func() { c.Close() })
// If necessary, the IPv4 policy should have been updated when the default bridge network was created.
assert.Check(t, is.Equal(getFwdPolicy(usingNftables, "ip"), tc.expPolicy))
assert.Check(t, is.Equal(getFwdPolicy("iptables"), tc.expPolicy))
// IPv6 policy should not have been updated yet.
assert.Check(t, is.Equal(getFwdPolicy(usingNftables, "ip6"), "ACCEPT"))
assert.Check(t, is.Equal(getFwdPolicy("ip6tables"), "ACCEPT"))
assert.Check(t, is.Equal(getSysctls(), sysctls{tc.expForwarding, tc.initForwarding, tc.initForwarding}))
// If necessary, creating an IPv6 network should update the sysctls and policy.
const netName = "testnetffp"
network.CreateNoError(ctx, t, c, netName, network.WithIPv6())
t.Cleanup(func() { network.RemoveNoError(ctx, t, c, netName) })
assert.Check(t, is.Equal(getFwdPolicy(usingNftables, "ip"), tc.expPolicy))
assert.Check(t, is.Equal(getFwdPolicy(usingNftables, "ip6"), tc.expPolicy))
assert.Check(t, is.Equal(getFwdPolicy("iptables"), tc.expPolicy))
assert.Check(t, is.Equal(getFwdPolicy("ip6tables"), tc.expPolicy))
assert.Check(t, is.Equal(getSysctls(), sysctls{tc.expForwarding, tc.expForwarding, tc.expForwarding}))
})
}

View File

@@ -358,7 +358,6 @@ func TestBridgeINCRouted(t *testing.T) {
d := daemon.New(t)
d.StartWithBusybox(ctx, t)
t.Cleanup(func() { d.Stop(t) })
firewallBackend := d.FirewallBackendDriver(t)
c := d.NewClientT(t)
t.Cleanup(func() { c.Close() })
@@ -457,10 +456,12 @@ func TestBridgeINCRouted(t *testing.T) {
},
}
for _, fwdPolicy := range []string{"ACCEPT", "DROP"} {
networking.SetFilterForwardPolicies(t, firewallBackend, fwdPolicy)
runTests := func(testName, policy string) {
networking.FirewalldReload(t, d)
t.Run(fwdPolicy, func(t *testing.T) {
t.Run(testName, func(t *testing.T) {
if policy != "" {
networking.SetFilterForwardPolicies(t, policy)
}
for _, tc := range testcases {
t.Run(tc.name+"/v4/ping", func(t *testing.T) {
t.Parallel()
@@ -497,6 +498,13 @@ func TestBridgeINCRouted(t *testing.T) {
}
})
}
if strings.HasPrefix(d.FirewallBackendDriver(t), "iptables") {
runTests("iptables-ACCEPT", "ACCEPT")
runTests("iptables-DROP", "DROP")
} else {
runTests("nftables", "")
}
}
// TestAccessToPublishedPort checks that a container in one network can

View File

@@ -648,7 +648,6 @@ func TestDirectRoutingOpenPorts(t *testing.T) {
d := daemon.New(t)
d.StartWithBusybox(ctx, t)
t.Cleanup(func() { d.Stop(t) })
firewallBackend := d.FirewallBackendDriver(t)
c := d.NewClientT(t)
t.Cleanup(func() { c.Close() })
@@ -770,9 +769,11 @@ func TestDirectRoutingOpenPorts(t *testing.T) {
// Run the ping and http tests in two parallel groups, rather than waiting for
// ping/http timeouts separately. (The iptables filter-FORWARD policy affects the
// whole host, so ACCEPT/DROP tests can't be parallelized).
for _, fwdPolicy := range []string{"ACCEPT", "DROP"} {
networking.SetFilterForwardPolicies(t, firewallBackend, fwdPolicy)
t.Run(fwdPolicy, func(t *testing.T) {
runTests := func(testName, policy string) {
t.Run(testName, func(t *testing.T) {
if policy != "" {
networking.SetFilterForwardPolicies(t, policy)
}
for gwMode := range networks {
t.Run(gwMode+"/v4/ping", func(t *testing.T) {
testPing(t, "ping", networks[gwMode].ipv4, expPingExit[gwMode])
@@ -795,6 +796,13 @@ func TestDirectRoutingOpenPorts(t *testing.T) {
}
})
}
if strings.HasPrefix(d.FirewallBackendDriver(t), "iptables") {
runTests("iptables-ACCEPT", "ACCEPT")
runTests("iptables-DROP", "DROP")
} else {
runTests("nftables", "")
}
}
func TestAcceptFwMark(t *testing.T) {