From 808120e5b84ad37c91e5030bb0e524e5ee89712f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 11 Sep 2023 19:32:02 -0400 Subject: [PATCH] New `host_ipv6` bridge option to SNAT IPv6 connections Add a new `com.docker.network.host_ipv6` bridge option to compliment the existing `com.docker.network.host_ipv4` option. When set to an IPv6 address, this causes the bridge to insert `SNAT` rules instead of `MASQUERADE` rules (assuming `ip6tables` is enabled). `SNAT` makes it possible for users to control the source IP address used for outgoing connections. Signed-off-by: Richard Hansen --- libnetwork/drivers/bridge/bridge_linux.go | 5 ++++ libnetwork/drivers/bridge/bridge_store.go | 4 +++ .../drivers/bridge/setup_ip_tables_linux.go | 10 +++++-- .../bridge/setup_ip_tables_linux_test.go | 28 +++++++++++++++++++ libnetwork/netlabel/labels.go | 3 ++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go index 47ac622320..f438e5480c 100644 --- a/libnetwork/drivers/bridge/bridge_linux.go +++ b/libnetwork/drivers/bridge/bridge_linux.go @@ -74,6 +74,7 @@ type networkConfiguration struct { DefaultBindingIP net.IP DefaultBridge bool HostIPv4 net.IP + HostIPv6 net.IP ContainerIfacePrefix string // Internal fields set after ipam data parsing AddressIPv4 *net.IPNet @@ -270,6 +271,10 @@ func (c *networkConfiguration) fromLabels(labels map[string]string) error { if c.HostIPv4 = net.ParseIP(value); c.HostIPv4 == nil { return parseErr(label, value, "nil ip") } + case netlabel.HostIPv6: + if c.HostIPv6 = net.ParseIP(value); c.HostIPv6 == nil { + return parseErr(label, value, "nil ip") + } } } diff --git a/libnetwork/drivers/bridge/bridge_store.go b/libnetwork/drivers/bridge/bridge_store.go index ad6845f72c..66928aba24 100644 --- a/libnetwork/drivers/bridge/bridge_store.go +++ b/libnetwork/drivers/bridge/bridge_store.go @@ -148,6 +148,7 @@ func (ncfg *networkConfiguration) MarshalJSON() ([]byte, error) { nMap["DefaultBindingIP"] = ncfg.DefaultBindingIP.String() // This key is "HostIP" instead of "HostIPv4" to preserve compatibility with the on-disk format. nMap["HostIP"] = ncfg.HostIPv4.String() + nMap["HostIPv6"] = ncfg.HostIPv6.String() nMap["DefaultGatewayIPv4"] = ncfg.DefaultGatewayIPv4.String() nMap["DefaultGatewayIPv6"] = ncfg.DefaultGatewayIPv6.String() nMap["ContainerIfacePrefix"] = ncfg.ContainerIfacePrefix @@ -194,6 +195,9 @@ func (ncfg *networkConfiguration) UnmarshalJSON(b []byte) error { if v, ok := nMap["HostIP"]; ok { ncfg.HostIPv4 = net.ParseIP(v.(string)) } + if v, ok := nMap["HostIPv6"]; ok { + ncfg.HostIPv6 = net.ParseIP(v.(string)) + } ncfg.DefaultBridge = nMap["DefaultBridge"].(bool) ncfg.DefaultBindingIP = net.ParseIP(nMap["DefaultBindingIP"].(string)) diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go index 436be73153..d7f5966dc0 100644 --- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go +++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go @@ -258,9 +258,13 @@ func setupIPTablesInternal(ipVer iptables.IPVersion, config *networkConfiguratio natArgs []string hpNatArgs []string ) - // If config.HostIPv4 is set, the user wants IPv4 SNAT with the given address. - if config.HostIPv4 != nil && ipVer == iptables.IPv4 { - hostAddr := config.HostIPv4.String() + hostIP := config.HostIPv4 + if ipVer == iptables.IPv6 { + hostIP = config.HostIPv6 + } + // If hostIP is set, the user wants IPv4/IPv6 SNAT with the given address. + if hostIP != nil { + hostAddr := hostIP.String() natArgs = []string{"-s", address, "!", "-o", config.BridgeName, "-j", "SNAT", "--to-source", hostAddr} hpNatArgs = []string{"-m", "addrtype", "--src-type", "LOCAL", "-o", config.BridgeName, "-j", "SNAT", "--to-source", hostAddr} // Else use MASQUERADE which picks the src-ip based on NH from the route table diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go index 2658a94dc2..dd5fd678b6 100644 --- a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go +++ b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go @@ -212,6 +212,7 @@ func TestOutgoingNATRules(t *testing.T) { maskedBrIPv4 := &net.IPNet{IP: brIPv4.IP.Mask(brIPv4.Mask), Mask: brIPv4.Mask} maskedBrIPv6 := &net.IPNet{IP: brIPv6.IP.Mask(brIPv6.Mask), Mask: brIPv6.Mask} hostIPv4 := net.ParseIP("192.0.2.2") + hostIPv6 := net.ParseIP("2001:db8:1::1") for _, tc := range []struct { desc string enableIPTables bool @@ -219,6 +220,7 @@ func TestOutgoingNATRules(t *testing.T) { enableIPv6 bool enableIPMasquerade bool hostIPv4 net.IP + hostIPv6 net.IP // Hairpin NAT rules are not tested here because they are orthogonal to outgoing NAT. They // exist to support the port forwarding DNAT rules: without any port forwarding there would be // no need for any hairpin NAT rules, and when there is port forwarding then hairpin NAT rules @@ -227,6 +229,7 @@ func TestOutgoingNATRules(t *testing.T) { wantIPv4Masq bool wantIPv4Snat bool wantIPv6Masq bool + wantIPv6Snat bool }{ { desc: "everything disabled", @@ -241,6 +244,7 @@ func TestOutgoingNATRules(t *testing.T) { enableIPv6: true, enableIPMasquerade: true, hostIPv4: hostIPv4, + hostIPv6: hostIPv6, }, { desc: "masquerade disabled, no host IP", @@ -254,6 +258,7 @@ func TestOutgoingNATRules(t *testing.T) { enableIP6Tables: true, enableIPv6: true, hostIPv4: hostIPv4, + hostIPv6: hostIPv6, }, { desc: "IPv4 masquerade, IPv6 disabled", @@ -277,6 +282,16 @@ func TestOutgoingNATRules(t *testing.T) { wantIPv4Masq: true, wantIPv6Masq: true, }, + { + desc: "IPv4 masquerade, IPv6 SNAT", + enableIPTables: true, + enableIP6Tables: true, + enableIPv6: true, + enableIPMasquerade: true, + hostIPv6: hostIPv6, + wantIPv4Masq: true, + wantIPv6Snat: true, + }, { desc: "IPv4 SNAT, IPv6 masquerade", enableIPTables: true, @@ -287,6 +302,17 @@ func TestOutgoingNATRules(t *testing.T) { wantIPv4Snat: true, wantIPv6Masq: true, }, + { + desc: "IPv4 SNAT, IPv6 SNAT", + enableIPTables: true, + enableIP6Tables: true, + enableIPv6: true, + enableIPMasquerade: true, + hostIPv4: hostIPv4, + hostIPv6: hostIPv6, + wantIPv4Snat: true, + wantIPv6Snat: true, + }, } { t.Run(tc.desc, func(t *testing.T) { defer netnsutils.SetupTestOSContext(t)() @@ -308,6 +334,7 @@ func TestOutgoingNATRules(t *testing.T) { EnableIPv6: tc.enableIPv6, EnableIPMasquerade: tc.enableIPMasquerade, HostIPv4: tc.hostIPv4, + HostIPv6: tc.hostIPv6, } ipv4Data := []driverapi.IPAMData{{Pool: maskedBrIPv4, Gateway: brIPv4}} ipv6Data := []driverapi.IPAMData{{Pool: maskedBrIPv6, Gateway: brIPv6}} @@ -343,6 +370,7 @@ func TestOutgoingNATRules(t *testing.T) { {tc.wantIPv4Masq, iptRule{iptables.IPv4, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv4.String(), "!", "-o", br, "-j", "MASQUERADE"}}}, {tc.wantIPv4Snat, iptRule{iptables.IPv4, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv4.String(), "!", "-o", br, "-j", "SNAT", "--to-source", hostIPv4.String()}}}, {tc.wantIPv6Masq, iptRule{iptables.IPv6, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv6.String(), "!", "-o", br, "-j", "MASQUERADE"}}}, + {tc.wantIPv6Snat, iptRule{iptables.IPv6, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv6.String(), "!", "-o", br, "-j", "SNAT", "--to-source", hostIPv6.String()}}}, } { assert.Equal(t, rc.rule.Exists(), rc.want) } diff --git a/libnetwork/netlabel/labels.go b/libnetwork/netlabel/labels.go index 0edc0f6cd2..91232af6aa 100644 --- a/libnetwork/netlabel/labels.go +++ b/libnetwork/netlabel/labels.go @@ -47,6 +47,9 @@ const ( // HostIPv4 is the Source-IPv4 Address used to SNAT IPv4 container traffic HostIPv4 = Prefix + ".host_ipv4" + // HostIPv6 is the Source-IPv6 Address used to SNAT IPv6 container traffic + HostIPv6 = Prefix + ".host_ipv6" + // LocalKVClient constants represents the local kv store client LocalKVClient = DriverPrivatePrefix + "localkv_client" )