blob: be4157b0d543e81b6c84114bce4d2b6e95b78a6d [file] [log] [blame]
package portmapper
import (
"errors"
"fmt"
"net"
"github.com/docker/libnetwork/portallocator"
"github.com/ishidawataru/sctp"
"github.com/sirupsen/logrus"
)
type mapping struct {
proto string
userlandProxy userlandProxy
host net.Addr
container net.Addr
}
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 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, container.(*net.TCPAddr).IP, container.(*net.TCPAddr).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, container.(*net.UDPAddr).IP, container.(*net.UDPAddr).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{IP: []net.IP{hostIP}, Port: allocatedHostPort},
container: container,
}
if useProxy {
sctpAddr := container.(*sctp.SCTPAddr)
if len(sctpAddr.IP) == 0 {
return nil, ErrSCTPAddrNoIP
}
m.userlandProxy, err = newProxy(proto, hostIP, allocatedHostPort, sctpAddr.IP[0], 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 hostIP.To4() != nil {
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 hostIP.To4() != nil {
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.IP) == 0 {
return ErrSCTPAddrNoIP
}
return pm.Allocator.ReleasePort(a.IP[0], "sctp", a.Port)
}
return ErrUnknownBackendAddressType
}
//ReMapAll will re-apply 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.IP) == 0 {
logrus.Error(ErrSCTPAddrNoIP)
return ""
}
return fmt.Sprintf("%s:%d/%s", t.IP[0].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.IP) == 0 {
logrus.Error(ErrSCTPAddrNoIP)
return nil, 0
}
return t.IP[0], t.Port
}
return nil, 0
}