libnet/d/bridge: move portBinding to portmapperapi

All unexported fields in portBinding are now exported.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit is contained in:
Albin Kerouanton
2025-06-24 11:05:25 +02:00
parent 429818f969
commit aa36cc5d25
4 changed files with 90 additions and 87 deletions

View File

@@ -29,6 +29,7 @@ import (
"github.com/docker/docker/daemon/libnetwork/netutils"
"github.com/docker/docker/daemon/libnetwork/ns"
"github.com/docker/docker/daemon/libnetwork/options"
"github.com/docker/docker/daemon/libnetwork/portmapperapi"
"github.com/docker/docker/daemon/libnetwork/scope"
"github.com/docker/docker/daemon/libnetwork/types"
"github.com/docker/docker/errdefs"
@@ -138,8 +139,8 @@ type bridgeEndpoint struct {
macAddress net.HardwareAddr
containerConfig *containerConfiguration
extConnConfig *connectivityConfiguration
portMapping []portBinding // Operational port bindings
portBindingState portBindingMode // Not persisted, even on live-restore port mappings are re-created.
portMapping []portmapperapi.PortBinding // Operational port bindings
portBindingState portBindingMode // Not persisted, even on live-restore port mappings are re-created.
dbIndex uint64
dbExists bool
}
@@ -1602,14 +1603,14 @@ func (d *driver) ProgramExternalConnectivity(ctx context.Context, nid, eid strin
// port bindings that are no longer required.
//
// ep.portMapping is updated when bindings are removed.
func (ep *bridgeEndpoint) trimPortBindings(ctx context.Context, n *bridgeNetwork, pbmReq portBindingMode) (func() []portBinding, error) {
func (ep *bridgeEndpoint) trimPortBindings(ctx context.Context, n *bridgeNetwork, pbmReq portBindingMode) (func() []portmapperapi.PortBinding, error) {
// If the endpoint is the gateway for IPv4 and IPv6, there's nothing to drop.
if pbmReq.ipv4 && pbmReq.ipv6 {
return nil, nil
}
toDrop := make([]portBinding, 0, len(ep.portMapping))
toKeep := slices.DeleteFunc(ep.portMapping, func(pb portBinding) bool {
toDrop := make([]portmapperapi.PortBinding, 0, len(ep.portMapping))
toKeep := slices.DeleteFunc(ep.portMapping, func(pb portmapperapi.PortBinding) bool {
is4 := pb.HostIP.To4() != nil
if (is4 && !pbmReq.ipv4) || (!is4 && !pbmReq.ipv6) {
toDrop = append(toDrop, pb)
@@ -1633,7 +1634,7 @@ func (ep *bridgeEndpoint) trimPortBindings(ctx context.Context, n *bridgeNetwork
}
ep.portMapping = toKeep
undo := func() []portBinding {
undo := func() []portmapperapi.PortBinding {
pbReq := make([]types.PortBinding, 0, len(toDrop))
for _, pb := range toDrop {
pbReq = append(pbReq, pb.PortBinding)

View File

@@ -23,6 +23,7 @@ import (
"github.com/docker/docker/daemon/libnetwork/netutils"
"github.com/docker/docker/daemon/libnetwork/options"
"github.com/docker/docker/daemon/libnetwork/portallocator"
"github.com/docker/docker/daemon/libnetwork/portmapperapi"
"github.com/docker/docker/daemon/libnetwork/types"
"github.com/docker/docker/internal/nlwrap"
"github.com/docker/docker/internal/testutils/netnsutils"
@@ -67,7 +68,7 @@ func TestEndpointMarshalling(t *testing.T) {
},
},
},
portMapping: []portBinding{
portMapping: []portmapperapi.PortBinding{
{
PortBinding: types.PortBinding{
Proto: 17,
@@ -113,7 +114,7 @@ func TestEndpointMarshalling(t *testing.T) {
// a different port cannot be selected on live-restore if the original is
// already in-use). So, fix up portMapping in the original before running
// the comparison.
epms := make([]portBinding, len(e.portMapping))
epms := make([]portmapperapi.PortBinding, len(e.portMapping))
for i, p := range e.portMapping {
epms[i] = p
epms[i].HostPortEnd = epms[i].HostPort
@@ -209,7 +210,7 @@ func comparePortBinding(p *types.PortBinding, o *types.PortBinding) bool {
return true
}
func compareBindings(a, b []portBinding) bool {
func compareBindings(a, b []portmapperapi.PortBinding) bool {
if len(a) != len(b) {
return false
}

View File

@@ -24,42 +24,6 @@ import (
"github.com/docker/docker/daemon/libnetwork/types"
)
type portBinding struct {
types.PortBinding
// boundSocket is used to reserve a host port for the binding. If the
// userland proxy is in-use, it's passed to the proxy when the proxy is
// started, then it's closed and set to nil here.
boundSocket *os.File
// childHostIP is the host IP address, as seen from the daemon. This
// is normally the same as PortBinding.HostIP but, in rootless mode, it
// will be an address in the rootless network namespace. RootlessKit
// binds the port on the real (parent) host address and maps it to the
// same port number on the address dockerd sees in the child namespace.
// So, for example, docker-proxy and DNAT rules need to use the child
// namespace's host address. (PortBinding.HostIP isn't replaced by the
// child address, because it's stored as user-config and the child
// address may change if RootlessKit is configured differently.)
childHostIP net.IP
// portDriverRemove is a function that will inform the RootlessKit
// port driver about removal of a port binding, or nil.
portDriverRemove func() error
// stopProxy is a function to stop the userland proxy for this binding,
// if a proxy has been started - else nil.
stopProxy func() error
// rootlesskitUnsupported is set to true when the port binding is not
// supported by the port driver of RootlessKit.
rootlesskitUnsupported bool
}
// childPortBinding is pb.PortBinding, with the host address the daemon
// will see - which, in rootless mode, will be an address in the RootlessKit's
// child namespace (see portBinding.childHostIP).
func (pb portBinding) childPortBinding() types.PortBinding {
res := pb.PortBinding
res.HostIP = pb.childHostIP
return res
}
// Allow unit tests to supply a dummy StartProxy.
var startProxy = portmapper.StartProxy
@@ -68,17 +32,17 @@ var startProxy = portmapper.StartProxy
// reserve them, starts docker-proxy if required, and sets up iptables
// NAT/forwarding rules as necessary. If anything goes wrong, it will undo any
// work it's done and return an error. Otherwise, the returned slice of
// portBinding has an entry per address family (if cfg describes a mapping for
// PortBinding has an entry per address family (if cfg describes a mapping for
// 'any' host address, it's expanded into mappings for IPv4 and IPv6, because
// that's how the mapping is presented in 'inspect'). HostPort and HostPortEnd in
// each returned portBinding are set to the selected and reserved port.
// each returned PortBinding are set to the selected and reserved port.
func (n *bridgeNetwork) addPortMappings(
ctx context.Context,
ep *bridgeEndpoint,
cfg []types.PortBinding,
defHostIP net.IP,
pbmReq portBindingMode,
) (_ []portBinding, retErr error) {
) (_ []portmapperapi.PortBinding, retErr error) {
if len(defHostIP) == 0 {
defHostIP = net.IPv4zero
} else if addr4 := defHostIP.To4(); addr4 != nil {
@@ -86,7 +50,7 @@ func (n *bridgeNetwork) addPortMappings(
defHostIP = addr4
}
bindings := make([]portBinding, 0, len(cfg)*2)
bindings := make([]portmapperapi.PortBinding, 0, len(cfg)*2)
defer func() {
if retErr != nil {
if err := releasePortBindings(bindings, n.firewallerNetwork); err != nil {
@@ -118,7 +82,7 @@ func (n *bridgeNetwork) addPortMappings(
continue
}
var newB []portBinding
var newB []portmapperapi.PortBinding
var err error
if c.DisableNAT {
newB, err = setupForwardedPorts(ctx, toBind, n.firewallerNetwork)
@@ -137,24 +101,24 @@ func (n *bridgeNetwork) addPortMappings(
// Start userland proxy processes.
if proxyPath != "" {
for i := range bindings {
if bindings[i].boundSocket == nil || bindings[i].rootlesskitUnsupported || bindings[i].stopProxy != nil {
if bindings[i].BoundSocket == nil || bindings[i].RootlesskitUnsupported || bindings[i].StopProxy != nil {
continue
}
var err error
bindings[i].stopProxy, err = startProxy(
bindings[i].childPortBinding(), proxyPath, bindings[i].boundSocket,
bindings[i].StopProxy, err = startProxy(
bindings[i].ChildPortBinding(), proxyPath, bindings[i].BoundSocket,
)
if err != nil {
return nil, fmt.Errorf("failed to start userland proxy for port mapping %s: %w",
bindings[i].PortBinding, err)
}
if err := bindings[i].boundSocket.Close(); err != nil {
if err := bindings[i].BoundSocket.Close(); err != nil {
log.G(ctx).WithFields(log.Fields{
"error": err,
"mapping": bindings[i].PortBinding,
}).Warnf("failed to close proxy socket")
}
bindings[i].boundSocket = nil
bindings[i].BoundSocket = nil
}
}
@@ -258,15 +222,15 @@ func needSamePort(a, b portmapperapi.PortBindingReq) bool {
a.HostPortEnd == b.HostPortEnd
}
// mergeChildHostIPs take a slice of portBinding and returns a slice of
// mergeChildHostIPs take a slice of PortBinding and returns a slice of
// types.PortBinding, where the HostIP in each of the results has the
// value of ChildHostIP from the input (if present).
func mergeChildHostIPs(pbs []portBinding) []types.PortBinding {
func mergeChildHostIPs(pbs []portmapperapi.PortBinding) []types.PortBinding {
res := make([]types.PortBinding, 0, len(pbs))
for _, b := range pbs {
pb := b.PortBinding
if b.childHostIP != nil {
pb.HostIP = b.childHostIP
if b.ChildHostIP != nil {
pb.HostIP = b.ChildHostIP
}
res = append(res, pb)
}
@@ -399,15 +363,15 @@ func setChildHostIP(pdc portDriverClient, req portmapperapi.PortBindingReq) port
// setupForwardedPorts sets up firewall rules to allow direct remote access to
// the container's ports in cfg.
func setupForwardedPorts(ctx context.Context, cfg []portmapperapi.PortBindingReq, fwn firewaller.Network) ([]portBinding, error) {
func setupForwardedPorts(ctx context.Context, cfg []portmapperapi.PortBindingReq, fwn firewaller.Network) ([]portmapperapi.PortBinding, error) {
if len(cfg) == 0 {
return nil, nil
}
res := make([]portBinding, 0, len(cfg))
res := make([]portmapperapi.PortBinding, 0, len(cfg))
bindings := make([]types.PortBinding, 0, len(cfg))
for _, c := range cfg {
pb := portBinding{PortBinding: c.GetCopy()}
pb := portmapperapi.PortBinding{PortBinding: c.GetCopy()}
if pb.HostPort != 0 || pb.HostPortEnd != 0 {
log.G(ctx).WithFields(log.Fields{"mapping": pb}).Infof(
"Host port ignored, because NAT is disabled")
@@ -434,7 +398,7 @@ func bindHostPorts(
proxyPath string,
pdc portDriverClient,
fwn firewaller.Network,
) ([]portBinding, error) {
) ([]portmapperapi.PortBinding, error) {
if len(cfg) == 0 {
return nil, nil
}
@@ -451,7 +415,7 @@ func bindHostPorts(
// Try up to maxAllocatePortAttempts times to get a port that's not already allocated.
var err error
for i := 0; i < maxAllocatePortAttempts; i++ {
var b []portBinding
var b []portmapperapi.PortBinding
b, err = attemptBindHostPorts(ctx, cfg, proto, hostPort, hostPortEnd, proxyPath, pdc, fwn)
if err == nil {
return b, nil
@@ -475,7 +439,7 @@ func bindHostPorts(
// If the allocator doesn't have an available port in the required range, or the
// port can't be bound (perhaps because another process has already bound it),
// all resources are released and an error is returned. When ports are
// successfully reserved, a portBinding is returned for each mapping.
// successfully reserved, a PortBinding is returned for each mapping.
func attemptBindHostPorts(
ctx context.Context,
cfg []portmapperapi.PortBindingReq,
@@ -484,7 +448,7 @@ func attemptBindHostPorts(
proxyPath string,
pdc portDriverClient,
fwn firewaller.Network,
) (_ []portBinding, retErr error) {
) (_ []portmapperapi.PortBinding, retErr error) {
var err error
var port int
@@ -513,7 +477,7 @@ func attemptBindHostPorts(
return nil, types.InternalErrorf("port allocator returned %d sockets for %d port bindings", len(socks), len(cfg))
}
res := make([]portBinding, 0, len(cfg))
res := make([]portmapperapi.PortBinding, 0, len(cfg))
defer func() {
if retErr != nil {
if err := releasePortBindings(res, fwn); err != nil {
@@ -523,10 +487,10 @@ func attemptBindHostPorts(
}()
for i := range cfg {
pb := portBinding{
pb := portmapperapi.PortBinding{
PortBinding: cfg[i].PortBinding.GetCopy(),
boundSocket: socks[i],
childHostIP: cfg[i].ChildHostIP,
BoundSocket: socks[i],
ChildHostIP: cfg[i].ChildHostIP,
}
pb.PortBinding.HostPort = uint16(port)
pb.PortBinding.HostPortEnd = pb.HostPort
@@ -556,7 +520,7 @@ func attemptBindHostPorts(
// configPortDriver passes the port binding's details to rootlesskit, and updates the
// port binding with callbacks to remove the rootlesskit config (or marks the binding as
// unsupported by rootlesskit).
func configPortDriver(ctx context.Context, pbs []portBinding, pdc portDriverClient) error {
func configPortDriver(ctx context.Context, pbs []portmapperapi.PortBinding, pdc portDriverClient) error {
for i := range pbs {
b := pbs[i]
if pdc != nil && b.HostPort != 0 {
@@ -565,18 +529,18 @@ func configPortDriver(ctx context.Context, pbs []portBinding, pdc portDriverClie
if !ok {
return fmt.Errorf("invalid host IP address in %s", b)
}
chip, ok := netip.AddrFromSlice(b.childHostIP)
chip, ok := netip.AddrFromSlice(b.ChildHostIP)
if !ok {
return fmt.Errorf("invalid child host IP address %s in %s", b.childHostIP, b)
return fmt.Errorf("invalid child host IP address %s in %s", b.ChildHostIP, b)
}
pbs[i].portDriverRemove, err = pdc.AddPort(ctx, b.Proto.String(), hip, chip, int(b.HostPort))
pbs[i].PortDriverRemove, err = pdc.AddPort(ctx, b.Proto.String(), hip, chip, int(b.HostPort))
if err != nil {
var pErr *rlkclient.ProtocolUnsupportedError
if errors.As(err, &pErr) {
log.G(ctx).WithFields(log.Fields{
"error": pErr,
}).Warnf("discarding request for %q", net.JoinHostPort(hip.String(), strconv.Itoa(int(b.HostPort))))
pbs[i].rootlesskitUnsupported = true
pbs[i].RootlesskitUnsupported = true
continue
}
return err
@@ -586,12 +550,12 @@ func configPortDriver(ctx context.Context, pbs []portBinding, pdc portDriverClie
return nil
}
func listenBoundPorts(pbs []portBinding, proxyPath string) error {
func listenBoundPorts(pbs []portmapperapi.PortBinding, proxyPath string) error {
for i := range pbs {
if pbs[i].boundSocket == nil || pbs[i].rootlesskitUnsupported || pbs[i].Proto == types.UDP {
if pbs[i].BoundSocket == nil || pbs[i].RootlesskitUnsupported || pbs[i].Proto == types.UDP {
continue
}
rc, err := pbs[i].boundSocket.SyscallConn()
rc, err := pbs[i].BoundSocket.SyscallConn()
if err != nil {
return fmt.Errorf("raw conn not available on %s socket: %w", pbs[i].Proto, err)
}
@@ -623,21 +587,21 @@ func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error {
return releasePortBindings(pbs, n.firewallerNetwork)
}
func releasePortBindings(pbs []portBinding, fwn firewaller.Network) error {
func releasePortBindings(pbs []portmapperapi.PortBinding, fwn firewaller.Network) error {
var errs []error
for _, pb := range pbs {
if pb.boundSocket != nil {
if err := pb.boundSocket.Close(); err != nil {
if pb.BoundSocket != nil {
if err := pb.BoundSocket.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close socket for port mapping %s: %w", pb, err))
}
}
if pb.portDriverRemove != nil {
if err := pb.portDriverRemove(); err != nil {
if pb.PortDriverRemove != nil {
if err := pb.PortDriverRemove(); err != nil {
errs = append(errs, err)
}
}
if pb.stopProxy != nil {
if err := pb.stopProxy(); err != nil && !errors.Is(err, os.ErrProcessDone) {
if pb.StopProxy != nil {
if err := pb.StopProxy(); err != nil && !errors.Is(err, os.ErrProcessDone) {
errs = append(errs, fmt.Errorf("failed to stop userland proxy for port mapping %s: %w", pb, err))
}
}
@@ -647,7 +611,7 @@ func releasePortBindings(pbs []portBinding, fwn firewaller.Network) error {
}
for _, pb := range pbs {
if pb.HostPort > 0 {
portallocator.Get().ReleasePort(pb.childHostIP, pb.Proto.String(), int(pb.HostPort))
portallocator.Get().ReleasePort(pb.ChildHostIP, pb.Proto.String(), int(pb.HostPort))
}
}
return errors.Join(errs...)
@@ -655,7 +619,7 @@ func releasePortBindings(pbs []portBinding, fwn firewaller.Network) error {
func (n *bridgeNetwork) reapplyPerPortIptables() {
n.Lock()
var allPBs []portBinding
var allPBs []portmapperapi.PortBinding
for _, ep := range n.endpoints {
allPBs = append(allPBs, ep.portMapping...)
}

View File

@@ -3,6 +3,7 @@ package portmapperapi
import (
"net"
"net/netip"
"os"
"github.com/docker/docker/daemon/libnetwork/types"
)
@@ -67,3 +68,39 @@ func (pbReq PortBindingReq) Compare(other PortBindingReq) int {
bIP, _ := netip.AddrFromSlice(other.IP)
return aIP.Unmap().Compare(bIP.Unmap())
}
type PortBinding struct {
types.PortBinding
// BoundSocket is used to reserve a host port for the binding. If the
// userland proxy is in-use, it's passed to the proxy when the proxy is
// started, then it's closed and set to nil here.
BoundSocket *os.File `json:"-"`
// ChildHostIP is the host IP address, as seen from the daemon. This
// is normally the same as PortBinding.HostIP but, in rootless mode, it
// will be an address in the rootless network namespace. RootlessKit
// binds the port on the real (parent) host address and maps it to the
// same port number on the address dockerd sees in the child namespace.
// So, for example, docker-proxy and DNAT rules need to use the child
// namespace's host address. (PortBinding.HostIP isn't replaced by the
// child address, because it's stored as user-config and the child
// address may change if RootlessKit is configured differently.)
ChildHostIP net.IP `json:"-"`
// PortDriverRemove is a function that will inform the RootlessKit
// port driver about removal of a port binding, or nil.
PortDriverRemove func() error `json:"-"`
// StopProxy is a function to stop the userland proxy for this binding,
// if a proxy has been started - else nil.
StopProxy func() error `json:"-"`
// RootlesskitUnsupported is set to true when the port binding is not
// supported by the port driver of RootlessKit.
RootlesskitUnsupported bool `json:"-"`
}
// ChildPortBinding is pb.PortBinding, with the host address the daemon
// will see - which, in rootless mode, will be an address in the RootlessKit's
// child namespace (see PortBinding.ChildHostIP).
func (pb PortBinding) ChildPortBinding() types.PortBinding {
res := pb.PortBinding
res.HostIP = pb.ChildHostIP
return res
}