From 29e0db25e7d81dd04e2f024bca9f097c3cd90ea6 Mon Sep 17 00:00:00 2001 From: Rob Murray Date: Wed, 12 Feb 2025 18:10:01 +0000 Subject: [PATCH] Factor out top-level iptables setup into its own object Signed-off-by: Rob Murray --- libnetwork/drivers/bridge/bridge_linux.go | 110 ++++++++++-------- .../drivers/bridge/bridge_linux_test.go | 6 +- .../drivers/bridge/port_mapping_linux.go | 8 +- .../drivers/bridge/setup_ip_tables_linux.go | 43 +++---- .../bridge/setup_ip_tables_linux_test.go | 6 +- 5 files changed, 89 insertions(+), 84 deletions(-) diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go index 815a195afb..e3ef3130c5 100644 --- a/libnetwork/drivers/bridge/bridge_linux.go +++ b/libnetwork/drivers/bridge/bridge_linux.go @@ -67,6 +67,12 @@ type configuration struct { Rootless bool } +type firewaller struct { + IPv4 bool + IPv6 bool + Hairpin bool +} + // networkConfiguration for network specific configuration type networkConfiguration struct { ID string @@ -159,6 +165,7 @@ type driver struct { nlh nlwrap.Handle portDriverClient portDriverClient configNetwork sync.Mutex + firewaller firewaller sync.Mutex } @@ -403,16 +410,13 @@ func (n *bridgeNetwork) newIptablesNetwork() (*iptablesNetwork, error) { if err != nil { return nil, err } - return newIptablesNetwork(networkConfig{ + return n.driver.firewaller.NewNetwork(networkConfig{ IfName: n.config.BridgeName, Internal: n.config.Internal, ICC: n.config.EnableICC, Masquerade: n.config.EnableIPMasquerade, Config4: config4, Config6: config6, - Hairpin: !n.driver.config.EnableUserlandProxy || n.driver.config.UserlandProxyPath == "", - Enable4: n.driver.config.EnableIPTables, - Enable6: n.driver.config.EnableIP6Tables, }) } @@ -503,50 +507,12 @@ func (d *driver) configure(option map[string]interface{}) error { return errdefs.InvalidParameter(fmt.Errorf("invalid configuration type (%T) passed", opt)) } - if config.EnableIPTables { - removeIPChains(iptables.IPv4) - - if err := setupIPChains(config, iptables.IPv4); err != nil { - return err - } - - // Make sure on firewall reload, first thing being re-played is chains creation - iptables.OnReloaded(func() { - log.G(context.TODO()).Debugf("Recreating iptables chains on firewall reload") - if err := setupIPChains(config, iptables.IPv4); err != nil { - log.G(context.TODO()).WithError(err).Error("Error reloading iptables chains") - } - }) - } - - if config.EnableIP6Tables { - if err := modprobe.LoadModules(context.TODO(), func() error { - iptable := iptables.GetIptable(iptables.IPv6) - _, err := iptable.Raw("-t", "filter", "-n", "-L", "FORWARD") - return err - }, "ip6_tables"); err != nil { - log.G(context.TODO()).WithError(err).Debug("Loading ip6_tables") - } - - removeIPChains(iptables.IPv6) - - if err := setupIPChains(config, iptables.IPv6); err != nil { - // If the chains couldn't be set up, it's probably because the kernel has no IPv6 - // support, or it doesn't have module ip6_tables loaded. It won't be possible to - // create IPv6 networks without enabling ip6_tables in the kernel, or disabling - // ip6tables in the daemon config. But, allow the daemon to start because IPv4 - // will work. So, log the problem, and continue. - log.G(context.TODO()).WithError(err).Warn("ip6tables is enabled, but cannot set up ip6tables chains") - } else { - // Make sure on firewall reload, first thing being re-played is chains creation - iptables.OnReloaded(func() { - log.G(context.TODO()).Debugf("Recreating ip6tables chains on firewall reload") - if err := setupIPChains(config, iptables.IPv6); err != nil { - log.G(context.TODO()).WithError(err).Error("Error reloading ip6tables chains") - } - }) - } + d.firewaller = firewaller{ + IPv4: config.EnableIPTables, + IPv6: config.EnableIP6Tables, + Hairpin: !config.EnableUserlandProxy || config.UserlandProxyPath == "", } + d.firewaller.init() iptables.OnReloaded(d.handleFirewalldReload) var pdc portDriverClient @@ -566,6 +532,56 @@ func (d *driver) configure(option map[string]interface{}) error { return d.initStore() } +func (fw *firewaller) init() error { + if fw.IPv4 { + removeIPChains(iptables.IPv4) + + if err := setupIPChains(iptables.IPv4, fw.Hairpin); err != nil { + return err + } + + // Make sure on firewall reload, first thing being re-played is chains creation + iptables.OnReloaded(func() { + log.G(context.TODO()).Debugf("Recreating iptables chains on firewall reload") + if err := setupIPChains(iptables.IPv4, fw.Hairpin); err != nil { + log.G(context.TODO()).WithError(err).Error("Error reloading iptables chains") + } + }) + } + + if fw.IPv6 { + if err := modprobe.LoadModules(context.TODO(), func() error { + iptable := iptables.GetIptable(iptables.IPv6) + _, err := iptable.Raw("-t", "filter", "-n", "-L", "FORWARD") + return err + }, "ip6_tables"); err != nil { + log.G(context.TODO()).WithError(err).Debug("Loading ip6_tables") + } + + removeIPChains(iptables.IPv6) + + err := setupIPChains(iptables.IPv6, fw.Hairpin) + if err != nil { + // If the chains couldn't be set up, it's probably because the kernel has no IPv6 + // support, or it doesn't have module ip6_tables loaded. It won't be possible to + // create IPv6 networks without enabling ip6_tables in the kernel, or disabling + // ip6tables in the daemon config. But, allow the daemon to start because IPv4 + // will work. So, log the problem, and continue. + log.G(context.TODO()).WithError(err).Warn("ip6tables is enabled, but cannot set up ip6tables chains") + } else { + // Make sure on firewall reload, first thing being re-played is chains creation + iptables.OnReloaded(func() { + log.G(context.TODO()).Debugf("Recreating ip6tables chains on firewall reload") + if err := setupIPChains(iptables.IPv6, fw.Hairpin); err != nil { + log.G(context.TODO()).WithError(err).Error("Error reloading ip6tables chains") + } + }) + } + } + + return nil +} + func (d *driver) getNetwork(id string) (*bridgeNetwork, error) { d.Lock() defer d.Unlock() diff --git a/libnetwork/drivers/bridge/bridge_linux_test.go b/libnetwork/drivers/bridge/bridge_linux_test.go index 5e9350d61d..b354903d3b 100644 --- a/libnetwork/drivers/bridge/bridge_linux_test.go +++ b/libnetwork/drivers/bridge/bridge_linux_test.go @@ -1260,13 +1260,9 @@ func TestCleanupIptableRules(t *testing.T) { } ipVersions := []iptables.IPVersion{iptables.IPv4, iptables.IPv6} - configs := map[iptables.IPVersion]configuration{ - iptables.IPv4: {EnableIPTables: true}, - iptables.IPv6: {EnableIP6Tables: true}, - } for _, version := range ipVersions { - err := setupIPChains(configs[version], version) + err := setupIPChains(version, true) assert.NilError(t, err, "version:%s", version) iptable := iptables.GetIptable(version) diff --git a/libnetwork/drivers/bridge/port_mapping_linux.go b/libnetwork/drivers/bridge/port_mapping_linux.go index e70795045e..00eae6a790 100644 --- a/libnetwork/drivers/bridge/port_mapping_linux.go +++ b/libnetwork/drivers/bridge/port_mapping_linux.go @@ -792,11 +792,11 @@ func (n *iptablesNetwork) modPorts(ctx context.Context, pbs []types.PortBinding, func (n *iptablesNetwork) setPerPortIptables(ctx context.Context, b types.PortBinding, enable bool) error { v := iptables.IPv4 - enabled := n.Enable4 + enabled := n.fw.IPv4 config := n.Config4 if b.IP.To4() == nil { v = iptables.IPv6 - enabled = n.Enable6 + enabled = n.fw.IPv6 config = n.Config6 } @@ -851,7 +851,7 @@ func (n *iptablesNetwork) setPerPortNAT(ipv iptables.IPVersion, b types.PortBind "-j", "DNAT", "--to-destination", net.JoinHostPort(b.IP.String(), strconv.Itoa(int(b.Port))), } - if !n.Hairpin { + if !n.fw.Hairpin { args = append(args, "!", "-i", n.IfName) } if ipv == iptables.IPv6 { @@ -869,7 +869,7 @@ func (n *iptablesNetwork) setPerPortNAT(ipv iptables.IPVersion, b types.PortBind "--dport", strconv.Itoa(int(b.Port)), "-j", "MASQUERADE", }} - if err := appendOrDelChainRule(rule, "MASQUERADE", n.Hairpin && enable); err != nil { + if err := appendOrDelChainRule(rule, "MASQUERADE", n.fw.Hairpin && enable); err != nil { return err } diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go index 9e414c3965..a7e89f2eef 100644 --- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go +++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go @@ -42,15 +42,7 @@ const ( // Can be modified by tests. var wslinfoPath = "/usr/bin/wslinfo" -func setupIPChains(config configuration, version iptables.IPVersion) (retErr error) { - // Sanity check. - if version == iptables.IPv4 && !config.EnableIPTables { - return errors.New("cannot create new chains, iptables is disabled") - } - if version == iptables.IPv6 && !config.EnableIP6Tables { - return errors.New("cannot create new chains, ip6tables is disabled") - } - +func setupIPChains(version iptables.IPVersion, hairpin bool) (retErr error) { iptable := iptables.GetIptable(version) _, err := iptable.NewChain(DockerChain, iptables.Nat) @@ -137,12 +129,12 @@ func setupIPChains(config configuration, version iptables.IPVersion) (retErr err } }() - if err := addNATJumpRules(version, !config.EnableUserlandProxy, true); err != nil { + if err := addNATJumpRules(version, hairpin, true); err != nil { return fmt.Errorf("failed to add jump rules to %s NAT table: %w", version, err) } defer func() { if retErr != nil { - if err := addNATJumpRules(version, !config.EnableUserlandProxy, false); err != nil { + if err := addNATJumpRules(version, hairpin, false); err != nil { log.G(context.TODO()).Warnf("failed on removing jump rules from %s NAT table: %v", version, err) } } @@ -164,7 +156,7 @@ func setupIPChains(config configuration, version iptables.IPVersion) (retErr err return err } - if err := mirroredWSL2Workaround(config, version); err != nil { + if err := mirroredWSL2Workaround(version, hairpin); err != nil { return err } @@ -206,23 +198,24 @@ type networkConfig struct { Masquerade bool Config4 networkConfigFam Config6 networkConfigFam - Hairpin bool - Enable4 bool - Enable6 bool } type iptablesNetwork struct { networkConfig + fw *firewaller cleanFuncs iptablesCleanFuncs } -func newIptablesNetwork(nc networkConfig) (_ *iptablesNetwork, retErr error) { +func (fw *firewaller) NewNetwork(nc networkConfig) (_ *iptablesNetwork, retErr error) { n := &iptablesNetwork{ + fw: fw, networkConfig: nc, } defer func() { if retErr != nil { - n.delNetworkLevelRules() + if err := n.delNetworkLevelRules(); err != nil { + log.G(context.TODO()).WithError(err).Warnf("Failed to delete network level rules following earlier error") + } } }() @@ -233,12 +226,12 @@ func newIptablesNetwork(nc networkConfig) (_ *iptablesNetwork, retErr error) { } func (n *iptablesNetwork) reapplyNetworkLevelRules() error { - if n.Enable4 { + if n.fw.IPv4 { if err := n.configure(iptables.IPv4, n.Config4); err != nil { return err } } - if n.Enable6 { + if n.fw.IPv6 { if err := n.configure(iptables.IPv6, n.Config6); err != nil { return err } @@ -510,7 +503,7 @@ func (n *iptablesNetwork) setupNonInternalNetworkRules(ipVer iptables.IPVersion, // enable access to ports published by containers in the same network. But, the INC rules // will block access to that published port from containers in other networks. (However, // users may add a rule to DOCKER-USER to work around the INC rules if needed.) - if !n.Hairpin { + if !n.fw.Hairpin { skipDNAT := iptables.Rule{IPVer: ipVer, Table: iptables.Nat, Chain: DockerChain, Args: []string{ "-i", n.IfName, "-j", "RETURN", @@ -523,7 +516,7 @@ func (n *iptablesNetwork) setupNonInternalNetworkRules(ipVer iptables.IPVersion, // In hairpin mode, masquerade traffic from localhost. If hairpin is disabled or if we're tearing down // that bridge, make sure the iptables rule isn't lying around. - if err := programChainRule(hpNatRule, "MASQ LOCAL HOST", enable && n.Hairpin); err != nil { + if err := programChainRule(hpNatRule, "MASQ LOCAL HOST", enable && n.fw.Hairpin); err != nil { return err } @@ -859,12 +852,12 @@ func clearConntrackEntries(nlh nlwrap.Handle, ep *bridgeEndpoint) { // arriving from any other bridge network. Similarly, this function adds (or // removes) a rule to RETURN early for packets delivered via loopback0 with // destination 127.0.0.0/8. -func mirroredWSL2Workaround(config configuration, ipv iptables.IPVersion) error { +func mirroredWSL2Workaround(ipv iptables.IPVersion, hairpin bool) error { // WSL2 does not (currently) support Windows<->Linux communication via ::1. if ipv != iptables.IPv4 { return nil } - return programChainRule(mirroredWSL2Rule(), "WSL2 loopback", insertMirroredWSL2Rule(config)) + return programChainRule(mirroredWSL2Rule(), "WSL2 loopback", insertMirroredWSL2Rule(hairpin)) } // insertMirroredWSL2Rule returns true if the NAT rule for mirrored WSL2 workaround @@ -875,8 +868,8 @@ func mirroredWSL2Workaround(config configuration, ipv iptables.IPVersion) error // running - no workaround is needed, the normal DNAT/masquerading works. // - and, the host Linux appears to be running under Windows WSL2 with mirrored // mode networking. -func insertMirroredWSL2Rule(config configuration) bool { - if !config.EnableUserlandProxy || config.UserlandProxyPath == "" { +func insertMirroredWSL2Rule(hairpin bool) bool { + if hairpin { return false } return isRunningUnderWSL2MirroredMode() diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go index 766fb1656b..aa11b85a05 100644 --- a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go +++ b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go @@ -157,11 +157,11 @@ func assertIPTableChainProgramming(rule iptables.Rule, descr string, t *testing. func assertChainConfig(d *driver, t *testing.T) { var err error - err = setupIPChains(d.config, iptables.IPv4) + err = setupIPChains(iptables.IPv4, !d.config.EnableUserlandProxy) assert.NilError(t, err) if d.config.EnableIP6Tables { - err = setupIPChains(d.config, iptables.IPv6) + err = setupIPChains(iptables.IPv6, !d.config.EnableUserlandProxy) assert.NilError(t, err) } } @@ -462,7 +462,7 @@ func TestMirroredWSL2Workaround(t *testing.T) { config.UserlandProxyPath = "some-proxy" config.EnableUserlandProxy = true } - err := setupIPChains(config, iptables.IPv4) + err := setupIPChains(iptables.IPv4, !tc.userlandProxy) assert.NilError(t, err) assert.Check(t, is.Equal(mirroredWSL2Rule().Exists(), tc.expLoopback0Rule)) })