Skip to content

Commit

Permalink
Fix IPv6 Port Forwarding for the Bridge Driver
Browse files Browse the repository at this point in the history
1. Allocate either a IPv4 and/or IPv6 Port Binding (HostIP, HostPort, ContainerIP,
ContainerPort) based on the input and system parameters
2. Update the userland proxy as well as dummy proxy (inside port mapper) to
specifically listen on either the IPv4 or IPv6 network

Signed-off-by: Arko Dasgupta <arko.dasgupta@docker.com>
  • Loading branch information
Arko Dasgupta committed Dec 15, 2020
1 parent 5c6a95b commit 28576a4
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 68 deletions.
10 changes: 10 additions & 0 deletions cmd/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import (
"github.com/ishidawataru/sctp"
)

// ipVersion refers to IP version - v4 or v6
type ipVersion string

const (
// IPv4 is version 4
ipv4 ipVersion = "4"
// IPv4 is version 6
ipv6 ipVersion = "6"
)

// Proxy defines the behavior of a proxy. It forwards traffic back and forth
// between two endpoints : the frontend and the backend.
// It can be used to do software port-mapping between two addresses.
Expand Down
7 changes: 6 additions & 1 deletion cmd/proxy/sctp_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ type SCTPProxy struct {

// NewSCTPProxy creates a new SCTPProxy.
func NewSCTPProxy(frontendAddr, backendAddr *sctp.SCTPAddr) (*SCTPProxy, error) {
listener, err := sctp.ListenSCTP("sctp", frontendAddr)
// detect version of hostIP to bind only to correct version
ipVersion := ipv4
if frontendAddr.IPAddrs[0].IP.To4() == nil {
ipVersion = ipv6
}
listener, err := sctp.ListenSCTP("sctp"+string(ipVersion), frontendAddr)
if err != nil {
return nil, err
}
Expand Down
7 changes: 6 additions & 1 deletion cmd/proxy/tcp_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ type TCPProxy struct {

// NewTCPProxy creates a new TCPProxy.
func NewTCPProxy(frontendAddr, backendAddr *net.TCPAddr) (*TCPProxy, error) {
listener, err := net.ListenTCP("tcp", frontendAddr)
// detect version of hostIP to bind only to correct version
ipVersion := ipv4
if frontendAddr.IP.To4() == nil {
ipVersion = ipv6
}
listener, err := net.ListenTCP("tcp"+string(ipVersion), frontendAddr)
if err != nil {
return nil, err
}
Expand Down
7 changes: 6 additions & 1 deletion cmd/proxy/udp_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ type UDPProxy struct {

// NewUDPProxy creates a new UDPProxy.
func NewUDPProxy(frontendAddr, backendAddr *net.UDPAddr) (*UDPProxy, error) {
listener, err := net.ListenUDP("udp", frontendAddr)
// detect version of hostIP to bind only to correct version
ipVersion := ipv4
if frontendAddr.IP.To4() == nil {
ipVersion = ipv6
}
listener, err := net.ListenUDP("udp"+string(ipVersion), frontendAddr)
if err != nil {
return nil, err
}
Expand Down
120 changes: 81 additions & 39 deletions drivers/bridge/port_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,71 +11,113 @@ import (
"github.com/sirupsen/logrus"
)

var (
defaultBindingIP = net.IPv4(0, 0, 0, 0)
defaultBindingIPV6 = net.ParseIP("::")
)

func (n *bridgeNetwork) allocatePorts(ep *bridgeEndpoint, reqDefBindIP net.IP, ulPxyEnabled bool) ([]types.PortBinding, error) {
if ep.extConnConfig == nil || ep.extConnConfig.PortBindings == nil {
return nil, nil
}

defHostIP := defaultBindingIP
defHostIP := net.IPv4zero // 0.0.0.0
if reqDefBindIP != nil {
defHostIP = reqDefBindIP
}

// IPv4 port binding including user land proxy
pb, err := n.allocatePortsInternal(ep.extConnConfig.PortBindings, ep.addr.IP, defHostIP, ulPxyEnabled)
if err != nil {
return nil, err
var containerIPv6 net.IP
if ep.addrv6 != nil {
containerIPv6 = ep.addrv6.IP
}

// IPv6 port binding excluding user land proxy
if n.driver.config.EnableIP6Tables && ep.addrv6 != nil {
// TODO IPv6 custom default binding IP
pbv6, err := n.allocatePortsInternal(ep.extConnConfig.PortBindings, ep.addrv6.IP, defaultBindingIPV6, false)
if err != nil {
// ensure we clear the previous allocated IPv4 ports
n.releasePortsInternal(pb)
return nil, err
}

pb = append(pb, pbv6...)
pb, err := n.allocatePortsInternal(ep.extConnConfig.PortBindings, ep.addr.IP, containerIPv6, defHostIP, ulPxyEnabled)
if err != nil {
return nil, err
}
return pb, nil
}

func (n *bridgeNetwork) allocatePortsInternal(bindings []types.PortBinding, containerIP, defHostIP net.IP, ulPxyEnabled bool) ([]types.PortBinding, error) {
func (n *bridgeNetwork) allocatePortsInternal(bindings []types.PortBinding, containerIPv4, containerIPv6, defHostIP net.IP, ulPxyEnabled bool) ([]types.PortBinding, error) {
bs := make([]types.PortBinding, 0, len(bindings))
for _, c := range bindings {
b := c.GetCopy()
if err := n.allocatePort(&b, containerIP, defHostIP, ulPxyEnabled); err != nil {
// On allocation failure, release previously allocated ports. On cleanup error, just log a warning message
if cuErr := n.releasePortsInternal(bs); cuErr != nil {
logrus.Warnf("Upon allocation failure for %v, failed to clear previously allocated port bindings: %v", b, cuErr)
bIPv4 := c.GetCopy()
bIPv6 := c.GetCopy()
// Allocate IPv4 Port mappings
if ok := n.validatePortBindingIPv4(&bIPv4, containerIPv4, defHostIP); ok {
if err := n.allocatePort(&bIPv4, ulPxyEnabled); err != nil {
// On allocation failure, release previously allocated ports. On cleanup error, just log a warning message
if cuErr := n.releasePortsInternal(bs); cuErr != nil {
logrus.Warnf("allocation failure for %v, failed to clear previously allocated ipv4 port bindings: %v", bIPv4, cuErr)
}
return nil, err
}
return nil, err
bs = append(bs, bIPv4)
}
// Allocate IPv6 Port mappings
if ok := n.validatePortBindingIPv6(&bIPv6, containerIPv6, defHostIP); ok {
if err := n.allocatePort(&bIPv6, ulPxyEnabled); err != nil {
// On allocation failure, release previously allocated ports. On cleanup error, just log a warning message
if cuErr := n.releasePortsInternal(bs); cuErr != nil {
logrus.Warnf("allocation failure for %v, failed to clear previously allocated ipv6 port bindings: %v", bIPv6, cuErr)
}
return nil, err
}
bs = append(bs, bIPv6)
}
bs = append(bs, b)
}
return bs, nil
}

func (n *bridgeNetwork) allocatePort(bnd *types.PortBinding, containerIP, defHostIP net.IP, ulPxyEnabled bool) error {
var (
host net.Addr
err error
)

// Store the container interface address in the operational binding
bnd.IP = containerIP

// validatePortBindingIPv4 validates the port binding, populates the missing Host IP field and returns true
// if this is a valid IPv4 binding, else returns false
func (n *bridgeNetwork) validatePortBindingIPv4(bnd *types.PortBinding, containerIPv4, defHostIP net.IP) bool {
//Return early if there is a valid Host IP, but its not a IPv6 address
if len(bnd.HostIP) > 0 && bnd.HostIP.To4() == nil {
return false
}
// Adjust the host address in the operational binding
if len(bnd.HostIP) == 0 {
// Return early if the default binding address is an IPv6 address
if defHostIP.To4() == nil {
return false
}
bnd.HostIP = defHostIP
}
bnd.IP = containerIPv4
return true

}

// validatePortBindingIPv6 validates the port binding, populates the missing Host IP field and returns true
// if this is a valid IP6v binding, else returns false
func (n *bridgeNetwork) validatePortBindingIPv6(bnd *types.PortBinding, containerIPv6, defHostIP net.IP) bool {
// Return early if there is no IPv6 container endpoint
if containerIPv6 == nil {
return false
}
// Return early if there is a valid Host IP, which is a IPv4 address
if len(bnd.HostIP) > 0 && bnd.HostIP.To4() != nil {
return false
}

// Setup a binding to "::" if Host IP is empty and the default binding IP is 0.0.0.0
if len(bnd.HostIP) == 0 {
if defHostIP.Equal(net.IPv4zero) {
bnd.HostIP = net.IPv6zero
// If the default binding IP is an IPv6 address, use it
} else if defHostIP.To4() == nil {
bnd.HostIP = defHostIP
// Return false if default binding ip is an IPv4 address
} else {
return false
}
}
bnd.IP = containerIPv6
return true

}

func (n *bridgeNetwork) allocatePort(bnd *types.PortBinding, ulPxyEnabled bool) error {
var (
host net.Addr
err error
)

// Adjust HostPortEnd if this is not a range.
if bnd.HostPortEnd == 0 {
Expand All @@ -90,7 +132,7 @@ func (n *bridgeNetwork) allocatePort(bnd *types.PortBinding, containerIP, defHos

portmapper := n.portMapper

if containerIP.To4() == nil {
if bnd.IP.To4() == nil {
portmapper = n.portMapperV6
}

Expand Down
71 changes: 71 additions & 0 deletions drivers/bridge/port_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,74 @@ func TestPortMappingConfig(t *testing.T) {
t.Fatal(err)
}
}

func TestPortMappingV6Config(t *testing.T) {
defer testutils.SetupTestOSContext(t)()
d := newDriver()

config := &configuration{
EnableIPTables: true,
EnableIP6Tables: true,
}
genericOption := make(map[string]interface{})
genericOption[netlabel.GenericData] = config

if err := d.configure(genericOption); err != nil {
t.Fatalf("Failed to setup driver config: %v", err)
}

portBindings := []types.PortBinding{
{Proto: types.UDP, Port: uint16(400), HostPort: uint16(54000)},
{Proto: types.TCP, Port: uint16(500), HostPort: uint16(65000)},
{Proto: types.SCTP, Port: uint16(500), HostPort: uint16(65000)},
}

sbOptions := make(map[string]interface{})
sbOptions[netlabel.PortMap] = portBindings
netConfig := &networkConfiguration{
BridgeName: DefaultBridgeName,
EnableIPv6: true,
}
netOptions := make(map[string]interface{})
netOptions[netlabel.GenericData] = netConfig

ipdList := getIPv4Data(t, "")
err := d.CreateNetwork("dummy", netOptions, nil, ipdList, nil)
if err != nil {
t.Fatalf("Failed to create bridge: %v", err)
}

te := newTestEndpoint(ipdList[0].Pool, 11)
err = d.CreateEndpoint("dummy", "ep1", te.Interface(), nil)
if err != nil {
t.Fatalf("Failed to create the endpoint: %s", err.Error())
}

if err = d.Join("dummy", "ep1", "sbox", te, sbOptions); err != nil {
t.Fatalf("Failed to join the endpoint: %v", err)
}

if err = d.ProgramExternalConnectivity("dummy", "ep1", sbOptions); err != nil {
t.Fatalf("Failed to program external connectivity: %v", err)
}

network, ok := d.networks["dummy"]
if !ok {
t.Fatalf("Cannot find network %s inside driver", "dummy")
}
ep, _ := network.endpoints["ep1"]
if len(ep.portMapping) != 6 {
t.Fatalf("Failed to store the port bindings into the sandbox info. Found: %v", ep.portMapping)
}

// release host mapped ports
err = d.Leave("dummy", "ep1")
if err != nil {
t.Fatal(err)
}

err = d.RevokeExternalConnectivity("dummy", "ep1")
if err != nil {
t.Fatal(err)
}
}
2 changes: 1 addition & 1 deletion libnetwork_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func TestBridge(t *testing.T) {
if !ok {
t.Fatalf("Unexpected format for port mapping in endpoint operational data")
}
if len(pm) != 5 {
if len(pm) != 10 {
t.Fatalf("Incomplete data for port mapping in endpoint operational data: %d", len(pm))
}
}
Expand Down
14 changes: 5 additions & 9 deletions portmapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,20 +151,16 @@ func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart,
}

containerIP, containerPort := getIPAndPort(m.container)
if pm.checkIP(hostIP) {
if err := pm.AppendForwardingTableEntry(m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort); err != nil {
return nil, err
}
if err := pm.AppendForwardingTableEntry(m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort); err != nil {
return nil, err
}

cleanup := func() error {
// need to undo the iptables rules before we return
m.userlandProxy.Stop()
if pm.checkIP(hostIP) {
pm.DeleteForwardingTableEntry(m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort)
if err := pm.Allocator.ReleasePort(hostIP, m.proto, allocatedHostPort); err != nil {
return err
}
pm.DeleteForwardingTableEntry(m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort)
if err := pm.Allocator.ReleasePort(hostIP, m.proto, allocatedHostPort); err != nil {
return err
}

return nil
Expand Down
8 changes: 0 additions & 8 deletions portmapper/mapper_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,3 @@ func (pm *PortMapper) forward(action iptables.Action, proto string, sourceIP net
}
return pm.chain.Forward(action, sourceIP, sourcePort, proto, containerIP, containerPort, pm.bridgeName)
}

// checkIP checks if IP is valid and matching to chain version
func (pm *PortMapper) checkIP(ip net.IP) bool {
if pm.chain == nil || pm.chain.IPTable.Version == iptables.IPv4 {
return ip.To4() != nil
}
return ip.To16() != nil
}
Loading

0 comments on commit 28576a4

Please sign in to comment.