Factor out top-level iptables setup into its own object

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2025-02-12 18:10:01 +00:00
parent bcbcbb73fa
commit 29e0db25e7
5 changed files with 89 additions and 84 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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))
})