package portmapper import ( "errors" "fmt" "net" "github.com/docker/docker/libnetwork/portallocator" "github.com/ishidawataru/sctp" "github.com/sirupsen/logrus" ) type mapping struct { proto string userlandProxy userlandProxy host net.Addr container net.Addr } // newProxy is used to mock out the proxy server in tests var newProxy = newProxyCommand var ( // ErrUnknownBackendAddressType refers to an unknown container or unsupported address type ErrUnknownBackendAddressType = errors.New("unknown container address type not supported") // ErrPortMappedForIP refers to a port already mapped to an ip address ErrPortMappedForIP = errors.New("port is already mapped to ip") // ErrPortNotMapped refers to an unmapped port ErrPortNotMapped = errors.New("port is not mapped") // ErrSCTPAddrNoIP refers to a SCTP address without IP address. ErrSCTPAddrNoIP = errors.New("sctp address does not contain any IP address") ) // New returns a new instance of PortMapper func New(proxyPath string) *PortMapper { return NewWithPortAllocator(portallocator.Get(), proxyPath) } // NewWithPortAllocator returns a new instance of PortMapper which will use the specified PortAllocator func NewWithPortAllocator(allocator *portallocator.PortAllocator, proxyPath string) *PortMapper { return &PortMapper{ currentMappings: make(map[string]*mapping), Allocator: allocator, proxyPath: proxyPath, } } // Map maps the specified container transport address to the host's network address and transport port func (pm *PortMapper) Map(container net.Addr, hostIP net.IP, hostPort int, useProxy bool) (host net.Addr, err error) { return pm.MapRange(container, hostIP, hostPort, hostPort, useProxy) } // MapRange maps the specified container transport address to the host's network address and transport port range func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart, hostPortEnd int, useProxy bool) (host net.Addr, err error) { pm.lock.Lock() defer pm.lock.Unlock() var ( m *mapping proto string allocatedHostPort int ) switch t := container.(type) { case *net.TCPAddr: proto = "tcp" if allocatedHostPort, err = pm.Allocator.RequestPortInRange(hostIP, proto, hostPortStart, hostPortEnd); err != nil { return nil, err } m = &mapping{ proto: proto, host: &net.TCPAddr{IP: hostIP, Port: allocatedHostPort}, container: container, } if useProxy { m.userlandProxy, err = newProxy(proto, hostIP, allocatedHostPort, t.IP, t.Port, pm.proxyPath) if err != nil { return nil, err } } else { m.userlandProxy, err = newDummyProxy(proto, hostIP, allocatedHostPort) if err != nil { return nil, err } } case *net.UDPAddr: proto = "udp" if allocatedHostPort, err = pm.Allocator.RequestPortInRange(hostIP, proto, hostPortStart, hostPortEnd); err != nil { return nil, err } m = &mapping{ proto: proto, host: &net.UDPAddr{IP: hostIP, Port: allocatedHostPort}, container: container, } if useProxy { m.userlandProxy, err = newProxy(proto, hostIP, allocatedHostPort, t.IP, t.Port, pm.proxyPath) if err != nil { return nil, err } } else { m.userlandProxy, err = newDummyProxy(proto, hostIP, allocatedHostPort) if err != nil { return nil, err } } case *sctp.SCTPAddr: proto = "sctp" if allocatedHostPort, err = pm.Allocator.RequestPortInRange(hostIP, proto, hostPortStart, hostPortEnd); err != nil { return nil, err } m = &mapping{ proto: proto, host: &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: hostIP}}, Port: allocatedHostPort}, container: container, } if useProxy { sctpAddr := container.(*sctp.SCTPAddr) if len(sctpAddr.IPAddrs) == 0 { return nil, ErrSCTPAddrNoIP } m.userlandProxy, err = newProxy(proto, hostIP, allocatedHostPort, sctpAddr.IPAddrs[0].IP, sctpAddr.Port, pm.proxyPath) if err != nil { return nil, err } } else { m.userlandProxy, err = newDummyProxy(proto, hostIP, allocatedHostPort) if err != nil { return nil, err } } default: return nil, ErrUnknownBackendAddressType } // release the allocated port on any further error during return. defer func() { if err != nil { pm.Allocator.ReleasePort(hostIP, proto, allocatedHostPort) } }() key := getKey(m.host) if _, exists := pm.currentMappings[key]; exists { return nil, ErrPortMappedForIP } containerIP, containerPort := getIPAndPort(m.container) 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() pm.DeleteForwardingTableEntry(m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort) if err := pm.Allocator.ReleasePort(hostIP, m.proto, allocatedHostPort); err != nil { return err } return nil } if err := m.userlandProxy.Start(); err != nil { if err := cleanup(); err != nil { return nil, fmt.Errorf("Error during port allocation cleanup: %v", err) } return nil, err } pm.currentMappings[key] = m return m.host, nil } // Unmap removes stored mapping for the specified host transport address func (pm *PortMapper) Unmap(host net.Addr) error { pm.lock.Lock() defer pm.lock.Unlock() key := getKey(host) data, exists := pm.currentMappings[key] if !exists { return ErrPortNotMapped } if data.userlandProxy != nil { data.userlandProxy.Stop() } delete(pm.currentMappings, key) containerIP, containerPort := getIPAndPort(data.container) hostIP, hostPort := getIPAndPort(data.host) if err := pm.DeleteForwardingTableEntry(data.proto, hostIP, hostPort, containerIP.String(), containerPort); err != nil { logrus.Errorf("Error on iptables delete: %s", err) } switch a := host.(type) { case *net.TCPAddr: return pm.Allocator.ReleasePort(a.IP, "tcp", a.Port) case *net.UDPAddr: return pm.Allocator.ReleasePort(a.IP, "udp", a.Port) case *sctp.SCTPAddr: if len(a.IPAddrs) == 0 { return ErrSCTPAddrNoIP } return pm.Allocator.ReleasePort(a.IPAddrs[0].IP, "sctp", a.Port) } return ErrUnknownBackendAddressType } // ReMapAll re-applies all port mappings func (pm *PortMapper) ReMapAll() { pm.lock.Lock() defer pm.lock.Unlock() logrus.Debugln("Re-applying all port mappings.") for _, data := range pm.currentMappings { containerIP, containerPort := getIPAndPort(data.container) hostIP, hostPort := getIPAndPort(data.host) if err := pm.AppendForwardingTableEntry(data.proto, hostIP, hostPort, containerIP.String(), containerPort); err != nil { logrus.Errorf("Error on iptables add: %s", err) } } } func getKey(a net.Addr) string { switch t := a.(type) { case *net.TCPAddr: return fmt.Sprintf("%s:%d/%s", t.IP.String(), t.Port, "tcp") case *net.UDPAddr: return fmt.Sprintf("%s:%d/%s", t.IP.String(), t.Port, "udp") case *sctp.SCTPAddr: if len(t.IPAddrs) == 0 { logrus.Error(ErrSCTPAddrNoIP) return "" } return fmt.Sprintf("%s:%d/%s", t.IPAddrs[0].IP.String(), t.Port, "sctp") } return "" } func getIPAndPort(a net.Addr) (net.IP, int) { switch t := a.(type) { case *net.TCPAddr: return t.IP, t.Port case *net.UDPAddr: return t.IP, t.Port case *sctp.SCTPAddr: if len(t.IPAddrs) == 0 { logrus.Error(ErrSCTPAddrNoIP) return nil, 0 } return t.IPAddrs[0].IP, t.Port } return nil, 0 }