Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions daemon/command/config_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
flags.BoolVar(&conf.BridgeConfig.DisableFilterForwardDrop, "ip-forward-no-drop", false, "Do not set the filter-FORWARD policy to DROP when enabling IP forwarding")
flags.BoolVar(&conf.BridgeConfig.EnableIPMasq, "ip-masq", true, "Enable IP masquerading for the default bridge network")
flags.BoolVar(&conf.BridgeConfig.EnableIPv6, "ipv6", false, "Enable IPv6 networking for the default bridge network")
flags.Var(opts.NewNamedMapOpts("bridge-nftables-priorities", conf.NftablesPriorities, nil), "bridge-nftables-priority", "Base chain priorities for bridge driver nftables")
flags.StringVar(&conf.BridgeConfig.IP, "bip", "", "IPv4 address for the default bridge")
flags.StringVar(&conf.BridgeConfig.IP6, "bip6", "", "IPv6 address for the default bridge")
flags.StringVarP(&conf.BridgeConfig.Iface, "bridge", "b", "", "Attach containers to a network bridge")
Expand Down
15 changes: 8 additions & 7 deletions daemon/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ const (
// Use this to differentiate these options
// with others like the ones in TLSOptions.
var flatOptions = map[string]bool{
"cluster-store-opts": true,
"default-network-opts": true,
"log-opts": true,
"runtimes": true,
"default-ulimits": true,
"features": true,
"builder": true,
"cluster-store-opts": true,
"default-network-opts": true,
"bridge-nftables-priorities": true,
"log-opts": true,
"runtimes": true,
"default-ulimits": true,
"features": true,
"builder": true,
}

// skipValidateOptions contains configuration keys
Expand Down
22 changes: 14 additions & 8 deletions daemon/config/config_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ const (
type BridgeConfig struct {
DefaultBridgeConfig

EnableIPTables bool `json:"iptables,omitempty"`
EnableIP6Tables bool `json:"ip6tables,omitempty"`
EnableIPForward bool `json:"ip-forward,omitempty"`
DisableFilterForwardDrop bool `json:"ip-forward-no-drop,omitempty"`
EnableIPMasq bool `json:"ip-masq,omitempty"`
EnableUserlandProxy bool `json:"userland-proxy,omitempty"`
UserlandProxyPath string `json:"userland-proxy-path,omitempty"`
AllowDirectRouting bool `json:"allow-direct-routing,omitempty"`
EnableIPTables bool `json:"iptables,omitempty"`
EnableIP6Tables bool `json:"ip6tables,omitempty"`
EnableIPForward bool `json:"ip-forward,omitempty"`
DisableFilterForwardDrop bool `json:"ip-forward-no-drop,omitempty"`
EnableIPMasq bool `json:"ip-masq,omitempty"`
EnableUserlandProxy bool `json:"userland-proxy,omitempty"`
UserlandProxyPath string `json:"userland-proxy-path,omitempty"`
AllowDirectRouting bool `json:"allow-direct-routing,omitempty"`
NftablesPriorities map[string]string `json:"bridge-nftables-priorities,omitempty"`
}

// DefaultBridgeConfig stores all the parameters for the default bridge network.
Expand Down Expand Up @@ -147,6 +148,7 @@ func setPlatformDefaults(cfg *Config) error {
cfg.SeccompProfile = SeccompProfileDefault
cfg.IpcMode = string(DefaultIpcMode)
cfg.Runtimes = make(map[string]system.Runtime)
cfg.NftablesPriorities = make(map[string]string)

if cgroups.Mode() != cgroups.Unified {
cfg.CgroupNamespaceMode = string(DefaultCgroupV1NamespaceMode)
Expand Down Expand Up @@ -243,6 +245,10 @@ func validatePlatformConfig(conf *Config) error {
return errors.Wrap(err, "invalid fixed-cidr-v6")
}

if err := bridge.ValidateBaseChainPriorities(conf.NftablesPriorities); err != nil {
return err
}

if err := validateFirewallBackend(conf.FirewallBackend); err != nil {
return errors.Wrap(err, "invalid firewall-backend")
}
Expand Down
1 change: 1 addition & 0 deletions daemon/daemon_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ func networkPlatformOptions(conf *config.Config) []nwconfig.Option {
"EnableIP6Tables": conf.BridgeConfig.EnableIP6Tables,
"Hairpin": !conf.EnableUserlandProxy || conf.UserlandProxyPath == "",
"AllowDirectRouting": conf.BridgeConfig.AllowDirectRouting,
"NftablesPriorities": conf.NftablesPriorities,
},
}),
}
Expand Down
35 changes: 32 additions & 3 deletions daemon/libnetwork/drivers/bridge/bridge_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net"
"net/netip"
"os"
"os/signal"
"slices"
"strconv"
"strings"
Expand Down Expand Up @@ -43,6 +44,7 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sys/unix"
)

const (
Expand Down Expand Up @@ -77,6 +79,7 @@ type configuration struct {
// hairpinned.
Hairpin bool
AllowDirectRouting bool
NftablesPriorities map[string]string
}

// networkConfiguration for network specific configuration
Expand Down Expand Up @@ -528,7 +531,7 @@ func (d *driver) configure(option map[string]interface{}) error {
Hairpin: config.Hairpin,
AllowDirectRouting: config.AllowDirectRouting,
WSL2Mirrored: isRunningUnderWSL2MirroredMode(context.Background()),
})
}, config.NftablesPriorities)
if err != nil {
return err
}
Expand All @@ -542,13 +545,19 @@ func (d *driver) configure(option map[string]interface{}) error {
d.configNetwork.Lock()
defer d.configNetwork.Unlock()
iptables.OnReloaded(d.handleFirewalldReload)
d.startFirewallReloader()

return d.initStore()
}

var newFirewaller = func(ctx context.Context, config firewaller.Config) (firewaller.Firewaller, error) {
// ValidateBaseChainPriorities checks nftables base chain priority configuration.
func ValidateBaseChainPriorities(prios map[string]string) error {
return nftabler.ValidateBaseChainPriorities(prios)
}

var newFirewaller = func(ctx context.Context, config firewaller.Config, nftablesPriorities map[string]string) (firewaller.Firewaller, error) {
if nftables.Enabled() {
fw, err := nftabler.NewNftabler(ctx, config)
fw, err := nftabler.NewNftabler(ctx, config, nftablesPriorities)
if err != nil {
return nil, err
}
Expand All @@ -566,6 +575,26 @@ var newFirewaller = func(ctx context.Context, config firewaller.Config) (firewal
return iptabler.NewIptabler(ctx, config)
}

func (d *driver) startFirewallReloader() {
r, ok := d.firewaller.(firewaller.Reloader)
if !ok {
return
}

hupC := make(chan os.Signal, 1)
signal.Notify(hupC, unix.SIGHUP)
go func() {
for range hupC {
d.configNetwork.Lock()
log.G(context.Background()).Info("Received SIGHUP, reloading firewall rules")
if err := r.Reload(context.Background()); err != nil {
log.G(context.Background()).Errorf("Failed to reload firewall rules: %v", err)
}
d.configNetwork.Unlock()
}
}()
}

func (d *driver) getNetwork(id string) (*bridgeNetwork, error) {
d.Lock()
defer d.Unlock()
Expand Down
2 changes: 1 addition & 1 deletion daemon/libnetwork/drivers/bridge/bridge_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1337,7 +1337,7 @@ func TestCreateParallel(t *testing.T) {

func useStubFirewaller(t *testing.T) {
origNewFirewaller := newFirewaller
newFirewaller = func(_ context.Context, config firewaller.Config) (firewaller.Firewaller, error) {
newFirewaller = func(_ context.Context, config firewaller.Config, _ map[string]string) (firewaller.Firewaller, error) {
return firewaller.NewStubFirewaller(config), nil
}
t.Cleanup(func() { newFirewaller = origNewFirewaller })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ type Network interface {
DelLink(ctx context.Context, parentIP, childIP netip.Addr, ports []types.TransportPort)
}

// Reloader is an optional interface for a Firewaller.
type Reloader interface {
// Reload the current firewall rules. The caller is responsible for locking,
// there must not be any concurrent requests to modify rules.
Reload(ctx context.Context) error
}

// FirewallCleanerSetter is an optional interface for a Firewaller.
type FirewallCleanerSetter interface {
// SetFirewallCleaner replaces the FirewallCleaner (possibly with 'nil').
Expand Down
38 changes: 24 additions & 14 deletions daemon/libnetwork/drivers/bridge/internal/nftabler/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,24 @@ func (n *network) DelEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr) er

func (n *network) modEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr, enable bool) error {
if n.fw.config.IPv4 && epIPv4.IsValid() {
if err := n.filterDirectAccess(ctx, n.fw.table4, n.config.Config4, epIPv4, enable); err != nil {
return err
tm := n.fw.table4.Modifier()
updater := tm.Create
if !enable {
updater = tm.Delete
}
if err := nftApply(ctx, n.fw.table4); err != nil {
n.filterDirectAccess(updater, tm.Family(), n.config.Config4, epIPv4)
if err := tm.Apply(ctx); err != nil {
return fmt.Errorf("adding rules for bridge %s: %w", n.config.IfName, err)
}
}
if n.fw.config.IPv6 && epIPv6.IsValid() {
if err := n.filterDirectAccess(ctx, n.fw.table6, n.config.Config6, epIPv6, enable); err != nil {
return err
tm := n.fw.table6.Modifier()
updater := tm.Create
if !enable {
updater = tm.Delete
}
if err := nftApply(ctx, n.fw.table6); err != nil {
n.filterDirectAccess(updater, tm.Family(), n.config.Config6, epIPv6)
if err := tm.Apply(ctx); err != nil {
return fmt.Errorf("adding rules for bridge %s: %w", n.config.IfName, err)
}
}
Expand All @@ -53,17 +59,21 @@ func (n *network) modEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr, en
// kernel support).
//
// Packets originating on the bridge's own interface and addressed directly to the
// container are allowed - the host always has direct access to its own containers
// (it doesn't need to use the port mapped to its own addresses, although it can).
// container are allowed - the host always has direct access to its own containers.
// (It doesn't need to use the port mapped to its own addresses, although it can.)
//
// "Trusted interfaces" are treated in the same way as the bridge itself.
func (n *network) filterDirectAccess(ctx context.Context, table nftables.TableRef, conf firewaller.NetworkConfigFam, epIP netip.Addr, enable bool) error {
func (n *network) filterDirectAccess(updater func(nftables.Obj), fam nftables.Family, conf firewaller.NetworkConfigFam, epIP netip.Addr) {
if n.config.Internal || conf.Unprotected || conf.Routed || n.fw.config.AllowDirectRouting {
return nil
return
}
updater := table.ChainUpdateFunc(ctx, rawPreroutingChain, enable)
ifNames := strings.Join(n.config.TrustedHostInterfaces, ", ")
return updater(ctx, rawPreroutingPortsRuleGroup,
`%s daddr %s iifname != { %s, %s } counter drop comment "DROP DIRECT ACCESS"`,
table.Family(), epIP, n.config.IfName, ifNames)
updater(nftables.Rule{
Chain: rawPreroutingChain,
Group: rawPreroutingPortsRuleGroup,
Rule: []string{
string(fam), "daddr", epIP.String(),
"iifname != {", n.config.IfName, ",", ifNames, `} counter drop comment "DROP DIRECT ACCESS"`,
},
})
}
56 changes: 31 additions & 25 deletions daemon/libnetwork/drivers/bridge/internal/nftabler/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"errors"
"fmt"
"net/netip"
"strconv"

"github.com/docker/docker/daemon/libnetwork/internal/nftables"

"github.com/containerd/log"
"github.com/docker/docker/daemon/libnetwork/types"
Expand All @@ -20,44 +23,47 @@ func (n *network) AddLink(ctx context.Context, parentIP, childIP netip.Addr, por
return errors.New("cannot link to a container with an empty child IP address")
}

chain := n.fw.table4.Chain(ctx, chainFilterFwdIn(n.config.IfName))
tm := n.fw.table4.Modifier()
for _, port := range ports {
for _, rule := range legacyLinkRules(parentIP, childIP, port) {
if err := chain.AppendRule(ctx, fwdInLegacyLinksRuleGroup, rule); err != nil {
return err
}
}
updateLegacyLinkRules(tm.Create, chainFilterFwdIn(n.config.IfName), parentIP, childIP, port)
}
if err := nftApply(ctx, n.fw.table4); err != nil {
if err := tm.Apply(ctx); err != nil {
return fmt.Errorf("adding rules for bridge %s: %w", n.config.IfName, err)
}
return nil
}

func (n *network) DelLink(ctx context.Context, parentIP, childIP netip.Addr, ports []types.TransportPort) {
chain := n.fw.table4.Chain(ctx, chainFilterFwdIn(n.config.IfName))
tm := n.fw.table4.Modifier()
for _, port := range ports {
for _, rule := range legacyLinkRules(parentIP, childIP, port) {
if err := chain.DeleteRule(ctx, fwdInLegacyLinksRuleGroup, rule); err != nil {
log.G(ctx).WithFields(log.Fields{
"rule": rule,
"error": err,
}).Warn("Failed to remove link between containers")
}
}
updateLegacyLinkRules(tm.Delete, chainFilterFwdIn(n.config.IfName), parentIP, childIP, port)
}
if err := nftApply(ctx, n.fw.table4); err != nil {
if err := tm.Apply(ctx); err != nil {
log.G(ctx).WithError(err).Warn("Removing link, failed to update nftables")
}
}

func legacyLinkRules(parentIP, childIP netip.Addr, port types.TransportPort) []string {
func updateLegacyLinkRules(updater func(command nftables.Obj), chainName string, parentIP, childIP netip.Addr, port types.TransportPort) {
// TODO(robmry) - could combine rules for each proto by using an anonymous set.
return []string{
// Match the iptables implementation, but without checking iifname/oifname (not needed
// because the addresses belong to the bridge).
fmt.Sprintf("ip saddr %s ip daddr %s %s dport %d counter accept", parentIP.Unmap(), childIP.Unmap(), port.Proto, port.Port),
// Conntrack will allow responses. So, this must be to allow unsolicited packets from an exposed port.
fmt.Sprintf("ip daddr %s ip saddr %s %s sport %d counter accept", parentIP.Unmap(), childIP.Unmap(), port.Proto, port.Port),
}
// Match the iptables implementation, but without checking iifname/oifname (not needed
// because the addresses belong to the bridge).
updater(nftables.Rule{
Chain: chainName,
Group: fwdInLegacyLinksRuleGroup,
Rule: []string{
"ip saddr", parentIP.Unmap().String(),
"ip daddr", childIP.Unmap().String(), port.Proto.String(), "dport", strconv.Itoa(int(port.Port)),
"counter accept",
},
})
// Conntrack will allow responses. So, this must be to allow unsolicited packets from an exposed port.
updater(nftables.Rule{
Chain: chainName,
Group: fwdInLegacyLinksRuleGroup,
Rule: []string{
"ip daddr", parentIP.Unmap().String(),
"ip saddr", childIP.Unmap().String(), port.Proto.String(), "sport", strconv.Itoa(int(port.Port)),
"counter accept",
},
})
}
Loading
Loading