mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
149
libnetwork/drivers/bridge/rlkclient/rootlesskit_client_linux.go
Normal file
149
libnetwork/drivers/bridge/rlkclient/rootlesskit_client_linux.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user