Browse code

Add daemon option --firewall-backend

Signed-off-by: Rob Murray <rob.murray@docker.com>

Rob Murray authored on 2025/06/10 03:39:21
Showing 16 changed files
... ...
@@ -51,6 +51,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
51 51
 	flags.BoolVar(&conf.NoNewPrivileges, "no-new-privileges", false, "Set no-new-privileges by default for new containers")
52 52
 	flags.StringVar(&conf.IpcMode, "default-ipc-mode", conf.IpcMode, `Default mode for containers ipc ("shareable" | "private")`)
53 53
 	flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "Default address pools for node specific local networks")
54
+	flags.StringVar(&conf.NetworkConfig.FirewallBackend, "firewall-backend", "", "Firewall backend to use, iptables or nftables")
54 55
 	// rootless needs to be explicitly specified for running "rootful" dockerd in rootless dockerd (#38702)
55 56
 	// Note that conf.BridgeConfig.UserlandProxyPath and honorXDG are configured according to the value of rootless.RunningWithRootlessKit, not the value of --rootless.
56 57
 	flags.BoolVar(&conf.Rootless, "rootless", conf.Rootless, "Enable rootless mode; typically used with RootlessKit")
... ...
@@ -148,6 +148,10 @@ type NetworkConfig struct {
148 148
 	NetworkControlPlaneMTU int `json:"network-control-plane-mtu,omitempty"`
149 149
 	// Default options for newly created networks
150 150
 	DefaultNetworkOpts map[string]map[string]string `json:"default-network-opts,omitempty"`
151
+	// FirewallBackend overrides the daemon's default selection of firewall
152
+	// implementation. Currently only used on Linux, it is an error to
153
+	// supply a value for other platforms.
154
+	FirewallBackend string `json:"firewall-backend,omitempty"`
151 155
 }
152 156
 
153 157
 // TLSOptions defines TLS configuration for the daemon server.
... ...
@@ -243,6 +243,10 @@ func validatePlatformConfig(conf *Config) error {
243 243
 		return errors.Wrap(err, "invalid fixed-cidr-v6")
244 244
 	}
245 245
 
246
+	if err := validateFirewallBackend(conf.FirewallBackend); err != nil {
247
+		return errors.Wrap(err, "invalid firewall-backend")
248
+	}
249
+
246 250
 	return verifyDefaultCgroupNsMode(conf.CgroupNamespaceMode)
247 251
 }
248 252
 
... ...
@@ -294,6 +298,14 @@ func verifyDefaultIpcMode(mode string) error {
294 294
 	return nil
295 295
 }
296 296
 
297
+func validateFirewallBackend(val string) error {
298
+	switch val {
299
+	case "", "iptables", "nftables":
300
+		return nil
301
+	}
302
+	return errors.New(`allowed values are "iptables" and "nftables"`)
303
+}
304
+
297 305
 func verifyDefaultCgroupNsMode(mode string) error {
298 306
 	cm := container.CgroupnsMode(mode)
299 307
 	if !cm.Valid() {
... ...
@@ -2,6 +2,7 @@ package config
2 2
 
3 3
 import (
4 4
 	"context"
5
+	"errors"
5 6
 	"fmt"
6 7
 	"os"
7 8
 	"path/filepath"
... ...
@@ -88,6 +89,9 @@ func validatePlatformConfig(conf *Config) error {
88 88
 	if conf.MTU != 0 && conf.MTU != DefaultNetworkMtu {
89 89
 		log.G(context.TODO()).Warn(`WARNING: MTU for the default network is not configurable on Windows, and this option will be ignored.`)
90 90
 	}
91
+	if conf.FirewallBackend != "" {
92
+		return errors.New("firewall-backend can only be configured on Linux")
93
+	}
91 94
 	return nil
92 95
 }
93 96
 
... ...
@@ -1457,6 +1457,7 @@ func (daemon *Daemon) networkOptions(conf *config.Config, pg plugingetter.Plugin
1457 1457
 		nwconfig.OptionDefaultNetwork(network.DefaultNetwork),
1458 1458
 		nwconfig.OptionLabels(conf.Labels),
1459 1459
 		nwconfig.OptionNetworkControlPlaneMTU(conf.NetworkControlPlaneMTU),
1460
+		nwconfig.OptionFirewallBackend(conf.FirewallBackend),
1460 1461
 		driverOptions(conf),
1461 1462
 	}
1462 1463
 
... ...
@@ -42,6 +42,7 @@ type Config struct {
42 42
 	DatastoreBucket        string
43 43
 	ActiveSandboxes        map[string]any
44 44
 	PluginGetter           plugingetter.PluginGetter
45
+	FirewallBackend        string
45 46
 }
46 47
 
47 48
 // New creates a new Config and initializes it with the given Options.
... ...
@@ -154,3 +155,10 @@ func OptionActiveSandboxes(sandboxes map[string]any) Option {
154 154
 		c.ActiveSandboxes = sandboxes
155 155
 	}
156 156
 }
157
+
158
+// OptionFirewallBackend returns an option setter for selection of the firewall backend.
159
+func OptionFirewallBackend(val string) Option {
160
+	return func(c *Config) {
161
+		c.FirewallBackend = val
162
+	}
163
+}
... ...
@@ -168,7 +168,9 @@ func New(ctx context.Context, cfgOptions ...config.Option) (_ *Controller, retEr
168 168
 		diagnosticServer: diagnostic.New(),
169 169
 	}
170 170
 
171
-	c.selectFirewallBackend()
171
+	if err := c.selectFirewallBackend(); err != nil {
172
+		return nil, err
173
+	}
172 174
 	c.drvRegistry.Notify = c
173 175
 
174 176
 	// External plugins don't need config passed through daemon. They can
... ...
@@ -4,7 +4,6 @@ import (
4 4
 	"context"
5 5
 	"errors"
6 6
 	"fmt"
7
-	"os"
8 7
 
9 8
 	"github.com/containerd/log"
10 9
 	"github.com/docker/docker/daemon/libnetwork/internal/nftables"
... ...
@@ -13,16 +12,23 @@ import (
13 13
 
14 14
 const userChain = "DOCKER-USER"
15 15
 
16
-func (c *Controller) selectFirewallBackend() {
17
-	// Don't try to enable nftables if firewalld is running.
18
-	if iptables.UsingFirewalld() {
19
-		return
16
+func (c *Controller) selectFirewallBackend() error {
17
+	// If explicitly configured to use iptables, don't consider nftables.
18
+	if c.cfg.FirewallBackend == "iptables" {
19
+		return nil
20 20
 	}
21
-	// Only try to use nftables if explicitly enabled by env-var.
22
-	// TODO(robmry) - command line options?
23
-	if os.Getenv("DOCKER_FIREWALL_BACKEND") == "nftables" {
24
-		_ = nftables.Enable()
21
+	// If configured to use nftables, but it can't be initialised, return an error.
22
+	if c.cfg.FirewallBackend == "nftables" {
23
+		// Don't try to enable nftables if firewalld is running.
24
+		if iptables.UsingFirewalld() {
25
+			return errors.New("firewall-backend is set to nftables, but firewalld is running")
26
+		}
27
+		if err := nftables.Enable(); err != nil {
28
+			return fmt.Errorf("firewall-backend is set to nftables: %v", err)
29
+		}
30
+		return nil
25 31
 	}
32
+	return nil
26 33
 }
27 34
 
28 35
 // Sets up the DOCKER-USER chain for each iptables version (IPv4, IPv6) that's
... ...
@@ -2,6 +2,8 @@
2 2
 
3 3
 package libnetwork
4 4
 
5
-func (c *Controller) selectFirewallBackend() {}
5
+func (c *Controller) selectFirewallBackend() error {
6
+	return nil
7
+}
6 8
 
7 9
 func (c *Controller) setupUserChains() {}
... ...
@@ -62,6 +62,8 @@ var (
62 62
 	// nftPath is the path of the "nft" tool, set by [Enable] and left empty if the tool
63 63
 	// is not present - in which case, nftables is disabled.
64 64
 	nftPath string
65
+	// Error returned by Enable if nftables could not be initialised.
66
+	nftEnableError error
65 67
 	// incrementalUpdateTempl is a parsed text/template, used to apply incremental updates.
66 68
 	incrementalUpdateTempl *template.Template
67 69
 	// reloadTempl is a parsed text/template, used to apply a whole table.
... ...
@@ -134,21 +136,23 @@ const (
134 134
 	nftTypeIfname      nftType = "ifname"
135 135
 )
136 136
 
137
-// Enable checks whether the "nft" tool is available, and returns true if it is.
138
-// Subsequent calls to [Enabled] will return the same result.
139
-func Enable() bool {
137
+// Enable tries once to initialise nftables.
138
+func Enable() error {
140 139
 	enableOnce.Do(func() {
141 140
 		path, err := exec.LookPath("nft")
142 141
 		if err != nil {
143 142
 			log.G(context.Background()).WithError(err).Warnf("Failed to find nft tool")
143
+			nftEnableError = fmt.Errorf("failed to find nft tool: %w", err)
144
+			return
144 145
 		}
145 146
 		if err := parseTemplate(); err != nil {
146 147
 			log.G(context.Background()).WithError(err).Error("Internal error while initialising nftables")
148
+			nftEnableError = fmt.Errorf("internal error while initialising nftables: %w", err)
147 149
 			return
148 150
 		}
149 151
 		nftPath = path
150 152
 	})
151
-	return nftPath != ""
153
+	return nftEnableError
152 154
 }
153 155
 
154 156
 // Enabled returns true if the "nft" tool is available and [Enable] has been called.
... ...
@@ -14,7 +14,7 @@ import (
14 14
 
15 15
 func testSetup(t *testing.T) func() {
16 16
 	t.Helper()
17
-	if !Enable() {
17
+	if err := Enable(); err != nil {
18 18
 		// Make sure it didn't fail because of a bug in the text/template.
19 19
 		assert.NilError(t, parseTemplate())
20 20
 		// If this is not CI, skip.
... ...
@@ -22,7 +22,7 @@ func testSetup(t *testing.T) func() {
22 22
 			t.Skip("Cannot enable nftables, no 'nft' command in $PATH ?")
23 23
 		}
24 24
 		// In CI, nft should always be installed, fail the test.
25
-		t.Fatal("Failed to enable nftables")
25
+		t.Fatalf("Failed to enable nftables: %s", err)
26 26
 	}
27 27
 	cleanupContext := netnsutils.SetupTestOSContext(t)
28 28
 	return func() {
... ...
@@ -118,6 +118,7 @@ if [ -z "$DOCKER_TEST_HOST" ]; then
118 118
 			--storage-driver "$DOCKER_GRAPHDRIVER" \
119 119
 			--pidfile "$DEST/docker.pid" \
120 120
 			--userland-proxy="$DOCKER_USERLANDPROXY" \
121
+			--firewall-backend="$DOCKER_FIREWALL_BACKEND" \
121 122
 			${storage_params} \
122 123
 			${extra_params} \
123 124
 			&> "$DEST/docker.log"
... ...
@@ -58,6 +58,7 @@ args=(
58 58
 	--host="unix://${socket}"
59 59
 	--storage-driver="${DOCKER_GRAPHDRIVER}"
60 60
 	--userland-proxy="${DOCKER_USERLANDPROXY}"
61
+	--firewall-backend="${DOCKER_FIREWALL_BACKEND}"
61 62
 	--tls=false
62 63
 	$storage_params
63 64
 	$extra_params
... ...
@@ -47,6 +47,7 @@ test_env() {
47 47
 			DOCKER_CERT_PATH="$DOCKER_TEST_CERT_PATH" \
48 48
 			DOCKER_GRAPHDRIVER="$DOCKER_GRAPHDRIVER" \
49 49
 			DOCKER_USERLANDPROXY="$DOCKER_USERLANDPROXY" \
50
+			DOCKER_FIREWALL_BACKEND="$DOCKER_FIREWALL_BACKEND" \
50 51
 			DOCKER_HOST="$DOCKER_HOST" \
51 52
 			DOCKER_REMAP_ROOT="$DOCKER_REMAP_ROOT" \
52 53
 			DOCKER_REMOTE_DAEMON="$DOCKER_REMOTE_DAEMON" \
... ...
@@ -856,9 +856,8 @@ func TestFirewallBackendSwitch(t *testing.T) {
856 856
 
857 857
 	networkCreated := false
858 858
 	runDaemon := func(backend string) {
859
-		d.SetEnvVar("DOCKER_FIREWALL_BACKEND", backend)
860 859
 		host.Do(t, func() {
861
-			d.StartWithBusybox(ctx, t)
860
+			d.StartWithBusybox(ctx, t, "--firewall-backend="+backend)
862 861
 			defer d.Stop(t)
863 862
 
864 863
 			// Create a network (and its firewall rules) first time through.
... ...
@@ -234,10 +234,6 @@ func New(t testing.TB, ops ...Option) *Daemon {
234 234
 	}
235 235
 	ops = append(ops, WithOOMScoreAdjust(-500))
236 236
 
237
-	if val, ok := os.LookupEnv("DOCKER_FIREWALL_BACKEND"); ok {
238
-		ops = append(ops, WithEnvVars("DOCKER_FIREWALL_BACKEND="+val))
239
-	}
240
-
241 237
 	d, err := NewDaemon(dest, ops...)
242 238
 	assert.NilError(t, err, "could not create daemon at %q", dest)
243 239
 	if d.rootlessUser != nil && d.dockerdBinary != defaultDockerdBinary {
... ...
@@ -534,6 +530,15 @@ func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error {
534 534
 		d.args = append(d.args, "--storage-driver", d.storageDriver)
535 535
 	}
536 536
 
537
+	hasFwBackendArg := !slices.ContainsFunc(providedArgs, func(s string) bool {
538
+		return strings.HasPrefix(s, "--firewall-backend")
539
+	})
540
+	if hasFwBackendArg {
541
+		if fw := os.Getenv("DOCKER_FIREWALL_BACKEND"); fw != "" {
542
+			d.args = append(d.args, "--firewall-backend="+fw)
543
+		}
544
+	}
545
+
537 546
 	d.args = append(d.args, providedArgs...)
538 547
 	cmd := exec.Command(dockerdBinary, d.args...)
539 548
 	cmd.Env = append(os.Environ(), "DOCKER_SERVICE_PREFER_OFFLINE_IMAGE=1")