Merge pull request #50289 from akerouanton/cleanup-windows-portmapper

libnet/portmapper: clean up windows port mapper
This commit is contained in:
Albin Kerouanton
2025-09-08 22:52:45 +02:00
committed by GitHub
9 changed files with 201 additions and 437 deletions

View File

@@ -159,14 +159,14 @@ func (d *driver) CreateEndpoint(ctx context.Context, nid, eid string, ifInfo dri
}
ep.portMapping = epConnectivity.PortBindings
ep.portMapping, err = windows.AllocatePorts(n.portMapper, ep.portMapping, ep.addr.IP)
ep.portMapping, err = windows.AllocatePorts(n.pa, ep.portMapping)
if err != nil {
return err
}
defer func() {
if err != nil {
windows.ReleasePorts(n.portMapper, ep.portMapping)
windows.ReleasePorts(n.pa, ep.portMapping)
}
}()
@@ -227,7 +227,7 @@ func (d *driver) DeleteEndpoint(nid, eid string) error {
return fmt.Errorf("endpoint id %q not found", eid)
}
windows.ReleasePorts(n.portMapper, ep.portMapping)
windows.ReleasePorts(n.pa, ep.portMapping)
n.deleteEndpoint(eid)

View File

@@ -15,7 +15,7 @@ import (
"github.com/moby/moby/v2/daemon/libnetwork/driverapi"
"github.com/moby/moby/v2/daemon/libnetwork/drivers/overlay"
"github.com/moby/moby/v2/daemon/libnetwork/netlabel"
"github.com/moby/moby/v2/daemon/libnetwork/portmapper"
"github.com/moby/moby/v2/daemon/libnetwork/portallocator"
"github.com/moby/moby/v2/daemon/libnetwork/types"
)
@@ -50,7 +50,7 @@ type network struct {
initErr error
subnets []*subnet
secure bool
portMapper *portmapper.PortMapper
pa *portallocator.OSAllocator
sync.Mutex
}
@@ -86,11 +86,11 @@ func (d *driver) CreateNetwork(ctx context.Context, id string, option map[string
}
n := &network{
id: id,
driver: d,
endpoints: endpointTable{},
subnets: []*subnet{},
portMapper: portmapper.New(),
id: id,
driver: d,
endpoints: endpointTable{},
subnets: []*subnet{},
pa: portallocator.New(),
}
genData, ok := option[netlabel.GenericData].(map[string]string)

View File

@@ -3,28 +3,26 @@
package windows
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"github.com/containerd/log"
"github.com/ishidawataru/sctp"
"github.com/moby/moby/v2/daemon/libnetwork/portmapper"
"github.com/moby/moby/v2/daemon/libnetwork/portallocator"
"github.com/moby/moby/v2/daemon/libnetwork/types"
)
const maxAllocatePortAttempts = 10
// AllocatePorts allocates ports specified in bindings from the portMapper
func AllocatePorts(portMapper *portmapper.PortMapper, bindings []types.PortBinding, containerIP net.IP) ([]types.PortBinding, error) {
// AllocatePorts allocates ports specified in bindings from the port allocator.
func AllocatePorts(pa *portallocator.OSAllocator, bindings []types.PortBinding) ([]types.PortBinding, error) {
bs := make([]types.PortBinding, 0, len(bindings))
for _, c := range bindings {
b := c.Copy()
if err := allocatePort(portMapper, &b, containerIP); err != nil {
b, err := allocatePort(pa, c)
if err != nil {
// On allocation failure, release previously allocated ports. On cleanup error, just log a warning message
if cuErr := ReleasePorts(portMapper, bs); cuErr != nil {
if cuErr := ReleasePorts(pa, bs); cuErr != nil {
log.G(context.TODO()).Warnf("Upon allocation failure for %v, failed to clear previously allocated port bindings: %v", b, cuErr)
}
return nil, err
@@ -34,35 +32,24 @@ func AllocatePorts(portMapper *portmapper.PortMapper, bindings []types.PortBindi
return bs, nil
}
func allocatePort(portMapper *portmapper.PortMapper, bnd *types.PortBinding, containerIP net.IP) error {
var (
host net.Addr
err error
)
func allocatePort(pa *portallocator.OSAllocator, bnd types.PortBinding) (types.PortBinding, error) {
// Windows does not support a host ip for port bindings (this is validated in ConvertPortBindings()).
// If the HostIP is nil, force it to be 0.0.0.0 for use as the key in portMapper.
// If the HostIP is nil, force it to be 0.0.0.0 for use as the key in the port allocator.
if bnd.HostIP == nil {
bnd.HostIP = net.IPv4zero
}
// Store the container interface address in the operational binding
bnd.IP = containerIP
// Adjust HostPortEnd if this is not a range.
if bnd.HostPortEnd == 0 {
bnd.HostPortEnd = bnd.HostPort
}
// Construct the container side transport address
container, err := bnd.ContainerAddr()
if err != nil {
return err
}
// Try up to maxAllocatePortAttempts times to get a port that's not already allocated.
var allocatedPort int
var err error
for i := 0; i < maxAllocatePortAttempts; i++ {
if host, err = portMapper.MapRange(container, bnd.HostIP, int(bnd.HostPort), int(bnd.HostPortEnd)); err == nil {
allocatedPort, err = pa.AllocateHostPort(bnd.HostIP, bnd.Proto, int(bnd.HostPort), int(bnd.HostPortEnd))
if err == nil {
break
}
// There is no point in immediately retrying to map an explicitly chosen port.
@@ -73,51 +60,25 @@ func allocatePort(portMapper *portmapper.PortMapper, bnd *types.PortBinding, con
log.G(context.TODO()).Warnf("Failed to allocate and map port: %s, retry: %d", err, i+1)
}
if err != nil {
return err
return types.PortBinding{}, err
}
// Save the host port (regardless it was or not specified in the binding)
switch netAddr := host.(type) {
case *net.TCPAddr:
bnd.HostPort = uint16(host.(*net.TCPAddr).Port)
break
case *net.UDPAddr:
bnd.HostPort = uint16(host.(*net.UDPAddr).Port)
break
case *sctp.SCTPAddr:
bnd.HostPort = uint16(host.(*sctp.SCTPAddr).Port)
break
default:
// For completeness
return fmt.Errorf("unsupported address type: %T", netAddr)
}
// Windows does not support host port ranges.
bnd.HostPortEnd = bnd.HostPort
return nil
bnd.HostPort = uint16(allocatedPort)
bnd.HostPortEnd = uint16(allocatedPort)
return bnd, nil
}
// ReleasePorts releases ports specified in bindings from the portMapper
func ReleasePorts(portMapper *portmapper.PortMapper, bindings []types.PortBinding) error {
var errorBuf bytes.Buffer
// ReleasePorts releases ports specified in bindings from the portAlloc
func ReleasePorts(pa *portallocator.OSAllocator, bindings []types.PortBinding) error {
var errs []error
// Attempt to release all port bindings, do not stop on failure
for _, m := range bindings {
if err := releasePort(portMapper, m); err != nil {
errorBuf.WriteString(fmt.Sprintf("\ncould not release %v because of %v", m, err))
if err := pa.Deallocate(m.HostIP, m.Proto, int(m.HostPort)); err != nil {
errs = append(errs, fmt.Errorf("could not release %v because of %v", m, err))
}
}
if errorBuf.Len() != 0 {
return errors.New(errorBuf.String())
}
return nil
}
func releasePort(portMapper *portmapper.PortMapper, bnd types.PortBinding) error {
// Construct the host side transport address
host, err := bnd.HostAddr()
if err != nil {
return err
}
return portMapper.Unmap(host)
return errors.Join(errs...)
}

View File

@@ -27,7 +27,7 @@ import (
"github.com/moby/moby/v2/daemon/libnetwork/datastore"
"github.com/moby/moby/v2/daemon/libnetwork/driverapi"
"github.com/moby/moby/v2/daemon/libnetwork/netlabel"
"github.com/moby/moby/v2/daemon/libnetwork/portmapper"
"github.com/moby/moby/v2/daemon/libnetwork/portallocator"
"github.com/moby/moby/v2/daemon/libnetwork/scope"
"github.com/moby/moby/v2/daemon/libnetwork/types"
"go.opentelemetry.io/otel"
@@ -94,12 +94,12 @@ type hnsEndpoint struct {
}
type hnsNetwork struct {
id string
created bool
config *networkConfiguration
endpoints map[string]*hnsEndpoint // key: endpoint id
driver *driver // The network's driver
portMapper *portmapper.PortMapper
id string
created bool
config *networkConfiguration
endpoints map[string]*hnsEndpoint // key: endpoint id
driver *driver // The network's driver
pa *portallocator.OSAllocator
sync.Mutex
}
@@ -308,11 +308,11 @@ func (ncfg *networkConfiguration) processIPAM(id string, ipamV4Data, ipamV6Data
func (d *driver) createNetwork(config *networkConfiguration) *hnsNetwork {
network := &hnsNetwork{
id: config.ID,
endpoints: make(map[string]*hnsEndpoint),
config: config,
driver: d,
portMapper: portmapper.New(),
id: config.ID,
endpoints: make(map[string]*hnsEndpoint),
config: config,
driver: d,
pa: portallocator.New(),
}
d.Lock()
@@ -701,19 +701,14 @@ func (d *driver) CreateEndpoint(ctx context.Context, nid, eid string, ifInfo dri
portMapping := epConnectivity.PortBindings
if n.config.Type == "l2bridge" || n.config.Type == "l2tunnel" {
ip := net.IPv4(0, 0, 0, 0)
if ifInfo.Address() != nil {
ip = ifInfo.Address().IP
}
portMapping, err = AllocatePorts(n.portMapper, portMapping, ip)
portMapping, err = AllocatePorts(n.pa, portMapping)
if err != nil {
return err
}
defer func() {
if err != nil {
ReleasePorts(n.portMapper, portMapping)
ReleasePorts(n.pa, portMapping)
}
}()
}
@@ -828,7 +823,7 @@ func (d *driver) DeleteEndpoint(nid, eid string) error {
}
if n.config.Type == "l2bridge" || n.config.Type == "l2tunnel" {
ReleasePorts(n.portMapper, ep.portMapping)
ReleasePorts(n.pa, ep.portMapping)
}
n.Lock()

View File

@@ -0,0 +1,153 @@
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
}

View File

@@ -1,215 +0,0 @@
//go:build windows
package portmapper
import (
"context"
"errors"
"fmt"
"net"
"github.com/containerd/log"
"github.com/ishidawataru/sctp"
"github.com/moby/moby/v2/daemon/libnetwork/portallocator"
)
type mapping struct {
proto string
stopUserlandProxy func() error
host net.Addr
container net.Addr
}
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,
}
}
// 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) (host net.Addr, retErr error) {
pm.lock.Lock()
defer pm.lock.Unlock()
var (
m *mapping
proto string
allocatedHostPort int
)
switch 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,
}
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,
}
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,
}
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
}
var err error
m.stopUserlandProxy, err = newDummyProxy(m.proto, hostIP, allocatedHostPort)
if err != nil {
// FIXME(thaJeztah): both stopping the proxy and deleting iptables rules can produce an error, and both are not currently handled.
m.stopUserlandProxy()
// need to undo the iptables rules before we return
pm.DeleteForwardingTableEntry(m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort)
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.stopUserlandProxy != nil {
data.stopUserlandProxy()
}
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:
pm.allocator.ReleasePort(a.IP, "tcp", a.Port)
case *net.UDPAddr:
pm.allocator.ReleasePort(a.IP, "udp", a.Port)
case *sctp.SCTPAddr:
if len(a.IPAddrs) == 0 {
return ErrSCTPAddrNoIP
}
pm.allocator.ReleasePort(a.IPAddrs[0].IP, "sctp", a.Port)
default:
return ErrUnknownBackendAddressType
}
return nil
}
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
}

View File

@@ -1,31 +0,0 @@
package portmapper
import (
"net"
"sync"
"github.com/moby/moby/v2/daemon/libnetwork/portallocator"
)
// PortMapper manages the network address translation
type PortMapper struct {
bridgeName string
// udp:ip:port
currentMappings map[string]*mapping
lock sync.Mutex
proxyPath string
allocator *portallocator.PortAllocator
}
// AppendForwardingTableEntry adds a port mapping to the forwarding table
func (pm *PortMapper) AppendForwardingTableEntry(proto string, sourceIP net.IP, sourcePort int, containerIP string, containerPort int) error {
return nil
}
// DeleteForwardingTableEntry removes a port mapping from the forwarding table
func (pm *PortMapper) DeleteForwardingTableEntry(proto string, sourceIP net.IP, sourcePort int, containerIP string, containerPort int) error {
return nil
}

View File

@@ -1,85 +0,0 @@
package portmapper
import (
"fmt"
"io"
"net"
"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"
)
// dummyProxy just listen on some port, it is needed to prevent accidental
// port allocations on bound port, because without userland proxy we using
// iptables rules and not net.Listen
type dummyProxy struct {
listener io.Closer
addr net.Addr
ipVersion ipVersion
}
func newDummyProxy(proto string, hostIP net.IP, hostPort int) (stop func() error, retErr error) {
// detect version of hostIP to bind only to correct version
version := ipv4
if hostIP.To4() == nil {
version = ipv6
}
var addr net.Addr
switch proto {
case "tcp":
addr = &net.TCPAddr{IP: hostIP, Port: hostPort}
case "udp":
addr = &net.UDPAddr{IP: hostIP, Port: hostPort}
case "sctp":
addr = &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: hostIP}}, Port: hostPort}
default:
return nil, fmt.Errorf("Unknown addr type: %s", proto)
}
p := &dummyProxy{addr: addr, ipVersion: version}
if err := p.start(); err != nil {
return nil, err
}
return p.stop, nil
}
func (p *dummyProxy) start() error {
switch addr := p.addr.(type) {
case *net.TCPAddr:
l, err := net.ListenTCP("tcp"+string(p.ipVersion), addr)
if err != nil {
return err
}
p.listener = l
case *net.UDPAddr:
l, err := net.ListenUDP("udp"+string(p.ipVersion), addr)
if err != nil {
return err
}
p.listener = l
case *sctp.SCTPAddr:
l, err := sctp.ListenSCTP("sctp"+string(p.ipVersion), addr)
if err != nil {
return err
}
p.listener = l
default:
return fmt.Errorf("Unknown addr type: %T", p.addr)
}
return nil
}
func (p *dummyProxy) stop() error {
if p.listener != nil {
return p.listener.Close()
}
return nil
}

View File

@@ -72,20 +72,6 @@ func (p PortBinding) HostAddr() (net.Addr, error) {
}
}
// ContainerAddr returns the container side transport address
func (p PortBinding) ContainerAddr() (net.Addr, error) {
switch p.Proto {
case UDP:
return &net.UDPAddr{IP: p.IP, Port: int(p.Port)}, nil
case TCP:
return &net.TCPAddr{IP: p.IP, Port: int(p.Port)}, nil
case SCTP:
return &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: p.IP}}, Port: int(p.Port)}, nil
default:
return nil, fmt.Errorf("invalid transport protocol: %s", p.Proto.String())
}
}
// Copy returns a deep copy of the PortBinding.
func (p PortBinding) Copy() PortBinding {
return PortBinding{