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 <rhansen@rhansen.org>
This commit is contained in:
Richard Hansen 2023-09-11 19:32:02 -04:00
parent fc4d035e7a
commit 808120e5b8
5 changed files with 47 additions and 3 deletions

View file

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

View file

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

View file

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

View file

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

View file

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