mirror of
https://github.com/moby/moby.git
synced 2026-08-02 22:26:52 +00:00
The only viable way to allocate a port is to bind and listen to it. So, the windows PortMapper was really a PortAllocator in disguise. Rename it to OSAllocator and move it to the portallocator package. Signed-off-by: Albin Kerouanton <albinker@gmail.com>
154 lines
4.2 KiB
Go
154 lines
4.2 KiB
Go
package portallocator
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"sync"
|
|
|
|
"github.com/containerd/log"
|
|
"github.com/ishidawataru/sctp"
|
|
"github.com/moby/moby/v2/daemon/libnetwork/types"
|
|
)
|
|
|
|
var (
|
|
// 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")
|
|
)
|
|
|
|
// OSAllocator allocates ports from the OS by creating listening sockets.
|
|
type OSAllocator struct {
|
|
// osListeners stores listening sockets used by active port mappings
|
|
// to reserve ports from the OS. Outer map is keyed by protocol, and inner
|
|
// map is keyed by host address and port.
|
|
osListeners map[types.Protocol]map[netip.AddrPort]io.Closer
|
|
lock sync.Mutex
|
|
|
|
allocator *PortAllocator
|
|
}
|
|
|
|
// New returns a new instance of OSAllocator
|
|
func New() *OSAllocator {
|
|
return &OSAllocator{
|
|
osListeners: make(map[types.Protocol]map[netip.AddrPort]io.Closer),
|
|
allocator: Get(),
|
|
}
|
|
}
|
|
|
|
// AllocateHostPort allocates a port from the OS (by creating a listening socket).
|
|
func (pa *OSAllocator) AllocateHostPort(hostIP net.IP, proto types.Protocol, hostPortStart, hostPortEnd int) (_ int, retErr error) {
|
|
pa.lock.Lock()
|
|
defer pa.lock.Unlock()
|
|
|
|
allocatedHostPort, err := pa.allocator.RequestPortInRange(hostIP, proto.String(), hostPortStart, hostPortEnd)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer func() {
|
|
if retErr != nil {
|
|
pa.allocator.ReleasePort(hostIP, proto.String(), allocatedHostPort)
|
|
}
|
|
}()
|
|
|
|
if pa.osListeners[proto] == nil {
|
|
pa.osListeners[proto] = make(map[netip.AddrPort]io.Closer)
|
|
}
|
|
|
|
addr, ok := netip.AddrFromSlice(hostIP)
|
|
if !ok {
|
|
return 0, fmt.Errorf("invalid HostIP: %s", hostIP)
|
|
}
|
|
|
|
hAddrPort := netip.AddrPortFrom(addr, uint16(allocatedHostPort))
|
|
if _, exists := pa.osListeners[proto][hAddrPort]; exists {
|
|
return 0, ErrPortMappedForIP
|
|
}
|
|
|
|
var osListener io.Closer
|
|
osListener, err = allocateHostPort(proto.String(), hostIP, allocatedHostPort)
|
|
if err != nil {
|
|
if osListener != nil {
|
|
if err := osListener.Close(); err != nil {
|
|
// Prior to v29.0, this error was never checked. So, instead of
|
|
// returning an error, log it and proceed.
|
|
log.G(context.TODO()).Infof("failed to stop dummy proxy for %s/%s: %v", hostIP, proto, err)
|
|
}
|
|
}
|
|
return 0, err
|
|
}
|
|
|
|
pa.osListeners[proto][hAddrPort] = osListener
|
|
return allocatedHostPort, nil
|
|
}
|
|
|
|
// allocateHostPort allocates a host port by binding to the specified host IP and port.
|
|
func allocateHostPort(proto string, hostIP net.IP, hostPort int) (io.Closer, error) {
|
|
// detect version of hostIP to bind only to correct version
|
|
protoVer := proto + "4"
|
|
if hostIP.To4() == nil {
|
|
protoVer = proto + "6"
|
|
}
|
|
|
|
switch proto {
|
|
case "tcp":
|
|
l, err := net.ListenTCP(protoVer, &net.TCPAddr{IP: hostIP, Port: hostPort})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return l, nil
|
|
case "udp":
|
|
l, err := net.ListenUDP(protoVer, &net.UDPAddr{IP: hostIP, Port: hostPort})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return l, nil
|
|
case "sctp":
|
|
l, err := sctp.ListenSCTP(protoVer, &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: hostIP}}, Port: hostPort})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return l, nil
|
|
default:
|
|
return nil, fmt.Errorf("protocol %s not supported", proto)
|
|
}
|
|
}
|
|
|
|
// Deallocate removes stored mapping for the specified host transport address
|
|
func (pa *OSAllocator) Deallocate(hostIP net.IP, proto types.Protocol, hostPort int) error {
|
|
pa.lock.Lock()
|
|
defer pa.lock.Unlock()
|
|
|
|
addr, ok := netip.AddrFromSlice(hostIP)
|
|
if !ok {
|
|
return fmt.Errorf("invalid HostIP: %s", hostIP)
|
|
}
|
|
|
|
if pa.osListeners[proto] == nil {
|
|
return ErrPortNotMapped
|
|
}
|
|
|
|
hAddrPort := netip.AddrPortFrom(addr, uint16(hostPort))
|
|
osListener, exists := pa.osListeners[proto][hAddrPort]
|
|
if !exists {
|
|
return ErrPortNotMapped
|
|
}
|
|
|
|
if osListener != nil {
|
|
if err := osListener.Close(); err != nil {
|
|
// Prior to v29.0, this error was never checked. So, instead of
|
|
// returning an error, log it and proceed.
|
|
log.G(context.TODO()).Infof("failed to stop dummy proxy for %s/%s: %v", hostIP, proto, err)
|
|
}
|
|
}
|
|
|
|
delete(pa.osListeners[proto], hAddrPort)
|
|
|
|
pa.allocator.ReleasePort(hostIP, proto.String(), int(hostPort))
|
|
return nil
|
|
}
|