6ae6dcfc53
The defer was set after the switch, but various code-paths inside the switch could return with an error after the port was allocated / reserved, which could result in those ports not being released. This patch moves the defer into each individual branch of the switch to set it immediately after succesfully reserving the port. We can also remove a redundant ReleasePort from the cleanup function, as it's only called if an error occurs, and the defers already take care of that. Note that the cleanup function was handling errors returned by ReleasePort, but this function never returns an error, so it was fully redundant. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
276 lines
7.5 KiB
Go
276 lines
7.5 KiB
Go
package portmapper
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
|
|
"github.com/containerd/log"
|
|
"github.com/docker/docker/libnetwork/portallocator"
|
|
"github.com/ishidawataru/sctp"
|
|
)
|
|
|
|
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() *PortMapper {
|
|
return NewWithPortAllocator(portallocator.Get(), "")
|
|
}
|
|
|
|
// 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, retErr error) {
|
|
pm.lock.Lock()
|
|
defer pm.lock.Unlock()
|
|
|
|
var (
|
|
m *mapping
|
|
proto string
|
|
allocatedHostPort int
|
|
)
|
|
|
|
switch t := container.(type) {
|
|
case *net.TCPAddr:
|
|
proto = "tcp"
|
|
|
|
var err error
|
|
allocatedHostPort, err = pm.allocator.RequestPortInRange(hostIP, proto, hostPortStart, hostPortEnd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if retErr != nil {
|
|
pm.allocator.ReleasePort(hostIP, proto, allocatedHostPort)
|
|
}
|
|
}()
|
|
|
|
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"
|
|
|
|
var err error
|
|
allocatedHostPort, err = pm.allocator.RequestPortInRange(hostIP, proto, hostPortStart, hostPortEnd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if retErr != nil {
|
|
pm.allocator.ReleasePort(hostIP, proto, allocatedHostPort)
|
|
}
|
|
}()
|
|
|
|
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"
|
|
|
|
var err error
|
|
allocatedHostPort, err = pm.allocator.RequestPortInRange(hostIP, proto, hostPortStart, hostPortEnd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if retErr != nil {
|
|
pm.allocator.ReleasePort(hostIP, proto, allocatedHostPort)
|
|
}
|
|
}()
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
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 {
|
|
log.G(context.TODO()).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()
|
|
log.G(context.TODO()).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 {
|
|
log.G(context.TODO()).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 {
|
|
log.G(context.TODO()).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 {
|
|
log.G(context.TODO()).Error(ErrSCTPAddrNoIP)
|
|
return nil, 0
|
|
}
|
|
return t.IPAddrs[0].IP, t.Port
|
|
}
|
|
return nil, 0
|
|
}
|