Skip to content

Commit cf1695b

Browse files
committed
Add option --bridge-accept-fwmark
Packets with the given firewall mark are accepted by the bridge driver's filter-FORWARD rules. The value can either be an integer mark, or it can include a mask in the format "<mark>/<mask>". Signed-off-by: Rob Murray <rob.murray@docker.com>
1 parent 0c60a0e commit cf1695b

File tree

11 files changed

+291
-4
lines changed

11 files changed

+291
-4
lines changed

daemon/command/config_unix.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
3939
flags.BoolVar(&conf.BridgeConfig.EnableUserlandProxy, "userland-proxy", true, "Use userland proxy for loopback traffic")
4040
flags.StringVar(&conf.BridgeConfig.UserlandProxyPath, "userland-proxy-path", conf.BridgeConfig.UserlandProxyPath, "Path to the userland proxy binary")
4141
flags.BoolVar(&conf.BridgeConfig.AllowDirectRouting, "allow-direct-routing", false, "Allow remote access to published ports on container IP addresses")
42+
flags.StringVar(&conf.BridgeConfig.BridgeAcceptFwMark, "bridge-accept-fwmark", "", "In bridge networks, accept packets with this firewall mark/mask")
4243
flags.StringVar(&conf.CgroupParent, "cgroup-parent", "", "Set parent cgroup for all containers")
4344
flags.StringVar(&conf.RemappedRoot, "userns-remap", "", "User/Group setting for user namespaces")
4445
flags.BoolVar(&conf.LiveRestoreEnabled, "live-restore", false, "Enable live restore of docker when containers are still running")

daemon/config/config_linux.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net"
77
"os/exec"
88
"path/filepath"
9+
"strconv"
910
"strings"
1011

1112
"github.com/containerd/cgroups/v3"
@@ -49,6 +50,7 @@ type BridgeConfig struct {
4950
EnableUserlandProxy bool `json:"userland-proxy,omitempty"`
5051
UserlandProxyPath string `json:"userland-proxy-path,omitempty"`
5152
AllowDirectRouting bool `json:"allow-direct-routing,omitempty"`
53+
BridgeAcceptFwMark string `json:"bridge-accept-fwmark,omitempty"`
5254
}
5355

5456
// DefaultBridgeConfig stores all the parameters for the default bridge network.
@@ -243,15 +245,15 @@ func validatePlatformConfig(conf *Config) error {
243245
if err := verifyDefaultIpcMode(conf.IpcMode); err != nil {
244246
return err
245247
}
246-
247248
if err := bridge.ValidateFixedCIDRV6(conf.FixedCIDRv6); err != nil {
248249
return errors.Wrap(err, "invalid fixed-cidr-v6")
249250
}
250-
251251
if err := validateFirewallBackend(conf.FirewallBackend); err != nil {
252252
return errors.Wrap(err, "invalid firewall-backend")
253253
}
254-
254+
if err := validateFwMarkMask(conf.BridgeAcceptFwMark); err != nil {
255+
return errors.Wrap(err, "invalid bridge-accept-fwmark")
256+
}
255257
return verifyDefaultCgroupNsMode(conf.CgroupNamespaceMode)
256258
}
257259

@@ -311,6 +313,22 @@ func validateFirewallBackend(val string) error {
311313
return errors.New(`allowed values are "iptables" and "nftables"`)
312314
}
313315

316+
func validateFwMarkMask(val string) error {
317+
if val == "" {
318+
return nil
319+
}
320+
mark, mask, haveMask := strings.Cut(val, "/")
321+
if _, err := strconv.ParseUint(mark, 0, 32); err != nil {
322+
return fmt.Errorf("invalid firewall mark %q: %w", val, err)
323+
}
324+
if haveMask {
325+
if _, err := strconv.ParseUint(mask, 0, 32); err != nil {
326+
return fmt.Errorf("invalid firewall mask %q: %w", val, err)
327+
}
328+
}
329+
return nil
330+
}
331+
314332
func verifyDefaultCgroupNsMode(mode string) error {
315333
cm := container.CgroupnsMode(mode)
316334
if !cm.Valid() {

daemon/config/config_linux_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,71 @@ func TestDaemonLegacyOptions(t *testing.T) {
396396
})
397397
}
398398
}
399+
400+
func TestValidateAcceptFwMarkMark(t *testing.T) {
401+
tests := []struct {
402+
name string
403+
val string
404+
expErr string
405+
}{
406+
{
407+
name: "empty",
408+
val: "",
409+
},
410+
{
411+
name: "dec/no-mask",
412+
val: "1",
413+
},
414+
{
415+
name: "hex/no-mask",
416+
val: "0x1",
417+
},
418+
{
419+
name: "dec/mask",
420+
val: "1/2",
421+
},
422+
{
423+
name: "hex/mask",
424+
val: "0x1/0x2",
425+
},
426+
{
427+
name: "octal/mask",
428+
val: "010/0xff",
429+
},
430+
{
431+
name: "bad/mark",
432+
val: "hello/0x2",
433+
expErr: `invalid firewall mark "hello/0x2": strconv.ParseUint: parsing "hello": invalid syntax`,
434+
},
435+
{
436+
name: "bad/mark",
437+
val: "1/hello",
438+
expErr: `invalid firewall mask "1/hello": strconv.ParseUint: parsing "hello": invalid syntax`,
439+
},
440+
{
441+
name: "bad/sep",
442+
val: "1+hello",
443+
expErr: `invalid firewall mark "1+hello": strconv.ParseUint: parsing "1+hello": invalid syntax`,
444+
},
445+
{
446+
name: "bad/no-mask",
447+
val: "1/",
448+
expErr: `invalid firewall mask "1/": strconv.ParseUint: parsing "": invalid syntax`,
449+
},
450+
{
451+
name: "bad/negative",
452+
val: "-1",
453+
expErr: `invalid firewall mark "-1": strconv.ParseUint: parsing "-1": invalid syntax`,
454+
},
455+
}
456+
for _, tc := range tests {
457+
t.Run(tc.name, func(t *testing.T) {
458+
err := validateFwMarkMask(tc.val)
459+
if tc.expErr == "" {
460+
assert.NilError(t, err)
461+
} else {
462+
assert.Check(t, is.ErrorContains(err, tc.expErr))
463+
}
464+
})
465+
}
466+
}

daemon/daemon_unix.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,7 @@ func networkPlatformOptions(conf *config.Config) []nwconfig.Option {
938938
"EnableIP6Tables": conf.BridgeConfig.EnableIP6Tables,
939939
"Hairpin": !conf.EnableUserlandProxy || conf.UserlandProxyPath == "",
940940
"AllowDirectRouting": conf.BridgeConfig.AllowDirectRouting,
941+
"AcceptFwMark": conf.BridgeConfig.BridgeAcceptFwMark,
941942
},
942943
}),
943944
}

daemon/libnetwork/drivers/bridge/bridge_linux.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ type configuration struct {
7777
// hairpinned.
7878
Hairpin bool
7979
AllowDirectRouting bool
80+
AcceptFwMark string
8081
}
8182

8283
// networkConfiguration for network specific configuration
@@ -429,6 +430,7 @@ func (n *bridgeNetwork) newFirewallerNetwork(ctx context.Context) (_ firewaller.
429430
ICC: n.config.EnableICC,
430431
Masquerade: n.config.EnableIPMasquerade,
431432
TrustedHostInterfaces: n.config.TrustedHostInterfaces,
433+
AcceptFwMark: n.driver.config.AcceptFwMark,
432434
Config4: config4,
433435
Config6: config6,
434436
})

daemon/libnetwork/drivers/bridge/internal/firewaller/firewaller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ type NetworkConfig struct {
4848
// bridge itself). In particular, these are not external interfaces for the purpose of
4949
// blocking direct-routing to a container's IP address.
5050
TrustedHostInterfaces []string
51+
// AcceptFwMark is a firewall mark/mask. Packets with this mark will not be dropped by
52+
// per-port blocking rules. So, packets with this mark have access to unpublished
53+
// container ports.
54+
AcceptFwMark string
5155
// Config4 contains IPv4-specific configuration for the network.
5256
Config4 NetworkConfigFam
5357
// Config6 contains IPv6-specific configuration for the network.

daemon/libnetwork/drivers/bridge/internal/iptabler/network.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"errors"
88
"fmt"
99
"net/netip"
10+
"strconv"
11+
"strings"
1012

1113
"github.com/containerd/log"
1214
"github.com/docker/docker/daemon/libnetwork/drivers/bridge/internal/firewaller"
@@ -263,6 +265,18 @@ func setDefaultForwardRule(ipVersion iptables.IPVersion, ifName string, unprotec
263265
}
264266

265267
func (n *network) setupNonInternalNetworkRules(ctx context.Context, ipVer iptables.IPVersion, config firewaller.NetworkConfigFam, enable bool) error {
268+
if n.config.AcceptFwMark != "" {
269+
fwm, err := iptablesFwMark(n.config.AcceptFwMark)
270+
if err != nil {
271+
return err
272+
}
273+
if err := programChainRule(iptables.Rule{IPVer: ipVer, Table: iptables.Filter, Chain: DockerForwardChain, Args: []string{
274+
"-m", "mark", "--mark", fwm, "-j", "ACCEPT",
275+
}}, "ALLOW FW MARK", enable); err != nil {
276+
return err
277+
}
278+
}
279+
266280
var natArgs, hpNatArgs []string
267281
if config.HostIP.IsValid() {
268282
// The user wants IPv4/IPv6 SNAT with the given address.
@@ -459,3 +473,23 @@ func setupInternalNetworkRules(ctx context.Context, bridgeIface string, prefix n
459473
// Set Inter Container Communication.
460474
return setIcc(ctx, version, bridgeIface, icc, true, insert)
461475
}
476+
477+
// iptablesFwMark takes a string representing a firewall mark with an optional
478+
// "/mask" parses the mark and mask, and returns the same "mark/mask" with the
479+
// numbers converted to decimal, because strings.ParseUint accepts more integer
480+
// formats than iptables.
481+
func iptablesFwMark(val string) (string, error) {
482+
markStr, maskStr, haveMask := strings.Cut(val, "/")
483+
mark, err := strconv.ParseUint(markStr, 0, 32)
484+
if err != nil {
485+
return "", fmt.Errorf("invalid firewall mark %q: %w", val, err)
486+
}
487+
if haveMask {
488+
mask, err := strconv.ParseUint(maskStr, 0, 32)
489+
if err != nil {
490+
return "", fmt.Errorf("invalid firewall mask %q: %w", val, err)
491+
}
492+
return fmt.Sprintf("%d/%d", mark, mask), nil
493+
}
494+
return strconv.FormatUint(mark, 10), nil
495+
}

daemon/libnetwork/drivers/bridge/internal/nftabler/network.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package nftabler
55
import (
66
"context"
77
"fmt"
8+
"strconv"
9+
"strings"
810

911
"github.com/containerd/log"
1012
"github.com/docker/docker/daemon/libnetwork/drivers/bridge/internal/firewaller"
@@ -157,6 +159,20 @@ func (n *network) configure(ctx context.Context, table nftables.TableRef, conf f
157159
}
158160
cleanup.Add(cf)
159161
} else {
162+
// AcceptFwMark
163+
if n.config.AcceptFwMark != "" {
164+
fwm, err := nftFwMark(n.config.AcceptFwMark)
165+
if err != nil {
166+
return nil, fmt.Errorf("adding fwmark %q for %q: %w", n.config.AcceptFwMark, n.config.IfName, err)
167+
}
168+
cf, err = fwdInChain.AppendRuleCf(ctx, fwdInAcceptFwMarkRuleGroup,
169+
`meta mark %s counter accept comment "ALLOW FW MARK"`, fwm)
170+
if err != nil {
171+
return nil, fmt.Errorf("adding ALLOW FW MARK rule for %q: %w", n.config.IfName, err)
172+
}
173+
cleanup.Add(cf)
174+
}
175+
160176
// Inter-Container Communication
161177
cf, err = fwdInChain.AppendRuleCf(ctx, fwdInICCRuleGroup, "iifname == %s counter %s comment ICC",
162178
n.config.IfName, iccVerdict)
@@ -270,3 +286,23 @@ func chainNatPostRtOut(ifName string) string {
270286
func chainNatPostRtIn(ifName string) string {
271287
return "nat-postrouting-in__" + ifName
272288
}
289+
290+
// nftFwMark takes a string representing a firewall mark with an optional
291+
// "/mask", parses the mark and mask, and returns an nftables expression
292+
// representing the same mask/mark. Numbers are converted to decimal, because
293+
// strings.ParseUint accepts more integer formats than nft.
294+
func nftFwMark(val string) (string, error) {
295+
markStr, maskStr, haveMask := strings.Cut(val, "/")
296+
mark, err := strconv.ParseUint(markStr, 0, 32)
297+
if err != nil {
298+
return "", fmt.Errorf("invalid firewall mark %q: %w", val, err)
299+
}
300+
if haveMask {
301+
mask, err := strconv.ParseUint(maskStr, 0, 32)
302+
if err != nil {
303+
return "", fmt.Errorf("invalid firewall mask %q: %w", val, err)
304+
}
305+
return fmt.Sprintf("and %d == %d", mask, mark), nil
306+
}
307+
return strconv.FormatUint(mark, 10), nil
308+
}

daemon/libnetwork/drivers/bridge/internal/nftabler/nftabler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const (
3434
)
3535

3636
const (
37-
fwdInLegacyLinksRuleGroup = iota + initialRuleGroup + 1
37+
fwdInAcceptFwMarkRuleGroup = iota + initialRuleGroup + 1
38+
fwdInLegacyLinksRuleGroup
3839
fwdInICCRuleGroup
3940
fwdInPortsRuleGroup
4041
fwdInFinalRuleGroup

0 commit comments

Comments
 (0)