Tell RootlessKit about docker-proxy port mappings

Before this change, when running rootless, instead of running
docker-proxy the daemon would run rootlesskit-docker-proxy.

The job of rootlesskit-docker-proxy was to tell RootlessKit
about mapped host ports before starting docker-proxy, and then
to remove the mapping when it was stopped.

So, rootlesskit-docker-proxy would need to be kept in-step
with changes to docker-proxy (particuarly the upcoming change
to bind TCP/UDP ports in the daemon and pass them to the proxy,
but also possible-future changes like running proxy per-container
rather than per-port-mapping).

This change runs the docker-proxy in rootless mode, instead of
rootlesskit-docker-proxy, and the daemon itself tells RootlessKit
about changes in host port mappings.

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2024-07-10 16:16:03 +01:00
parent 384ca56d90
commit f1e0746c08
6 changed files with 368 additions and 39 deletions

View File

@@ -34,7 +34,6 @@ const (
StockRuntimeName = "runc"
// userlandProxyBinary is the name of the userland-proxy binary.
// In rootless-mode, [rootless.RootlessKitDockerProxyBinary] is used instead.
userlandProxyBinary = "docker-proxy"
)
@@ -234,16 +233,25 @@ func setPlatformDefaults(cfg *Config) error {
cfg.CgroupNamespaceMode = string(DefaultCgroupNamespaceMode)
}
var err error
cfg.BridgeConfig.UserlandProxyPath, err = lookupBinPath(userlandProxyBinary)
if err != nil {
// Log, but don't error here. This allows running a daemon with
// userland-proxy disabled (which does not require the binary
// to be present).
//
// An error is still produced by [Config.ValidatePlatformConfig] if
// userland-proxy is enabled in the configuration.
//
// We log this at "debug" level, as this code is also executed
// when running "--version", and we don't want to print logs in
// that case..
log.G(context.TODO()).WithError(err).Debug("failed to lookup default userland-proxy binary")
}
if rootless.RunningWithRootlessKit() {
cfg.Rootless = true
var err error
// use rootlesskit-docker-proxy for exposing the ports in RootlessKit netns to the initial namespace.
cfg.BridgeConfig.UserlandProxyPath, err = lookupBinPath(rootless.RootlessKitDockerProxyBinary)
if err != nil {
return errors.Wrapf(err, "running with RootlessKit, but %s not installed", rootless.RootlessKitDockerProxyBinary)
}
dataHome, err := homedir.GetDataHome()
if err != nil {
return err
@@ -257,21 +265,6 @@ func setPlatformDefaults(cfg *Config) error {
cfg.ExecRoot = filepath.Join(runtimeDir, "docker")
cfg.Pidfile = filepath.Join(runtimeDir, "docker.pid")
} else {
var err error
cfg.BridgeConfig.UserlandProxyPath, err = lookupBinPath(userlandProxyBinary)
if err != nil {
// Log, but don't error here. This allows running a daemon with
// userland-proxy disabled (which does not require the binary
// to be present).
//
// An error is still produced by [Config.ValidatePlatformConfig] if
// userland-proxy is enabled in the configuration.
//
// We log this at "debug" level, as this code is also executed
// when running "--version", and we don't want to print logs in
// that case..
log.G(context.TODO()).WithError(err).Debug("failed to lookup default userland-proxy binary")
}
cfg.Root = "/var/lib/docker"
cfg.ExecRoot = "/var/run/docker"
cfg.Pidfile = "/var/run/docker.pid"

View File

@@ -915,6 +915,7 @@ func driverOptions(config *config.Config) nwconfig.Option {
"EnableIP6Tables": config.BridgeConfig.EnableIP6Tables,
"EnableUserlandProxy": config.BridgeConfig.EnableUserlandProxy,
"UserlandProxyPath": config.BridgeConfig.UserlandProxyPath,
"Rootless": config.Rootless,
},
})
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/docker/docker/errdefs"
"github.com/docker/docker/libnetwork/datastore"
"github.com/docker/docker/libnetwork/driverapi"
"github.com/docker/docker/libnetwork/drivers/bridge/rlkclient"
"github.com/docker/docker/libnetwork/internal/netiputil"
"github.com/docker/docker/libnetwork/iptables"
"github.com/docker/docker/libnetwork/netlabel"
@@ -56,6 +57,7 @@ type configuration struct {
EnableIP6Tables bool
EnableUserlandProxy bool
UserlandProxyPath string
Rootless bool
}
// networkConfiguration for network specific configuration
@@ -131,6 +133,14 @@ type bridgeNetwork struct {
sync.Mutex
}
type portDriverClient interface {
ChildHostIP(hostIP netip.Addr) netip.Addr
AddPort(ctx context.Context, proto string, hostIP, childIP netip.Addr, hostPort int) (func() error, error)
}
// Allow unit tests to supply a dummy RootlessKit port driver client.
var newPortDriverClient = func() (portDriverClient, error) { return rlkclient.NewPortDriverClient() }
type driver struct {
config configuration
natChain *iptables.ChainInfo
@@ -144,6 +154,7 @@ type driver struct {
networks map[string]*bridgeNetwork
store *datastore.Store
nlh *netlink.Handle
portDriverClient portDriverClient
configNetwork sync.Mutex
sync.Mutex
}
@@ -414,6 +425,15 @@ func (n *bridgeNetwork) userlandProxyPath() string {
return n.driver.userlandProxyPath()
}
func (n *bridgeNetwork) getPortDriverClient() portDriverClient {
n.Lock()
defer n.Unlock()
if n.driver == nil {
return nil
}
return n.driver.getPortDriverClient()
}
func (n *bridgeNetwork) getEndpoint(eid string) (*bridgeEndpoint, error) {
if eid == "" {
return nil, InvalidEndpointIDError(eid)
@@ -465,6 +485,7 @@ func (d *driver) configure(option map[string]interface{}) error {
filterChainV6 *iptables.ChainInfo
isolationChain1V6 *iptables.ChainInfo
isolationChain2V6 *iptables.ChainInfo
pdc portDriverClient
)
switch opt := option[netlabel.GenericData].(type) {
@@ -537,6 +558,14 @@ func (d *driver) configure(option map[string]interface{}) error {
}
}
if config.EnableUserlandProxy && config.Rootless {
var err error
pdc, err = newPortDriverClient()
if err != nil {
return err
}
}
d.Lock()
d.natChain = natChain
d.filterChain = filterChain
@@ -546,6 +575,7 @@ func (d *driver) configure(option map[string]interface{}) error {
d.filterChainV6 = filterChainV6
d.isolationChain1V6 = isolationChain1V6
d.isolationChain2V6 = isolationChain2V6
d.portDriverClient = pdc
d.config = config
d.Unlock()
@@ -577,6 +607,12 @@ func (d *driver) userlandProxyPath() string {
return ""
}
func (d *driver) getPortDriverClient() portDriverClient {
d.Lock()
defer d.Unlock()
return d.portDriverClient
}
func parseNetworkGenericOptions(data interface{}) (*networkConfiguration, error) {
var (
err error

View File

@@ -23,12 +23,28 @@ import (
type portBinding struct {
types.PortBinding
// 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
}
type portBindingReq struct {
types.PortBinding
disableNAT bool
childHostIP net.IP
disableNAT bool
}
// addPortMappings takes cfg, the configuration for port mappings, selects host
@@ -79,6 +95,7 @@ func (n *bridgeNetwork) addPortMappings(
sortAndNormPBs(sortedCfg)
proxyPath := n.userlandProxyPath()
pdc := n.getPortDriverClient()
// toBind accumulates port bindings that should be allocated the same host port
// (if required by NAT config). If the host address is unspecified, and defHostIP
@@ -91,7 +108,7 @@ func (n *bridgeNetwork) addPortMappings(
// bindings to collect, they're applied and toBind is reset.
var toBind []portBindingReq
for i, c := range sortedCfg {
if bindingIPv4, ok := configurePortBindingIPv4(disableNAT4, c, containerIPv4, defHostIP); ok {
if bindingIPv4, ok := configurePortBindingIPv4(pdc, disableNAT4, c, containerIPv4, defHostIP); ok {
toBind = append(toBind, bindingIPv4)
}
@@ -107,7 +124,7 @@ func (n *bridgeNetwork) addPortMappings(
if proxyPath != "" && (containerIPv6 == nil) {
containerIP = containerIPv4
}
if bindingIPv6, ok := configurePortBindingIPv6(disableNAT6, c, containerIP, defHostIP); ok {
if bindingIPv6, ok := configurePortBindingIPv6(pdc, disableNAT6, c, containerIP, defHostIP); ok {
toBind = append(toBind, bindingIPv6)
}
@@ -129,8 +146,24 @@ func (n *bridgeNetwork) addPortMappings(
toBind = toBind[:0]
}
for _, b := range bindings {
if err := n.setPerPortIptables(b, true); err != nil {
for i := range bindings {
if pdc != nil && bindings[i].HostPort != 0 {
var err error
b := &bindings[i]
hip, ok := netip.AddrFromSlice(b.HostIP)
if !ok {
return nil, fmt.Errorf("invalid host IP address in %s", b)
}
chip, ok := netip.AddrFromSlice(b.childHostIP)
if !ok {
return nil, fmt.Errorf("invalid child host IP address %s in %s", b.childHostIP, b)
}
b.portDriverRemove, err = pdc.AddPort(context.TODO(), b.Proto.String(), hip, chip, int(b.HostPort))
if err != nil {
return nil, err
}
}
if err := n.setPerPortIptables(bindings[i], true); err != nil {
return nil, err
}
}
@@ -263,7 +296,7 @@ func needSamePort(a, b types.PortBinding) bool {
// configurePortBindingIPv4 returns a new port binding with the HostIP field populated
// if a binding is required, else nil.
func configurePortBindingIPv4(disableNAT bool, bnd types.PortBinding, containerIPv4, defHostIP net.IP) (portBindingReq, bool) {
func configurePortBindingIPv4(pdc portDriverClient, disableNAT bool, bnd types.PortBinding, containerIPv4, defHostIP net.IP) (portBindingReq, bool) {
if len(containerIPv4) == 0 {
return portBindingReq{}, false
}
@@ -282,15 +315,15 @@ func configurePortBindingIPv4(disableNAT bool, bnd types.PortBinding, containerI
// Unmap the addresses if they're IPv4-mapped IPv6.
bnd.HostIP = bnd.HostIP.To4()
bnd.IP = containerIPv4.To4()
return portBindingReq{
return setChildHostIP(pdc, portBindingReq{
PortBinding: bnd,
disableNAT: disableNAT,
}, true
}), true
}
// configurePortBindingIPv6 returns a new port binding with the HostIP field populated
// if a binding is required, else nil.
func configurePortBindingIPv6(disableNAT bool, bnd types.PortBinding, containerIP, defHostIP net.IP) (portBindingReq, bool) {
func configurePortBindingIPv6(pdc portDriverClient, disableNAT bool, bnd types.PortBinding, containerIP, defHostIP net.IP) (portBindingReq, bool) {
if containerIP == nil {
return portBindingReq{}, false
}
@@ -317,10 +350,20 @@ func configurePortBindingIPv6(disableNAT bool, bnd types.PortBinding, containerI
}
}
bnd.IP = containerIP
return portBindingReq{
return setChildHostIP(pdc, portBindingReq{
PortBinding: bnd,
disableNAT: disableNAT,
}, true
}), true
}
func setChildHostIP(pdc portDriverClient, req portBindingReq) portBindingReq {
if pdc == nil {
req.childHostIP = req.HostIP
return req
}
hip, _ := netip.AddrFromSlice(req.HostIP)
req.childHostIP = pdc.ChildHostIP(hip).AsSlice()
return req
}
// bindHostPorts allocates ports and starts docker-proxy for the given cfg. The
@@ -410,7 +453,7 @@ func attemptBindHostPorts(
if c.disableNAT {
pb.HostPort = 0
} else {
pb.stopProxy, err = startProxy(c.Proto.String(), c.HostIP, port, c.IP, int(c.Port), proxyPath)
pb.stopProxy, err = startProxy(c.Proto.String(), c.childHostIP, port, c.IP, int(c.Port), proxyPath)
if err != nil {
return nil, fmt.Errorf("failed to bind port %s:%d/%s: %w", c.HostIP, port, c.Proto, err)
}
@@ -424,6 +467,7 @@ func attemptBindHostPorts(
pb.HostPort = uint16(port)
}
pb.HostPortEnd = pb.HostPort
pb.childHostIP = c.childHostIP
res = append(res, pb)
}
return res, nil
@@ -442,7 +486,10 @@ func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error {
func (n *bridgeNetwork) releasePortBindings(pbs []portBinding) error {
var errs []error
for _, pb := range pbs {
var errP error
var errPD, errP error
if pb.portDriverRemove != nil {
errPD = pb.portDriverRemove()
}
if pb.stopProxy != nil {
errP = pb.stopProxy()
if errP != nil {
@@ -456,7 +503,7 @@ func (n *bridgeNetwork) releasePortBindings(pbs []portBinding) error {
if pb.HostPort > 0 {
portallocator.Get().ReleasePort(pb.HostIP, pb.Proto.String(), int(pb.HostPort))
}
errs = append(errs, errP, errN)
errs = append(errs, errPD, errP, errN)
}
return errors.Join(errs...)
}

View File

@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"testing"
@@ -420,6 +422,7 @@ func TestAddPortMappings(t *testing.T) {
defHostIP net.IP
proxyPath string
busyPortIPv4 int
rootless bool
expErr string
expPBs []types.PortBinding
@@ -720,6 +723,23 @@ func TestAddPortMappings(t *testing.T) {
{Proto: types.TCP, IP: ctrIP6.IP, Port: 12345, HostIP: net.IPv6zero, HostPort: 12346},
},
},
{
name: "rootless",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{
{Proto: types.TCP, Port: 22},
{Proto: types.TCP, Port: 80},
},
proxyPath: "/dummy/path/to/proxy",
rootless: true,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort + 1},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort + 1},
},
},
}
for _, tc := range testcases {
@@ -756,6 +776,11 @@ func TestAddPortMappings(t *testing.T) {
}, nil
}
// Mock the RootlessKit port driver.
origNewPortDriverClient := newPortDriverClient
defer func() { newPortDriverClient = origNewPortDriverClient }()
newPortDriverClient = func() (portDriverClient, error) { return newMockPortDriverClient() }
n := &bridgeNetwork{
config: &networkConfiguration{
BridgeName: "dummybridge",
@@ -771,11 +796,23 @@ func TestAddPortMappings(t *testing.T) {
EnableIP6Tables: true,
EnableUserlandProxy: tc.proxyPath != "",
UserlandProxyPath: tc.proxyPath,
Rootless: tc.rootless,
},
}
err := n.driver.configure(genericOption)
assert.NilError(t, err)
assert.Check(t, is.Equal(n.driver.portDriverClient == nil, !tc.rootless))
expChildIP := func(hostIP net.IP) net.IP {
if !tc.rootless {
return hostIP
}
if hostIP.To4() == nil {
return net.ParseIP("::1")
}
return net.ParseIP("127.0.0.1")
}
err = portallocator.Get().ReleaseAll()
assert.NilError(t, err)
@@ -852,16 +889,37 @@ func TestAddPortMappings(t *testing.T) {
// Check a docker-proxy was started and stopped for each expected port binding.
expProxies := map[proxyCall]bool{}
for _, expPB := range tc.expPBs {
is4 := expPB.HostIP.To4() != nil
hip := expChildIP(expPB.HostIP)
is4 := hip.To4() != nil
if (is4 && tc.gwMode4.natDisabled()) || (!is4 && tc.gwMode6.natDisabled()) {
continue
}
p := newProxyCall(expPB.Proto.String(),
expPB.HostIP, int(expPB.HostPort),
hip, int(expPB.HostPort),
expPB.IP, int(expPB.Port), tc.proxyPath)
expProxies[p] = tc.expReleaseErr != ""
}
assert.Check(t, is.DeepEqual(expProxies, proxies))
// Check the port driver has seen the expected port mappings and no others,
// and that they have all been closed.
if n.driver.portDriverClient != nil {
pdc := n.driver.portDriverClient.(*mockPortDriverClient)
expPorts := map[mockPortDriverPort]bool{}
for _, expPB := range tc.expPBs {
if expPB.HostPort == 0 {
continue
}
pdp := mockPortDriverPort{
proto: expPB.Proto.String(),
hostIP: expPB.HostIP.String(),
childIP: expChildIP(expPB.HostIP).String(),
hostPort: int(expPB.HostPort),
}
expPorts[pdp] = false
}
assert.Check(t, is.DeepEqual(pdc.openPorts, expPorts))
}
})
}
}
@@ -881,3 +939,48 @@ func newProxyCall(proto string,
proxyPath: proxyPath,
}
}
// Types for tracking calls to the port driver client (mock for RootlessKit client).
type mockPortDriverPort struct {
proto string
hostIP string
childIP string
hostPort int
}
func (p mockPortDriverPort) String() string {
return p.hostIP + ":" + strconv.Itoa(p.hostPort) + "/" + p.proto
}
type mockPortDriverClient struct {
openPorts map[mockPortDriverPort]bool
}
func newMockPortDriverClient() (*mockPortDriverClient, error) {
return &mockPortDriverClient{
openPorts: map[mockPortDriverPort]bool{},
}, nil
}
func (c *mockPortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr {
if hostIP.Is6() {
return netip.IPv6Loopback()
}
return netip.MustParseAddr("127.0.0.1")
}
func (c *mockPortDriverClient) AddPort(_ context.Context, proto string, hostIP, childIP netip.Addr, hostPort int) (func() error, error) {
key := mockPortDriverPort{proto: proto, hostIP: hostIP.String(), childIP: childIP.String(), hostPort: hostPort}
if _, exists := c.openPorts[key]; exists {
return nil, fmt.Errorf("mockPortDriverClient: port %s is already open", key)
}
c.openPorts[key] = true
return func() error {
if !c.openPorts[key] {
return fmt.Errorf("mockPortDriverClient: port %s is not open", key)
}
c.openPorts[key] = false
return nil
}, nil
}

View File

@@ -0,0 +1,149 @@
// RootlessKit integration - if required by RootlessKit's port driver, let it know
// about port mappings as they're added and removed.
//
// This is based on / copied from rootlesskit-docker-proxy, which was previously
// installed as a proxy for docker-proxy:
// https://github.com/rootless-containers/rootlesskit/blob/4fb2e2cb80bf13eb28b7f2a4317b63406b89ad32/cmd/rootlesskit-docker-proxy/main.go
package rlkclient
import (
"context"
"fmt"
"net"
"net/netip"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/rootless-containers/rootlesskit/v2/pkg/api/client"
"github.com/rootless-containers/rootlesskit/v2/pkg/port"
)
type PortDriverClient struct {
client client.Client
portDriverName string
protos map[string]struct{}
childIP netip.Addr
}
func NewPortDriverClient() (*PortDriverClient, error) {
stateDir := os.Getenv("ROOTLESSKIT_STATE_DIR")
if stateDir == "" {
return nil, errors.New("$ROOTLESSKIT_STATE_DIR needs to be set")
}
socketPath := filepath.Join(stateDir, "api.sock")
c, err := client.New(socketPath)
if err != nil {
return nil, fmt.Errorf("error while connecting to RootlessKit API socket: %w", err)
}
info, err := c.Info(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to call info API, probably RootlessKit binary is too old (needs to be v0.14.0 or later): %w", err)
}
// info.PortDriver is currently nil for "none" and "implicit", but this may change in future
if info.PortDriver == nil || info.PortDriver.Driver == "none" || info.PortDriver.Driver == "implicit" {
return nil, nil
}
pdc := &PortDriverClient{
client: c,
portDriverName: info.PortDriver.Driver,
}
if info.PortDriver.DisallowLoopbackChildIP {
// i.e., port-driver="slirp4netns"
if info.NetworkDriver.ChildIP == nil {
return nil, fmt.Errorf("RootlessKit port driver (%q) does not allow loopback child IP, but network driver (%q) has no non-loopback IP",
info.PortDriver.Driver, info.NetworkDriver.Driver)
}
childIP, ok := netip.AddrFromSlice(info.NetworkDriver.ChildIP)
if !ok {
return nil, fmt.Errorf("unable to use child IP %s from network driver (%q)",
info.NetworkDriver.ChildIP, info.NetworkDriver.Driver)
}
pdc.childIP = childIP
}
pdc.protos = make(map[string]struct{}, len(info.PortDriver.Protos))
for _, p := range info.PortDriver.Protos {
pdc.protos[p] = struct{}{}
}
return pdc, nil
}
// ChildHostIP returns the address that must be used in the child network
// namespace in place of hostIP, a host IP address. In particular, port
// mappings from host IP addresses, and DNAT rules, must use this child
// address in place of the real host address.
func (c *PortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr {
if c.childIP.IsValid() {
return c.childIP
}
if hostIP.Is6() {
return netip.IPv6Loopback()
}
return netip.MustParseAddr("127.0.0.1")
}
// AddPort makes a request to RootlessKit asking it to set up a port
// mapping between a host IP address and a child host IP address.
func (c *PortDriverClient) AddPort(
ctx context.Context,
proto string,
hostIP netip.Addr,
childIP netip.Addr,
hostPort int,
) (func() error, error) { // proto is like "tcp", but we need to convert it to "tcp4" or "tcp6" explicitly
// for libnetwork >= 20201216
//
// See https://github.com/moby/libnetwork/pull/2604/files#diff-8fa48beed55dd033bf8e4f8c40b31cf69d0b2cc5d4bb53cde8594670ea6c938aR20
// See also https://github.com/rootless-containers/rootlesskit/issues/231
apiProto := proto
if !strings.HasSuffix(apiProto, "4") && !strings.HasSuffix(apiProto, "6") {
if hostIP.Is6() {
apiProto += "6"
} else {
apiProto += "4"
}
}
if _, ok := c.protos[apiProto]; !ok {
// This happens when apiProto="tcp6", portDriverName="slirp4netns",
// because "slirp4netns" port driver does not support listening on IPv6 yet.
//
// Note that "slirp4netns" port driver is not used by default,
// even when network driver is set to "slirp4netns".
//
// Most users are using "builtin" port driver and will not see this warning.
return nil, fmt.Errorf("protocol %q is not supported by the RootlessKit port driver %q, discarding request for %q",
proto,
c.portDriverName,
net.JoinHostPort(hostIP.String(), strconv.Itoa(hostPort)))
}
pm := c.client.PortManager()
p := port.Spec{
Proto: apiProto,
ParentIP: hostIP.String(),
ParentPort: hostPort,
ChildIP: childIP.String(),
ChildPort: hostPort,
}
st, err := pm.AddPort(ctx, p)
if err != nil {
return nil, fmt.Errorf("error while calling RootlessKit PortManager.AddPort(): %w", err)
}
deferFunc := func() error {
if dErr := pm.RemovePort(ctx, st.ID); dErr != nil {
return fmt.Errorf("error while calling RootlessKit PortManager.RemovePort(): %w", err)
}
return nil
}
return deferFunc, nil
}