diff --git a/cmd/podman/system/service_abi.go b/cmd/podman/system/service_abi.go index e08d3b9558..5c57a5df2c 100644 --- a/cmd/podman/system/service_abi.go +++ b/cmd/podman/system/service_abi.go @@ -12,12 +12,12 @@ import ( "path/filepath" "github.com/containers/common/pkg/cgroups" + "github.com/containers/common/pkg/servicereaper" "github.com/containers/podman/v4/cmd/podman/registry" api "github.com/containers/podman/v4/pkg/api/server" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/domain/infra" "github.com/containers/podman/v4/pkg/rootless" - "github.com/containers/podman/v4/pkg/servicereaper" "github.com/containers/podman/v4/utils" "github.com/coreos/go-systemd/v22/activation" "github.com/sirupsen/logrus" diff --git a/cmd/rootlessport/main.go b/cmd/rootlessport/main.go index 2508eb1c28..574d808cfe 100644 --- a/cmd/rootlessport/main.go +++ b/cmd/rootlessport/main.go @@ -17,7 +17,7 @@ import ( "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/common/libnetwork/types" - "github.com/containers/podman/v4/pkg/rootlessport" + "github.com/containers/common/pkg/rootlessport" rkport "github.com/rootless-containers/rootlesskit/pkg/port" rkbuiltin "github.com/rootless-containers/rootlesskit/pkg/port/builtin" rkportutil "github.com/rootless-containers/rootlesskit/pkg/port/portutil" diff --git a/go.mod b/go.mod index e6920c6122..c3125f9ec6 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/containernetworking/cni v1.1.2 github.com/containernetworking/plugins v1.3.0 github.com/containers/buildah v1.30.1-0.20230504052500-e925b5852e07 - github.com/containers/common v0.53.1-0.20230621115248-a2cd3ea30337 + github.com/containers/common v0.53.1-0.20230621174116-586a3be4e1fc github.com/containers/conmon v2.0.20+incompatible github.com/containers/image/v5 v5.25.1-0.20230613183705-07ced6137083 github.com/containers/libhvee v0.0.5 @@ -30,7 +30,6 @@ require ( github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 github.com/docker/go-plugins-helpers v0.0.0-20211224144127-6eecb7beb651 github.com/docker/go-units v0.5.0 - github.com/fsnotify/fsnotify v1.6.0 github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 github.com/google/gofuzz v1.2.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 @@ -93,6 +92,7 @@ require ( github.com/disiqueira/gotree/v3 v3.0.2 // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsouza/go-dockerclient v1.9.7 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-logr/logr v1.2.4 // indirect diff --git a/go.sum b/go.sum index 035f51d5ab..f10266a4ad 100644 --- a/go.sum +++ b/go.sum @@ -239,8 +239,8 @@ github.com/containernetworking/plugins v1.3.0 h1:QVNXMT6XloyMUoO2wUOqWTC1hWFV62Q github.com/containernetworking/plugins v1.3.0/go.mod h1:Pc2wcedTQQCVuROOOaLBPPxrEXqqXBFt3cZ+/yVg6l0= github.com/containers/buildah v1.30.1-0.20230504052500-e925b5852e07 h1:Bs2sNFh/fSYr4J6JJLFqzyn3dp6HhlA6ewFwRYUpeIE= github.com/containers/buildah v1.30.1-0.20230504052500-e925b5852e07/go.mod h1:6A/BK0YJLXL8+AqlbceKJrhUT+NtEgsvAc51F7TAllc= -github.com/containers/common v0.53.1-0.20230621115248-a2cd3ea30337 h1:Z9wxp08tzCKgI3ziVwpoMyQcDKH8z9VmgyeHJcnunj4= -github.com/containers/common v0.53.1-0.20230621115248-a2cd3ea30337/go.mod h1:qE1MzGl69IoK7ZNCCH51+aLVjyQtnH0LiZe0wG32Jy0= +github.com/containers/common v0.53.1-0.20230621174116-586a3be4e1fc h1:6yxDNgJGrddAWKeeAH7m0GUzCFRuvc2BqXund52Ui7k= +github.com/containers/common v0.53.1-0.20230621174116-586a3be4e1fc/go.mod h1:qE1MzGl69IoK7ZNCCH51+aLVjyQtnH0LiZe0wG32Jy0= github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg= github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I= github.com/containers/image/v5 v5.25.1-0.20230613183705-07ced6137083 h1:6Pbnll97ls6G0U3DSxaTqp7Sd8Fykc4gd7BUJm7Bpn8= diff --git a/libpod/container_exec.go b/libpod/container_exec.go index 451f221855..e20274f34f 100644 --- a/libpod/container_exec.go +++ b/libpod/container_exec.go @@ -11,6 +11,7 @@ import ( "time" "github.com/containers/common/pkg/resize" + "github.com/containers/common/pkg/util" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/libpod/events" "github.com/containers/storage/pkg/stringid" @@ -923,7 +924,7 @@ func (c *Container) readExecExitCode(sessionID string) (int, error) { chWait := make(chan error) defer close(chWait) - _, err := WaitForFile(exitFile, chWait, time.Second*5) + _, err := util.WaitForFile(exitFile, chWait, time.Second*5) if err != nil { return -1, err } diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 487246e7c0..136902d45a 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -153,7 +153,7 @@ func (c *Container) waitForExitFileAndSync() error { chWait := make(chan error) defer close(chWait) - _, err = WaitForFile(exitFile, chWait, time.Second*5) + _, err = cutil.WaitForFile(exitFile, chWait, time.Second*5) if err != nil { // Exit file did not appear // Reset our state diff --git a/libpod/container_internal_common.go b/libpod/container_internal_common.go index 198cad1f8c..a389abe4eb 100644 --- a/libpod/container_internal_common.go +++ b/libpod/container_internal_common.go @@ -2066,10 +2066,7 @@ func (c *Container) addResolvConf() error { } } - ipv6, err := c.checkForIPv6(netStatus) - if err != nil { - return err - } + ipv6 := c.checkForIPv6(netStatus) networkBackend := c.runtime.config.Network.NetworkBackend nameservers := make([]string, 0, len(c.runtime.config.Containers.DNSServers)+len(c.config.DNSServer)) @@ -2137,13 +2134,13 @@ func (c *Container) addResolvConf() error { } // Check if a container uses IPv6. -func (c *Container) checkForIPv6(netStatus map[string]types.StatusBlock) (bool, error) { +func (c *Container) checkForIPv6(netStatus map[string]types.StatusBlock) bool { for _, status := range netStatus { for _, netInt := range status.Interfaces { for _, netAddress := range netInt.Subnets { // Note: only using To16() does not work since it also returns a valid ip for ipv4 if netAddress.IPNet.IP.To4() == nil && netAddress.IPNet.IP.To16() != nil { - return true, nil + return true } } } @@ -2207,7 +2204,7 @@ func (c *Container) getHostsEntries() (etchosts.HostEntries, error) { case c.config.NetMode.IsBridge(): entries = etchosts.GetNetworkHostEntries(c.state.NetworkStatus, names...) case c.config.NetMode.IsSlirp4netns(): - ip, err := GetSlirp4netnsIP(c.slirp4netnsSubnet) + ip, err := getSlirp4netnsIP(c.slirp4netnsSubnet) if err != nil { return nil, err } diff --git a/libpod/container_internal_freebsd.go b/libpod/container_internal_freebsd.go index de3a0f63f6..5f4538cc0a 100644 --- a/libpod/container_internal_freebsd.go +++ b/libpod/container_internal_freebsd.go @@ -281,8 +281,8 @@ func (c *Container) addSlirp4netnsDNS(nameservers []string) []string { return nameservers } -func (c *Container) isSlirp4netnsIPv6() (bool, error) { - return false, nil +func (c *Container) isSlirp4netnsIPv6() bool { + return false } // check for net=none diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 22efb94f76..08e4df12fd 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -15,6 +15,7 @@ import ( "syscall" "time" + "github.com/containers/common/libnetwork/slirp4netns" "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/cgroups" "github.com/containers/common/pkg/config" @@ -621,7 +622,7 @@ func (c *Container) setCgroupsPath(g *generate.Generator) error { func (c *Container) addSlirp4netnsDNS(nameservers []string) []string { // slirp4netns has a built in DNS forwarder. if c.config.NetMode.IsSlirp4netns() { - slirp4netnsDNS, err := GetSlirp4netnsDNS(c.slirp4netnsSubnet) + slirp4netnsDNS, err := slirp4netns.GetDNS(c.slirp4netnsSubnet) if err != nil { logrus.Warn("Failed to determine Slirp4netns DNS: ", err.Error()) } else { @@ -631,20 +632,28 @@ func (c *Container) addSlirp4netnsDNS(nameservers []string) []string { return nameservers } -func (c *Container) isSlirp4netnsIPv6() (bool, error) { +func (c *Container) isSlirp4netnsIPv6() bool { if c.config.NetMode.IsSlirp4netns() { - ctrNetworkSlipOpts := []string{} - if c.config.NetworkOptions != nil { - ctrNetworkSlipOpts = append(ctrNetworkSlipOpts, c.config.NetworkOptions["slirp4netns"]...) + extraOptions := c.config.NetworkOptions[slirp4netns.BinaryName] + options := make([]string, 0, len(c.runtime.config.Engine.NetworkCmdOptions)+len(extraOptions)) + options = append(options, c.runtime.config.Engine.NetworkCmdOptions...) + options = append(options, extraOptions...) + + // loop backwards as the last argument wins and we can exit early + // This should be kept in sync with c/common/libnetwork/slirp4netns. + for i := len(options) - 1; i >= 0; i-- { + switch options[i] { + case "enable_ipv6=true": + return true + case "enable_ipv6=false": + return false + } } - slirpOpts, err := parseSlirp4netnsNetworkOptions(c.runtime, ctrNetworkSlipOpts) - if err != nil { - return false, err - } - return slirpOpts.enableIPv6, nil + // default is true + return true } - return false, nil + return false } // check for net=none diff --git a/libpod/info_linux.go b/libpod/info_linux.go index 37a16b0d94..2ce4d24f88 100644 --- a/libpod/info_linux.go +++ b/libpod/info_linux.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/containers/common/libnetwork/pasta" + libpod "github.com/containers/common/libnetwork/slirp4netns" "github.com/containers/common/pkg/apparmor" "github.com/containers/common/pkg/cgroups" "github.com/containers/common/pkg/seccomp" @@ -58,7 +59,7 @@ func (r *Runtime) setPlatformHostInfo(info *define.HostInfo) error { slirp4netnsPath := r.config.Engine.NetworkCmdPath if slirp4netnsPath == "" { - slirp4netnsPath, _ = r.config.FindHelperBinary(slirp4netnsBinaryName, true) + slirp4netnsPath, _ = r.config.FindHelperBinary(libpod.BinaryName, true) } if slirp4netnsPath != "" { version, err := util.ProgramVersion(slirp4netnsPath) diff --git a/libpod/networking_common.go b/libpod/networking_common.go index 0a60770dd3..54b08240dd 100644 --- a/libpod/networking_common.go +++ b/libpod/networking_common.go @@ -583,10 +583,7 @@ func (c *Container) NetworkConnect(nameOrID, netName string, netOpts types.PerNe } } - ipv6, err := c.checkForIPv6(networkStatus) - if err != nil { - return err - } + ipv6 := c.checkForIPv6(networkStatus) // Update resolv.conf if required stringIPs := make([]string, 0, len(results[netName].DNSServerIPs)) diff --git a/libpod/networking_freebsd.go b/libpod/networking_freebsd.go index 8a96e7e8d1..aa0f9bbb3a 100644 --- a/libpod/networking_freebsd.go +++ b/libpod/networking_freebsd.go @@ -105,7 +105,7 @@ func (r *Runtime) GetRootlessNetNs(new bool) (*RootlessNetNS, error) { return nil, nil } -func GetSlirp4netnsIP(subnet *net.IPNet) (*net.IP, error) { +func getSlirp4netnsIP(subnet *net.IPNet) (*net.IP, error) { return nil, errors.New("not implemented GetSlirp4netnsIP") } diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index 62b466aada..8e45759153 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -10,19 +10,17 @@ import ( "fmt" "net" "os" - "os/exec" "path/filepath" "strconv" "strings" "syscall" - "time" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/common/libnetwork/resolvconf" + "github.com/containers/common/libnetwork/slirp4netns" "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/netns" "github.com/containers/common/pkg/util" - "github.com/containers/podman/v4/pkg/errorhandling" "github.com/containers/podman/v4/pkg/rootless" "github.com/containers/podman/v4/utils" "github.com/containers/storage/pkg/lockfile" @@ -34,12 +32,6 @@ import ( ) const ( - // slirp4netnsMTU the default MTU override - slirp4netnsMTU = 65520 - - // default slirp4ns subnet - defaultSlirp4netnsSubnet = "10.0.2.0/24" - // rootlessNetNsName is the file name for the rootless network namespace bind mount rootlessNetNsName = "rootless-netns" @@ -388,91 +380,25 @@ func (r *Runtime) GetRootlessNetNs(new bool) (*RootlessNetNS, error) { if err != nil { return nil, fmt.Errorf("creating rootless network namespace: %w", err) } - // set up slirp4netns here - path := r.config.Engine.NetworkCmdPath - if path == "" { - var err error - path, err = r.config.FindHelperBinary(slirp4netnsBinaryName, true) - if err != nil { - return nil, err - } - } - - syncR, syncW, err := os.Pipe() + res, err := slirp4netns.Setup(&slirp4netns.SetupOptions{ + Config: r.config, + ContainerID: "rootless-netns", + Netns: nsReference.Path(), + }) if err != nil { - return nil, fmt.Errorf("failed to open pipe: %w", err) - } - defer errorhandling.CloseQuiet(syncR) - defer errorhandling.CloseQuiet(syncW) - - netOptions, err := parseSlirp4netnsNetworkOptions(r, nil) - if err != nil { - return nil, err - } - slirpFeatures, err := checkSlirpFlags(path) - if err != nil { - return nil, fmt.Errorf("checking slirp4netns binary %s: %q: %w", path, err, err) - } - cmdArgs, err := createBasicSlirp4netnsCmdArgs(netOptions, slirpFeatures) - if err != nil { - return nil, err - } - // Note we do not use --exit-fd, we kill this process by pid - cmdArgs = append(cmdArgs, "-c", "-r", "3") - cmdArgs = append(cmdArgs, "--netns-type=path", nsReference.Path(), "tap0") - - cmd := exec.Command(path, cmdArgs...) - logrus.Debugf("slirp4netns command: %s", strings.Join(cmd.Args, " ")) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } - - // workaround for https://github.com/rootless-containers/slirp4netns/pull/153 - if !netOptions.noPivotRoot && slirpFeatures.HasEnableSandbox { - cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWNS - cmd.SysProcAttr.Unshareflags = syscall.CLONE_NEWNS - } - - // Leak one end of the pipe in slirp4netns - cmd.ExtraFiles = append(cmd.ExtraFiles, syncW) - - logPath := filepath.Join(r.config.Engine.TmpDir, "slirp4netns-rootless-netns.log") - logFile, err := os.Create(logPath) - if err != nil { - return nil, fmt.Errorf("failed to open slirp4netns log file %s: %w", logPath, err) - } - defer logFile.Close() - // Unlink immediately the file so we won't need to worry about cleaning it up later. - // It is still accessible through the open fd logFile. - if err := os.Remove(logPath); err != nil { - return nil, fmt.Errorf("delete file %s: %w", logPath, err) - } - cmd.Stdout = logFile - cmd.Stderr = logFile - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("failed to start slirp4netns process: %w", err) + return nil, fmt.Errorf("failed to start rootless-netns slirp4netns: %w", err) } // create pid file for the slirp4netns process // this is need to kill the process in the cleanup - pid := strconv.Itoa(cmd.Process.Pid) + pid := strconv.Itoa(res.Pid) err = os.WriteFile(filepath.Join(rootlessNetNsDir, rootlessNetNsSilrp4netnsPidFile), []byte(pid), 0700) if err != nil { return nil, fmt.Errorf("unable to write rootless-netns slirp4netns pid file: %w", err) } - defer func() { - if err := cmd.Process.Release(); err != nil { - logrus.Errorf("Unable to release command process: %q", err) - } - }() - - if err := waitForSync(syncR, cmd, logFile, 1*time.Second); err != nil { - return nil, err - } - if utils.RunsOnSystemd() { // move to systemd scope to prevent systemd from killing it - err = utils.MoveRootlessNetnsSlirpProcessToUserSlice(cmd.Process.Pid) + err = utils.MoveRootlessNetnsSlirpProcessToUserSlice(res.Pid) if err != nil { // only log this, it is not fatal but can lead to issues when running podman inside systemd units logrus.Errorf("failed to move the rootless netns slirp4netns process to the systemd user.slice: %v", err) @@ -480,29 +406,18 @@ func (r *Runtime) GetRootlessNetNs(new bool) (*RootlessNetNS, error) { } // build a new resolv.conf file which uses the slirp4netns dns server address - resolveIP, err := GetSlirp4netnsDNS(nil) + resolveIP, err := slirp4netns.GetDNS(res.Subnet) if err != nil { return nil, fmt.Errorf("failed to determine default slirp4netns DNS address: %w", err) } - if netOptions.cidr != "" { - _, cidr, err := net.ParseCIDR(netOptions.cidr) - if err != nil { - return nil, fmt.Errorf("failed to parse slirp4netns cidr: %w", err) - } - resolveIP, err = GetSlirp4netnsDNS(cidr) - if err != nil { - return nil, fmt.Errorf("failed to determine slirp4netns DNS address from cidr: %s: %w", cidr.String(), err) - } - } - if err := resolvconf.New(&resolvconf.Params{ Path: filepath.Join(rootlessNetNsDir, "resolv.conf"), // fake the netns since we want to filter localhost Namespaces: []specs.LinuxNamespace{ {Type: specs.NetworkNamespace}, }, - IPv6Enabled: netOptions.enableIPv6, + IPv6Enabled: res.IPv6, KeepHostServers: true, Nameservers: []string{resolveIP.String()}, }); err != nil { @@ -842,12 +757,3 @@ func (c *Container) inspectJoinedNetworkNS(networkns string) (q types.StatusBloc }) return result, err } - -type logrusDebugWriter struct { - prefix string -} - -func (w *logrusDebugWriter) Write(p []byte) (int, error) { - logrus.Debugf("%s%s", w.prefix, string(p)) - return len(p), nil -} diff --git a/libpod/networking_slirp4netns.go b/libpod/networking_slirp4netns.go index 0d185885d7..664b73d391 100644 --- a/libpod/networking_slirp4netns.go +++ b/libpod/networking_slirp4netns.go @@ -4,713 +4,72 @@ package libpod import ( - "bytes" - "errors" "fmt" "io" "net" "os" - "os/exec" "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "time" - "github.com/containernetworking/plugins/pkg/ns" + "github.com/containers/common/libnetwork/slirp4netns" "github.com/containers/common/libnetwork/types" "github.com/containers/podman/v4/pkg/errorhandling" - "github.com/containers/podman/v4/pkg/rootless" - "github.com/containers/podman/v4/pkg/rootlessport" - "github.com/containers/podman/v4/pkg/servicereaper" "github.com/sirupsen/logrus" ) -type slirpFeatures struct { - HasDisableHostLoopback bool - HasMTU bool - HasEnableSandbox bool - HasEnableSeccomp bool - HasCIDR bool - HasOutboundAddr bool - HasIPv6 bool -} - -type slirp4netnsCmdArg struct { - Proto string `json:"proto,omitempty"` - HostAddr string `json:"host_addr"` - HostPort uint16 `json:"host_port"` - GuestAddr string `json:"guest_addr"` - GuestPort uint16 `json:"guest_port"` -} - -type slirp4netnsCmd struct { - Execute string `json:"execute"` - Args slirp4netnsCmdArg `json:"arguments"` -} - -type slirp4netnsNetworkOptions struct { - cidr string - disableHostLoopback bool - enableIPv6 bool - isSlirpHostForward bool - noPivotRoot bool - mtu int - outboundAddr string - outboundAddr6 string -} - -const ( - ipv6ConfDefaultAcceptDadSysctl = "/proc/sys/net/ipv6/conf/default/accept_dad" - slirp4netnsBinaryName = "slirp4netns" -) - -func checkSlirpFlags(path string) (*slirpFeatures, error) { - cmd := exec.Command(path, "--help") - out, err := cmd.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("slirp4netns %q: %w", out, err) - } - return &slirpFeatures{ - HasDisableHostLoopback: strings.Contains(string(out), "--disable-host-loopback"), - HasMTU: strings.Contains(string(out), "--mtu"), - HasEnableSandbox: strings.Contains(string(out), "--enable-sandbox"), - HasEnableSeccomp: strings.Contains(string(out), "--enable-seccomp"), - HasCIDR: strings.Contains(string(out), "--cidr"), - HasOutboundAddr: strings.Contains(string(out), "--outbound-addr"), - HasIPv6: strings.Contains(string(out), "--enable-ipv6"), - }, nil -} - -func parseSlirp4netnsNetworkOptions(r *Runtime, extraOptions []string) (*slirp4netnsNetworkOptions, error) { - slirpOptions := make([]string, 0, len(r.config.Engine.NetworkCmdOptions)+len(extraOptions)) - slirpOptions = append(slirpOptions, r.config.Engine.NetworkCmdOptions...) - slirpOptions = append(slirpOptions, extraOptions...) - slirp4netnsOpts := &slirp4netnsNetworkOptions{ - // overwrite defaults - disableHostLoopback: true, - mtu: slirp4netnsMTU, - noPivotRoot: r.config.Engine.NoPivotRoot, - enableIPv6: true, - } - for _, o := range slirpOptions { - parts := strings.SplitN(o, "=", 2) - if len(parts) < 2 { - return nil, fmt.Errorf("unknown option for slirp4netns: %q", o) - } - option, value := parts[0], parts[1] - switch option { - case "cidr": - ipv4, _, err := net.ParseCIDR(value) - if err != nil || ipv4.To4() == nil { - return nil, fmt.Errorf("invalid cidr %q", value) - } - slirp4netnsOpts.cidr = value - case "port_handler": - switch value { - case "slirp4netns": - slirp4netnsOpts.isSlirpHostForward = true - case "rootlesskit": - slirp4netnsOpts.isSlirpHostForward = false - default: - return nil, fmt.Errorf("unknown port_handler for slirp4netns: %q", value) - } - case "allow_host_loopback": - switch value { - case "true": - slirp4netnsOpts.disableHostLoopback = false - case "false": - slirp4netnsOpts.disableHostLoopback = true - default: - return nil, fmt.Errorf("invalid value of allow_host_loopback for slirp4netns: %q", value) - } - case "enable_ipv6": - switch value { - case "true": - slirp4netnsOpts.enableIPv6 = true - case "false": - slirp4netnsOpts.enableIPv6 = false - default: - return nil, fmt.Errorf("invalid value of enable_ipv6 for slirp4netns: %q", value) - } - case "outbound_addr": - ipv4 := net.ParseIP(value) - if ipv4 == nil || ipv4.To4() == nil { - _, err := net.InterfaceByName(value) - if err != nil { - return nil, fmt.Errorf("invalid outbound_addr %q", value) - } - } - slirp4netnsOpts.outboundAddr = value - case "outbound_addr6": - ipv6 := net.ParseIP(value) - if ipv6 == nil || ipv6.To4() != nil { - _, err := net.InterfaceByName(value) - if err != nil { - return nil, fmt.Errorf("invalid outbound_addr6: %q", value) - } - } - slirp4netnsOpts.outboundAddr6 = value - case "mtu": - var err error - slirp4netnsOpts.mtu, err = strconv.Atoi(value) - if slirp4netnsOpts.mtu < 68 || err != nil { - return nil, fmt.Errorf("invalid mtu %q", value) - } - default: - return nil, fmt.Errorf("unknown option for slirp4netns: %q", o) - } - } - return slirp4netnsOpts, nil -} - -func createBasicSlirp4netnsCmdArgs(options *slirp4netnsNetworkOptions, features *slirpFeatures) ([]string, error) { - cmdArgs := []string{} - if options.disableHostLoopback && features.HasDisableHostLoopback { - cmdArgs = append(cmdArgs, "--disable-host-loopback") - } - if options.mtu > -1 && features.HasMTU { - cmdArgs = append(cmdArgs, fmt.Sprintf("--mtu=%d", options.mtu)) - } - if !options.noPivotRoot && features.HasEnableSandbox { - cmdArgs = append(cmdArgs, "--enable-sandbox") - } - if features.HasEnableSeccomp { - cmdArgs = append(cmdArgs, "--enable-seccomp") - } - - if options.cidr != "" { - if !features.HasCIDR { - return nil, fmt.Errorf("cidr not supported") - } - cmdArgs = append(cmdArgs, fmt.Sprintf("--cidr=%s", options.cidr)) - } - - if options.enableIPv6 { - if !features.HasIPv6 { - return nil, fmt.Errorf("enable_ipv6 not supported") - } - cmdArgs = append(cmdArgs, "--enable-ipv6") - } - - if options.outboundAddr != "" { - if !features.HasOutboundAddr { - return nil, fmt.Errorf("outbound_addr not supported") - } - cmdArgs = append(cmdArgs, fmt.Sprintf("--outbound-addr=%s", options.outboundAddr)) - } - - if options.outboundAddr6 != "" { - if !features.HasOutboundAddr || !features.HasIPv6 { - return nil, fmt.Errorf("outbound_addr6 not supported") - } - if !options.enableIPv6 { - return nil, fmt.Errorf("enable_ipv6=true is required for outbound_addr6") - } - cmdArgs = append(cmdArgs, fmt.Sprintf("--outbound-addr6=%s", options.outboundAddr6)) - } - - return cmdArgs, nil -} - // setupSlirp4netns can be called in rootful as well as in rootless func (r *Runtime) setupSlirp4netns(ctr *Container, netns string) error { - path := r.config.Engine.NetworkCmdPath - if path == "" { - var err error - path, err = r.config.FindHelperBinary(slirp4netnsBinaryName, true) - if err != nil { - return fmt.Errorf("could not find slirp4netns, the network namespace can't be configured: %w", err) - } - } + ports := ctr.convertPortMappings() - syncR, syncW, err := os.Pipe() - if err != nil { - return fmt.Errorf("failed to open pipe: %w", err) - } - defer errorhandling.CloseQuiet(syncR) - defer errorhandling.CloseQuiet(syncW) - - havePortMapping := len(ctr.config.PortMappings) > 0 - logPath := filepath.Join(ctr.runtime.config.Engine.TmpDir, fmt.Sprintf("slirp4netns-%s.log", ctr.config.ID)) - - ctrNetworkSlipOpts := []string{} - if ctr.config.NetworkOptions != nil { - ctrNetworkSlipOpts = append(ctrNetworkSlipOpts, ctr.config.NetworkOptions[slirp4netnsBinaryName]...) - } - netOptions, err := parseSlirp4netnsNetworkOptions(r, ctrNetworkSlipOpts) - if err != nil { - return err - } - slirpFeatures, err := checkSlirpFlags(path) - if err != nil { - return fmt.Errorf("checking slirp4netns binary %s: %q: %w", path, err, err) - } - cmdArgs, err := createBasicSlirp4netnsCmdArgs(netOptions, slirpFeatures) - if err != nil { - return err - } - - // the slirp4netns arguments being passed are described as follows: - // from the slirp4netns documentation: https://github.com/rootless-containers/slirp4netns - // -c, --configure Brings up the tap interface - // -e, --exit-fd=FD specify the FD for terminating slirp4netns - // -r, --ready-fd=FD specify the FD to write to when the initialization steps are finished - cmdArgs = append(cmdArgs, "-c", "-e", "3", "-r", "4") - - var apiSocket string - if havePortMapping && netOptions.isSlirpHostForward { - apiSocket = filepath.Join(ctr.runtime.config.Engine.TmpDir, fmt.Sprintf("%s.net", ctr.config.ID)) - cmdArgs = append(cmdArgs, "--api-socket", apiSocket) - } - netnsPath := "" if !ctr.config.PostConfigureNetNS { + var err error ctr.rootlessSlirpSyncR, ctr.rootlessSlirpSyncW, err = os.Pipe() if err != nil { return fmt.Errorf("failed to create rootless network sync pipe: %w", err) } - netnsPath = netns - cmdArgs = append(cmdArgs, "--netns-type=path", netnsPath, "tap0") - } else { - defer errorhandling.CloseQuiet(ctr.rootlessSlirpSyncR) - defer errorhandling.CloseQuiet(ctr.rootlessSlirpSyncW) - netnsPath = fmt.Sprintf("/proc/%d/ns/net", ctr.state.PID) - // we don't use --netns-path here (unavailable for slirp4netns < v0.4) - cmdArgs = append(cmdArgs, fmt.Sprintf("%d", ctr.state.PID), "tap0") - } - - cmd := exec.Command(path, cmdArgs...) - logrus.Debugf("slirp4netns command: %s", strings.Join(cmd.Args, " ")) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } - - // workaround for https://github.com/rootless-containers/slirp4netns/pull/153 - if !netOptions.noPivotRoot && slirpFeatures.HasEnableSandbox { - cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWNS - cmd.SysProcAttr.Unshareflags = syscall.CLONE_NEWNS - } - - // Leak one end of the pipe in slirp4netns, the other will be sent to conmon - cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessSlirpSyncR, syncW) - - logFile, err := os.Create(logPath) - if err != nil { - return fmt.Errorf("failed to open slirp4netns log file %s: %w", logPath, err) - } - defer logFile.Close() - // Unlink immediately the file so we won't need to worry about cleaning it up later. - // It is still accessible through the open fd logFile. - if err := os.Remove(logPath); err != nil { - return fmt.Errorf("delete file %s: %w", logPath, err) - } - cmd.Stdout = logFile - cmd.Stderr = logFile - - var slirpReadyWg, netnsReadyWg *sync.WaitGroup - if netOptions.enableIPv6 { - // use two wait groups to make sure we set the sysctl before - // starting slirp and reset it only after slirp is ready - slirpReadyWg = &sync.WaitGroup{} - netnsReadyWg = &sync.WaitGroup{} - slirpReadyWg.Add(1) - netnsReadyWg.Add(1) - - go func() { - err := ns.WithNetNSPath(netnsPath, func(_ ns.NetNS) error { - // Duplicate Address Detection slows the ipv6 setup down for 1-2 seconds. - // Since slirp4netns is run in its own namespace and not directly routed - // we can skip this to make the ipv6 address immediately available. - // We change the default to make sure the slirp tap interface gets the - // correct value assigned so DAD is disabled for it - // Also make sure to change this value back to the original after slirp4netns - // is ready in case users rely on this sysctl. - orgValue, err := os.ReadFile(ipv6ConfDefaultAcceptDadSysctl) - if err != nil { - netnsReadyWg.Done() - // on ipv6 disabled systems the sysctl does not exist - // so we should not error - if errors.Is(err, os.ErrNotExist) { - return nil - } - return err - } - err = os.WriteFile(ipv6ConfDefaultAcceptDadSysctl, []byte("0"), 0644) - netnsReadyWg.Done() - if err != nil { - return err - } - - // wait until slirp4nets is ready before resetting this value - slirpReadyWg.Wait() - return os.WriteFile(ipv6ConfDefaultAcceptDadSysctl, orgValue, 0644) - }) + if len(ports) > 0 { + ctr.rootlessPortSyncR, ctr.rootlessPortSyncW, err = os.Pipe() if err != nil { - logrus.Warnf("failed to set net.ipv6.conf.default.accept_dad sysctl: %v", err) + return fmt.Errorf("failed to create rootless port sync pipe: %w", err) } - }() - - // wait until we set the sysctl - netnsReadyWg.Wait() - } - - if err := cmd.Start(); err != nil { - if netOptions.enableIPv6 { - slirpReadyWg.Done() } - return fmt.Errorf("failed to start slirp4netns process: %w", err) } - defer func() { - servicereaper.AddPID(cmd.Process.Pid) - if err := cmd.Process.Release(); err != nil { - logrus.Errorf("Unable to release command process: %q", err) - } - }() + defer errorhandling.CloseQuiet(ctr.rootlessSlirpSyncR) + if ctr.rootlessPortSyncR != nil { + defer errorhandling.CloseQuiet(ctr.rootlessPortSyncR) + } - err = waitForSync(syncR, cmd, logFile, 1*time.Second) - if netOptions.enableIPv6 { - slirpReadyWg.Done() - } + res, err := slirp4netns.Setup(&slirp4netns.SetupOptions{ + Config: r.config, + ContainerID: ctr.ID(), + Netns: netns, + Ports: ports, + ExtraOptions: ctr.config.NetworkOptions[slirp4netns.BinaryName], + Slirp4netnsExitPipeR: ctr.rootlessSlirpSyncR, + RootlessPortExitPipeR: ctr.rootlessPortSyncR, + }) if err != nil { return err } - - // Set a default slirp subnet. Parsing a string with the net helper is easier than building the struct myself - _, ctr.slirp4netnsSubnet, _ = net.ParseCIDR(defaultSlirp4netnsSubnet) - - // Set slirp4netnsSubnet addresses now that we are pretty sure the command executed - if netOptions.cidr != "" { - ipv4, ipv4network, err := net.ParseCIDR(netOptions.cidr) - if err != nil || ipv4.To4() == nil { - return fmt.Errorf("invalid cidr %q", netOptions.cidr) - } - ctr.slirp4netnsSubnet = ipv4network - } - - if havePortMapping { - if netOptions.isSlirpHostForward { - return r.setupRootlessPortMappingViaSlirp(ctr, cmd, apiSocket) - } - return r.setupRootlessPortMappingViaRLK(ctr, netnsPath, nil) - } - - return nil -} - -// Get expected slirp ipv4 address based on subnet. If subnet is null use default subnet -// Reference: https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md#description -func GetSlirp4netnsIP(subnet *net.IPNet) (*net.IP, error) { - _, slirpSubnet, _ := net.ParseCIDR(defaultSlirp4netnsSubnet) - if subnet != nil { - slirpSubnet = subnet - } - expectedIP, err := addToIP(slirpSubnet, uint32(100)) - if err != nil { - return nil, fmt.Errorf("calculating expected ip for slirp4netns: %w", err) - } - return expectedIP, nil -} - -// Get expected slirp Gateway ipv4 address based on subnet -// Reference: https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md#description -func GetSlirp4netnsGateway(subnet *net.IPNet) (*net.IP, error) { - _, slirpSubnet, _ := net.ParseCIDR(defaultSlirp4netnsSubnet) - if subnet != nil { - slirpSubnet = subnet - } - expectedGatewayIP, err := addToIP(slirpSubnet, uint32(2)) - if err != nil { - return nil, fmt.Errorf("calculating expected gateway ip for slirp4netns: %w", err) - } - return expectedGatewayIP, nil -} - -// Get expected slirp DNS ipv4 address based on subnet -// Reference: https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md#description -func GetSlirp4netnsDNS(subnet *net.IPNet) (*net.IP, error) { - _, slirpSubnet, _ := net.ParseCIDR(defaultSlirp4netnsSubnet) - if subnet != nil { - slirpSubnet = subnet - } - expectedDNSIP, err := addToIP(slirpSubnet, uint32(3)) - if err != nil { - return nil, fmt.Errorf("calculating expected dns ip for slirp4netns: %w", err) - } - return expectedDNSIP, nil -} - -// Helper function to calculate slirp ip address offsets -// Adapted from: https://github.com/signalsciences/ipv4/blob/master/int.go#L12-L24 -func addToIP(subnet *net.IPNet, offset uint32) (*net.IP, error) { - // I have no idea why I have to do this, but if I don't ip is 0 - ipFixed := subnet.IP.To4() - - ipInteger := uint32(ipFixed[3]) | uint32(ipFixed[2])<<8 | uint32(ipFixed[1])<<16 | uint32(ipFixed[0])<<24 - ipNewRaw := ipInteger + offset - // Avoid overflows - if ipNewRaw < ipInteger { - return nil, fmt.Errorf("integer overflow while calculating ip address offset, %s + %d", ipFixed, offset) - } - ipNew := net.IPv4(byte(ipNewRaw>>24), byte(ipNewRaw>>16&0xFF), byte(ipNewRaw>>8)&0xFF, byte(ipNewRaw&0xFF)) - if !subnet.Contains(ipNew) { - return nil, fmt.Errorf("calculated ip address %s is not within given subnet %s", ipNew.String(), subnet.String()) - } - return &ipNew, nil -} - -func waitForSync(syncR *os.File, cmd *exec.Cmd, logFile io.ReadSeeker, timeout time.Duration) error { - prog := filepath.Base(cmd.Path) - if len(cmd.Args) > 0 { - prog = cmd.Args[0] - } - b := make([]byte, 16) - for { - if err := syncR.SetDeadline(time.Now().Add(timeout)); err != nil { - return fmt.Errorf("setting %s pipe timeout: %w", prog, err) - } - // FIXME: return err as soon as proc exits, without waiting for timeout - if _, err := syncR.Read(b); err == nil { - break - } else { - if os.IsTimeout(err) { - // Check if the process is still running. - var status syscall.WaitStatus - pid, err := syscall.Wait4(cmd.Process.Pid, &status, syscall.WNOHANG, nil) - if err != nil { - return fmt.Errorf("failed to read %s process status: %w", prog, err) - } - if pid != cmd.Process.Pid { - continue - } - if status.Exited() { - // Seek at the beginning of the file and read all its content - if _, err := logFile.Seek(0, 0); err != nil { - logrus.Errorf("Could not seek log file: %q", err) - } - logContent, err := io.ReadAll(logFile) - if err != nil { - return fmt.Errorf("%s failed: %w", prog, err) - } - return fmt.Errorf("%s failed: %q", prog, logContent) - } - if status.Signaled() { - return fmt.Errorf("%s killed by signal", prog) - } - continue - } - return fmt.Errorf("failed to read from %s sync pipe: %w", prog, err) - } - } + ctr.slirp4netnsSubnet = res.Subnet return nil } func (r *Runtime) setupRootlessPortMappingViaRLK(ctr *Container, netnsPath string, netStatus map[string]types.StatusBlock) error { - syncR, syncW, err := os.Pipe() - if err != nil { - return fmt.Errorf("failed to open pipe: %w", err) - } - defer errorhandling.CloseQuiet(syncR) - defer errorhandling.CloseQuiet(syncW) - - logPath := filepath.Join(ctr.runtime.config.Engine.TmpDir, fmt.Sprintf("rootlessport-%s.log", ctr.config.ID)) - logFile, err := os.Create(logPath) - if err != nil { - return fmt.Errorf("failed to open rootlessport log file %s: %w", logPath, err) - } - defer logFile.Close() - // Unlink immediately the file so we won't need to worry about cleaning it up later. - // It is still accessible through the open fd logFile. - if err := os.Remove(logPath); err != nil { - return fmt.Errorf("delete file %s: %w", logPath, err) - } - + var err error if !ctr.config.PostConfigureNetNS { ctr.rootlessPortSyncR, ctr.rootlessPortSyncW, err = os.Pipe() if err != nil { return fmt.Errorf("failed to create rootless port sync pipe: %w", err) } } - - childIP := getRootlessPortChildIP(ctr, netStatus) - cfg := rootlessport.Config{ - Mappings: ctr.convertPortMappings(), - NetNSPath: netnsPath, - ExitFD: 3, - ReadyFD: 4, - TmpDir: ctr.runtime.config.Engine.TmpDir, - ChildIP: childIP, - ContainerID: ctr.config.ID, - RootlessCNI: ctr.config.NetMode.IsBridge() && rootless.IsRootless(), - } - cfgJSON, err := json.Marshal(cfg) - if err != nil { - return err - } - cfgR := bytes.NewReader(cfgJSON) - var stdout bytes.Buffer - path, err := r.config.FindHelperBinary(rootlessport.BinaryName, false) - if err != nil { - return err - } - cmd := exec.Command(path) - cmd.Args = []string{rootlessport.BinaryName} - - // Leak one end of the pipe in rootlessport process, the other will be sent to conmon defer errorhandling.CloseQuiet(ctr.rootlessPortSyncR) - - cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessPortSyncR, syncW) - cmd.Stdin = cfgR - // stdout is for human-readable error, stderr is for debug log - cmd.Stdout = &stdout - cmd.Stderr = io.MultiWriter(logFile, &logrusDebugWriter{"rootlessport: "}) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } - if err := cmd.Start(); err != nil { - return fmt.Errorf("failed to start rootlessport process: %w", err) - } - defer func() { - servicereaper.AddPID(cmd.Process.Pid) - if err := cmd.Process.Release(); err != nil { - logrus.Errorf("Unable to release rootlessport process: %q", err) - } - }() - if err := waitForSync(syncR, cmd, logFile, 3*time.Second); err != nil { - stdoutStr := stdout.String() - if stdoutStr != "" { - // err contains full debug log and too verbose, so return stdoutStr - logrus.Debug(err) - return fmt.Errorf("rootlessport " + strings.TrimSuffix(stdoutStr, "\n")) - } - return err - } - logrus.Debug("rootlessport is ready") - return nil -} - -func (r *Runtime) setupRootlessPortMappingViaSlirp(ctr *Container, cmd *exec.Cmd, apiSocket string) (err error) { - const pidWaitTimeout = 60 * time.Second - chWait := make(chan error) - go func() { - interval := 25 * time.Millisecond - for i := time.Duration(0); i < pidWaitTimeout; i += interval { - // Check if the process is still running. - var status syscall.WaitStatus - pid, err := syscall.Wait4(cmd.Process.Pid, &status, syscall.WNOHANG, nil) - if err != nil { - break - } - if pid != cmd.Process.Pid { - continue - } - if status.Exited() || status.Signaled() { - chWait <- fmt.Errorf("slirp4netns exited with status %d", status.ExitStatus()) - } - time.Sleep(interval) - } - }() - defer close(chWait) - - // wait that API socket file appears before trying to use it. - if _, err := WaitForFile(apiSocket, chWait, pidWaitTimeout); err != nil { - return fmt.Errorf("waiting for slirp4nets to create the api socket file %s: %w", apiSocket, err) - } - - // for each port we want to add we need to open a connection to the slirp4netns control socket - // and send the add_hostfwd command. - for _, port := range ctr.convertPortMappings() { - protocols := strings.Split(port.Protocol, ",") - for _, protocol := range protocols { - hostIP := port.HostIP - if hostIP == "" { - hostIP = "0.0.0.0" - } - for i := uint16(0); i < port.Range; i++ { - if err := openSlirp4netnsPort(apiSocket, protocol, hostIP, port.HostPort+i, port.ContainerPort+i); err != nil { - return err - } - } - } - } - logrus.Debug("slirp4netns port-forwarding setup via add_hostfwd is ready") - return nil -} - -// openSlirp4netnsPort sends the slirp4netns pai quey to the given socket -func openSlirp4netnsPort(apiSocket, proto, hostip string, hostport, guestport uint16) error { - conn, err := net.Dial("unix", apiSocket) - if err != nil { - return fmt.Errorf("cannot open connection to %s: %w", apiSocket, err) - } - defer func() { - if err := conn.Close(); err != nil { - logrus.Errorf("Unable to close slirp4netns connection: %q", err) - } - }() - apiCmd := slirp4netnsCmd{ - Execute: "add_hostfwd", - Args: slirp4netnsCmdArg{ - Proto: proto, - HostAddr: hostip, - HostPort: hostport, - GuestPort: guestport, - }, - } - // create the JSON payload and send it. Mark the end of request shutting down writes - // to the socket, as requested by slirp4netns. - data, err := json.Marshal(&apiCmd) - if err != nil { - return fmt.Errorf("cannot marshal JSON for slirp4netns: %w", err) - } - if _, err := conn.Write([]byte(fmt.Sprintf("%s\n", data))); err != nil { - return fmt.Errorf("cannot write to control socket %s: %w", apiSocket, err) - } - if err := conn.(*net.UnixConn).CloseWrite(); err != nil { - return fmt.Errorf("cannot shutdown the socket %s: %w", apiSocket, err) - } - buf := make([]byte, 2048) - readLength, err := conn.Read(buf) - if err != nil { - return fmt.Errorf("cannot read from control socket %s: %w", apiSocket, err) - } - // if there is no 'error' key in the received JSON data, then the operation was - // successful. - var y map[string]interface{} - if err := json.Unmarshal(buf[0:readLength], &y); err != nil { - return fmt.Errorf("parsing error status from slirp4netns: %w", err) - } - if e, found := y["error"]; found { - return fmt.Errorf("from slirp4netns while setting up port redirection: %v", e) - } - return nil -} - -func getRootlessPortChildIP(c *Container, netStatus map[string]types.StatusBlock) string { - if c.config.NetMode.IsSlirp4netns() { - slirp4netnsIP, err := GetSlirp4netnsIP(c.slirp4netnsSubnet) - if err != nil { - return "" - } - return slirp4netnsIP.String() - } - - var ipv6 net.IP - for _, status := range netStatus { - for _, netInt := range status.Interfaces { - for _, netAddress := range netInt.Subnets { - ipv4 := netAddress.IPNet.IP.To4() - if ipv4 != nil { - return ipv4.String() - } - ipv6 = netAddress.IPNet.IP - } - } - } - if ipv6 != nil { - return ipv6.String() - } - return "" + return slirp4netns.SetupRootlessPortMappingViaRLK(&slirp4netns.SetupOptions{ + Config: r.config, + ContainerID: ctr.ID(), + Netns: netnsPath, + Ports: ctr.convertPortMappings(), + RootlessPortExitPipeR: ctr.rootlessPortSyncR, + }, nil, netStatus) } // reloadRootlessRLKPortMapping will trigger a reload for the port mappings in the rootlessport process. @@ -719,7 +78,7 @@ func (c *Container) reloadRootlessRLKPortMapping() error { if len(c.config.PortMappings) == 0 { return nil } - childIP := getRootlessPortChildIP(c, c.state.NetworkStatus) + childIP := slirp4netns.GetRootlessPortChildIP(nil, c.state.NetworkStatus) logrus.Debugf("reloading rootless ports for container %s, childIP is %s", c.config.ID, childIP) conn, err := openUnixSocket(filepath.Join(c.runtime.config.Engine.TmpDir, "rp", c.config.ID)) @@ -742,3 +101,7 @@ func (c *Container) reloadRootlessRLKPortMapping() error { } return nil } + +func getSlirp4netnsIP(subnet *net.IPNet) (*net.IP, error) { + return slirp4netns.GetIP(subnet) +} diff --git a/libpod/networking_unsupported.go b/libpod/networking_unsupported.go index 7c00d25b3d..d6f58314fc 100644 --- a/libpod/networking_unsupported.go +++ b/libpod/networking_unsupported.go @@ -85,7 +85,3 @@ func (r *Runtime) GetRootlessNetNs(new bool) (*RootlessNetNS, error) { func (c *Container) convertPortMappings() []types.PortMapping { return []types.PortMapping{} } - -func GetSlirp4netnsIP(subnet *net.IPNet) (*net.IP, error) { - return nil, errors.New("not implemented GetSlirp4netnsIP") -} diff --git a/libpod/oci_conmon_common.go b/libpod/oci_conmon_common.go index eeaf63aa65..8ef767454c 100644 --- a/libpod/oci_conmon_common.go +++ b/libpod/oci_conmon_common.go @@ -1233,16 +1233,13 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co if err != nil { return 0, fmt.Errorf("failed to create rootless network sync pipe: %w", err) } - } else { - if ctr.rootlessSlirpSyncR != nil { - defer errorhandling.CloseQuiet(ctr.rootlessSlirpSyncR) - } - if ctr.rootlessSlirpSyncW != nil { - defer errorhandling.CloseQuiet(ctr.rootlessSlirpSyncW) - } } - // Leak one end in conmon, the other one will be leaked into slirp4netns - cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessSlirpSyncW) + + if ctr.rootlessSlirpSyncW != nil { + defer errorhandling.CloseQuiet(ctr.rootlessSlirpSyncW) + // Leak one end in conmon, the other one will be leaked into slirp4netns + cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessSlirpSyncW) + } if ctr.rootlessPortSyncW != nil { defer errorhandling.CloseQuiet(ctr.rootlessPortSyncW) diff --git a/libpod/util.go b/libpod/util.go index 2ecd9661fa..94eef3a03c 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -15,7 +15,6 @@ import ( "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/libpod/define" - "github.com/fsnotify/fsnotify" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux/label" "github.com/sirupsen/logrus" @@ -39,57 +38,6 @@ func MountExists(specMounts []spec.Mount, dest string) bool { return false } -// WaitForFile waits until a file has been created or the given timeout has occurred -func WaitForFile(path string, chWait chan error, timeout time.Duration) (bool, error) { - var inotifyEvents chan fsnotify.Event - watcher, err := fsnotify.NewWatcher() - if err == nil { - if err := watcher.Add(filepath.Dir(path)); err == nil { - inotifyEvents = watcher.Events - } - defer func() { - if err := watcher.Close(); err != nil { - logrus.Errorf("Failed to close fsnotify watcher: %v", err) - } - }() - } - - var timeoutChan <-chan time.Time - - if timeout != 0 { - timeoutChan = time.After(timeout) - } - - for { - select { - case e := <-chWait: - return true, e - case <-inotifyEvents: - _, err := os.Stat(path) - if err == nil { - return false, nil - } - if !os.IsNotExist(err) { - return false, err - } - case <-time.After(25 * time.Millisecond): - // Check periodically for the file existence. It is needed - // if the inotify watcher could not have been created. It is - // also useful when using inotify as if for any reasons we missed - // a notification, we won't hang the process. - _, err := os.Stat(path) - if err == nil { - return false, nil - } - if !os.IsNotExist(err) { - return false, err - } - case <-timeoutChan: - return false, fmt.Errorf("timed out waiting for file %s: %w", path, define.ErrInternal) - } - } -} - type byDestination []spec.Mount func (m byDestination) Len() int { diff --git a/vendor/github.com/containers/common/libnetwork/slirp4netns/const.go b/vendor/github.com/containers/common/libnetwork/slirp4netns/const.go new file mode 100644 index 0000000000..9dc0c2625a --- /dev/null +++ b/vendor/github.com/containers/common/libnetwork/slirp4netns/const.go @@ -0,0 +1,12 @@ +package slirp4netns + +const ( + ipv6ConfDefaultAcceptDadSysctl = "/proc/sys/net/ipv6/conf/default/accept_dad" + BinaryName = "slirp4netns" + + // defaultMTU the default MTU override + defaultMTU = 65520 + + // default slirp4ns subnet + defaultSubnet = "10.0.2.0/24" +) diff --git a/vendor/github.com/containers/common/libnetwork/slirp4netns/slirp4netns.go b/vendor/github.com/containers/common/libnetwork/slirp4netns/slirp4netns.go new file mode 100644 index 0000000000..712abedbc3 --- /dev/null +++ b/vendor/github.com/containers/common/libnetwork/slirp4netns/slirp4netns.go @@ -0,0 +1,750 @@ +//go:build linux +// +build linux + +package slirp4netns + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containers/common/libnetwork/types" + "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/rootlessport" + "github.com/containers/common/pkg/servicereaper" + "github.com/containers/common/pkg/util" + "github.com/sirupsen/logrus" +) + +type slirpFeatures struct { + HasDisableHostLoopback bool + HasMTU bool + HasEnableSandbox bool + HasEnableSeccomp bool + HasCIDR bool + HasOutboundAddr bool + HasIPv6 bool +} + +type slirp4netnsCmdArg struct { + Proto string `json:"proto,omitempty"` + HostAddr string `json:"host_addr"` + HostPort uint16 `json:"host_port"` + GuestAddr string `json:"guest_addr"` + GuestPort uint16 `json:"guest_port"` +} + +type slirp4netnsCmd struct { + Execute string `json:"execute"` + Args slirp4netnsCmdArg `json:"arguments"` +} + +type networkOptions struct { + cidr string + disableHostLoopback bool + enableIPv6 bool + isSlirpHostForward bool + noPivotRoot bool + mtu int + outboundAddr string + outboundAddr6 string +} + +type SetupOptions struct { + // Config used to get slip4netns path and other default options + Config *config.Config + // ContainerID is the ID of the container + ContainerID string + // Netns path to the netns + Netns string + // ContainerPID is the pid of container process + ContainerPID int + // Ports the should be forwarded + Ports []types.PortMapping + // ExtraOptions for slirp4netns that were set on the cli + ExtraOptions []string + // Slirp4netnsExitPipeR pipe used to exit the slirp4netns process. + // This is must be the reading end, the writer must be kept open until you want the + // process to exit. For podman, conmon will hold the pipe open. + // It can be set to nil in which case we do not use the pipe exit and the caller + // must use the returned pid to kill the process after it is done. + Slirp4netnsExitPipeR *os.File + // RootlessPortSyncPipe pipe used to exit the rootlessport process. + // Same as Slirp4netnsExitPipeR, except this is only used when ports are given. + RootlessPortExitPipeR *os.File +} + +// SetupResult return type from Setup() +type SetupResult struct { + // Pid of the created slirp4netns process + Pid int + // Subnet which is used by slirp4netns + Subnet *net.IPNet + // IPv6 whenever Ipv6 is enabled in slirp4netns + IPv6 bool +} + +type logrusDebugWriter struct { + prefix string +} + +func (w *logrusDebugWriter) Write(p []byte) (int, error) { + logrus.Debugf("%s%s", w.prefix, string(p)) + return len(p), nil +} + +func checkSlirpFlags(path string) (*slirpFeatures, error) { + cmd := exec.Command(path, "--help") + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("slirp4netns %q: %w", out, err) + } + return &slirpFeatures{ + HasDisableHostLoopback: strings.Contains(string(out), "--disable-host-loopback"), + HasMTU: strings.Contains(string(out), "--mtu"), + HasEnableSandbox: strings.Contains(string(out), "--enable-sandbox"), + HasEnableSeccomp: strings.Contains(string(out), "--enable-seccomp"), + HasCIDR: strings.Contains(string(out), "--cidr"), + HasOutboundAddr: strings.Contains(string(out), "--outbound-addr"), + HasIPv6: strings.Contains(string(out), "--enable-ipv6"), + }, nil +} + +func parseNetworkOptions(config *config.Config, extraOptions []string) (*networkOptions, error) { + options := make([]string, 0, len(config.Engine.NetworkCmdOptions)+len(extraOptions)) + options = append(options, config.Engine.NetworkCmdOptions...) + options = append(options, extraOptions...) + opts := &networkOptions{ + // overwrite defaults + disableHostLoopback: true, + mtu: defaultMTU, + noPivotRoot: config.Engine.NoPivotRoot, + enableIPv6: true, + } + for _, o := range options { + parts := strings.SplitN(o, "=", 2) + if len(parts) < 2 { + return nil, fmt.Errorf("unknown option for slirp4netns: %q", o) + } + option, value := parts[0], parts[1] + switch option { + case "cidr": + ipv4, _, err := net.ParseCIDR(value) + if err != nil || ipv4.To4() == nil { + return nil, fmt.Errorf("invalid cidr %q", value) + } + opts.cidr = value + case "port_handler": + switch value { + case "slirp4netns": + opts.isSlirpHostForward = true + case "rootlesskit": + opts.isSlirpHostForward = false + default: + return nil, fmt.Errorf("unknown port_handler for slirp4netns: %q", value) + } + case "allow_host_loopback": + switch value { + case "true": + opts.disableHostLoopback = false + case "false": + opts.disableHostLoopback = true + default: + return nil, fmt.Errorf("invalid value of allow_host_loopback for slirp4netns: %q", value) + } + case "enable_ipv6": + switch value { + case "true": + opts.enableIPv6 = true + case "false": + opts.enableIPv6 = false + default: + return nil, fmt.Errorf("invalid value of enable_ipv6 for slirp4netns: %q", value) + } + case "outbound_addr": + ipv4 := net.ParseIP(value) + if ipv4 == nil || ipv4.To4() == nil { + _, err := net.InterfaceByName(value) + if err != nil { + return nil, fmt.Errorf("invalid outbound_addr %q", value) + } + } + opts.outboundAddr = value + case "outbound_addr6": + ipv6 := net.ParseIP(value) + if ipv6 == nil || ipv6.To4() != nil { + _, err := net.InterfaceByName(value) + if err != nil { + return nil, fmt.Errorf("invalid outbound_addr6: %q", value) + } + } + opts.outboundAddr6 = value + case "mtu": + var err error + opts.mtu, err = strconv.Atoi(value) + if opts.mtu < 68 || err != nil { + return nil, fmt.Errorf("invalid mtu %q", value) + } + default: + return nil, fmt.Errorf("unknown option for slirp4netns: %q", o) + } + } + return opts, nil +} + +func createBasicSlirpCmdArgs(options *networkOptions, features *slirpFeatures) ([]string, error) { + cmdArgs := []string{} + if options.disableHostLoopback && features.HasDisableHostLoopback { + cmdArgs = append(cmdArgs, "--disable-host-loopback") + } + if options.mtu > -1 && features.HasMTU { + cmdArgs = append(cmdArgs, fmt.Sprintf("--mtu=%d", options.mtu)) + } + if !options.noPivotRoot && features.HasEnableSandbox { + cmdArgs = append(cmdArgs, "--enable-sandbox") + } + if features.HasEnableSeccomp { + cmdArgs = append(cmdArgs, "--enable-seccomp") + } + + if options.cidr != "" { + if !features.HasCIDR { + return nil, fmt.Errorf("cidr not supported") + } + cmdArgs = append(cmdArgs, fmt.Sprintf("--cidr=%s", options.cidr)) + } + + if options.enableIPv6 { + if !features.HasIPv6 { + return nil, fmt.Errorf("enable_ipv6 not supported") + } + cmdArgs = append(cmdArgs, "--enable-ipv6") + } + + if options.outboundAddr != "" { + if !features.HasOutboundAddr { + return nil, fmt.Errorf("outbound_addr not supported") + } + cmdArgs = append(cmdArgs, fmt.Sprintf("--outbound-addr=%s", options.outboundAddr)) + } + + if options.outboundAddr6 != "" { + if !features.HasOutboundAddr || !features.HasIPv6 { + return nil, fmt.Errorf("outbound_addr6 not supported") + } + if !options.enableIPv6 { + return nil, fmt.Errorf("enable_ipv6=true is required for outbound_addr6") + } + cmdArgs = append(cmdArgs, fmt.Sprintf("--outbound-addr6=%s", options.outboundAddr6)) + } + + return cmdArgs, nil +} + +// Setup can be called in rootful as well as in rootless. +// Spawns the slirp4netns process and setup port forwarding if ports are given. +func Setup(opts *SetupOptions) (*SetupResult, error) { + path := opts.Config.Engine.NetworkCmdPath + if path == "" { + var err error + path, err = opts.Config.FindHelperBinary(BinaryName, true) + if err != nil { + return nil, fmt.Errorf("could not find slirp4netns, the network namespace can't be configured: %w", err) + } + } + + syncR, syncW, err := os.Pipe() + if err != nil { + return nil, fmt.Errorf("failed to open pipe: %w", err) + } + defer closeQuiet(syncR) + defer closeQuiet(syncW) + + havePortMapping := len(opts.Ports) > 0 + logPath := filepath.Join(opts.Config.Engine.TmpDir, fmt.Sprintf("slirp4netns-%s.log", opts.ContainerID)) + + netOptions, err := parseNetworkOptions(opts.Config, opts.ExtraOptions) + if err != nil { + return nil, err + } + slirpFeatures, err := checkSlirpFlags(path) + if err != nil { + return nil, fmt.Errorf("checking slirp4netns binary %s: %q: %w", path, err, err) + } + cmdArgs, err := createBasicSlirpCmdArgs(netOptions, slirpFeatures) + if err != nil { + return nil, err + } + + // the slirp4netns arguments being passed are described as follows: + // from the slirp4netns documentation: https://github.com/rootless-containers/slirp4netns + // -c, --configure Brings up the tap interface + // -e, --exit-fd=FD specify the FD for terminating slirp4netns + // -r, --ready-fd=FD specify the FD to write to when the initialization steps are finished + cmdArgs = append(cmdArgs, "-c", "-r", "3") + if opts.Slirp4netnsExitPipeR != nil { + cmdArgs = append(cmdArgs, "-e", "4") + } + + var apiSocket string + if havePortMapping && netOptions.isSlirpHostForward { + apiSocket = filepath.Join(opts.Config.Engine.TmpDir, fmt.Sprintf("%s.net", opts.ContainerID)) + cmdArgs = append(cmdArgs, "--api-socket", apiSocket) + } + + cmdArgs = append(cmdArgs, "--netns-type=path", opts.Netns, "tap0") + + cmd := exec.Command(path, cmdArgs...) + logrus.Debugf("slirp4netns command: %s", strings.Join(cmd.Args, " ")) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + + // workaround for https://github.com/rootless-containers/slirp4netns/pull/153 + if !netOptions.noPivotRoot && slirpFeatures.HasEnableSandbox { + cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWNS + cmd.SysProcAttr.Unshareflags = syscall.CLONE_NEWNS + } + + // Leak one end of the pipe in slirp4netns, the other will be sent to conmon + cmd.ExtraFiles = append(cmd.ExtraFiles, syncW) + if opts.Slirp4netnsExitPipeR != nil { + cmd.ExtraFiles = append(cmd.ExtraFiles, opts.Slirp4netnsExitPipeR) + } + + logFile, err := os.Create(logPath) + if err != nil { + return nil, fmt.Errorf("failed to open slirp4netns log file %s: %w", logPath, err) + } + defer logFile.Close() + // Unlink immediately the file so we won't need to worry about cleaning it up later. + // It is still accessible through the open fd logFile. + if err := os.Remove(logPath); err != nil { + return nil, fmt.Errorf("delete file %s: %w", logPath, err) + } + cmd.Stdout = logFile + cmd.Stderr = logFile + + var slirpReadyWg, netnsReadyWg *sync.WaitGroup + if netOptions.enableIPv6 { + // use two wait groups to make sure we set the sysctl before + // starting slirp and reset it only after slirp is ready + slirpReadyWg = &sync.WaitGroup{} + netnsReadyWg = &sync.WaitGroup{} + slirpReadyWg.Add(1) + netnsReadyWg.Add(1) + + go func() { + err := ns.WithNetNSPath(opts.Netns, func(_ ns.NetNS) error { + // Duplicate Address Detection slows the ipv6 setup down for 1-2 seconds. + // Since slirp4netns is run in its own namespace and not directly routed + // we can skip this to make the ipv6 address immediately available. + // We change the default to make sure the slirp tap interface gets the + // correct value assigned so DAD is disabled for it + // Also make sure to change this value back to the original after slirp4netns + // is ready in case users rely on this sysctl. + orgValue, err := os.ReadFile(ipv6ConfDefaultAcceptDadSysctl) + if err != nil { + netnsReadyWg.Done() + // on ipv6 disabled systems the sysctl does not exist + // so we should not error + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err + } + err = os.WriteFile(ipv6ConfDefaultAcceptDadSysctl, []byte("0"), 0o644) + netnsReadyWg.Done() + if err != nil { + return err + } + + // wait until slirp4nets is ready before resetting this value + slirpReadyWg.Wait() + return os.WriteFile(ipv6ConfDefaultAcceptDadSysctl, orgValue, 0o644) + }) + if err != nil { + logrus.Warnf("failed to set net.ipv6.conf.default.accept_dad sysctl: %v", err) + } + }() + + // wait until we set the sysctl + netnsReadyWg.Wait() + } + + if err := cmd.Start(); err != nil { + if netOptions.enableIPv6 { + slirpReadyWg.Done() + } + return nil, fmt.Errorf("failed to start slirp4netns process: %w", err) + } + defer func() { + servicereaper.AddPID(cmd.Process.Pid) + if err := cmd.Process.Release(); err != nil { + logrus.Errorf("Unable to release command process: %q", err) + } + }() + + err = waitForSync(syncR, cmd, logFile, 1*time.Second) + if netOptions.enableIPv6 { + slirpReadyWg.Done() + } + if err != nil { + return nil, err + } + + // Set a default slirp subnet. Parsing a string with the net helper is easier than building the struct myself + _, slirpSubnet, _ := net.ParseCIDR(defaultSubnet) + + // Set slirp4netnsSubnet addresses now that we are pretty sure the command executed + if netOptions.cidr != "" { + ipv4, ipv4network, err := net.ParseCIDR(netOptions.cidr) + if err != nil || ipv4.To4() == nil { + return nil, fmt.Errorf("invalid cidr %q", netOptions.cidr) + } + slirpSubnet = ipv4network + } + + if havePortMapping { + if netOptions.isSlirpHostForward { + err = setupRootlessPortMappingViaSlirp(opts.Ports, cmd, apiSocket) + } else { + err = SetupRootlessPortMappingViaRLK(opts, slirpSubnet, nil) + } + if err != nil { + return nil, err + } + } + + return &SetupResult{ + Pid: cmd.Process.Pid, + Subnet: slirpSubnet, + IPv6: netOptions.enableIPv6, + }, nil +} + +// Get expected slirp ipv4 address based on subnet. If subnet is null use default subnet +// Reference: https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md#description +func GetIP(subnet *net.IPNet) (*net.IP, error) { + _, slirpSubnet, _ := net.ParseCIDR(defaultSubnet) + if subnet != nil { + slirpSubnet = subnet + } + expectedIP, err := addToIP(slirpSubnet, uint32(100)) + if err != nil { + return nil, fmt.Errorf("calculating expected ip for slirp4netns: %w", err) + } + return expectedIP, nil +} + +// Get expected slirp Gateway ipv4 address based on subnet +// Reference: https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md#description +func GetGateway(subnet *net.IPNet) (*net.IP, error) { + _, slirpSubnet, _ := net.ParseCIDR(defaultSubnet) + if subnet != nil { + slirpSubnet = subnet + } + expectedGatewayIP, err := addToIP(slirpSubnet, uint32(2)) + if err != nil { + return nil, fmt.Errorf("calculating expected gateway ip for slirp4netns: %w", err) + } + return expectedGatewayIP, nil +} + +// Get expected slirp DNS ipv4 address based on subnet +// Reference: https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md#description +func GetDNS(subnet *net.IPNet) (*net.IP, error) { + _, slirpSubnet, _ := net.ParseCIDR(defaultSubnet) + if subnet != nil { + slirpSubnet = subnet + } + expectedDNSIP, err := addToIP(slirpSubnet, uint32(3)) + if err != nil { + return nil, fmt.Errorf("calculating expected dns ip for slirp4netns: %w", err) + } + return expectedDNSIP, nil +} + +// Helper function to calculate slirp ip address offsets +// Adapted from: https://github.com/signalsciences/ipv4/blob/master/int.go#L12-L24 +func addToIP(subnet *net.IPNet, offset uint32) (*net.IP, error) { + // I have no idea why I have to do this, but if I don't ip is 0 + ipFixed := subnet.IP.To4() + + ipInteger := uint32(ipFixed[3]) | uint32(ipFixed[2])<<8 | uint32(ipFixed[1])<<16 | uint32(ipFixed[0])<<24 + ipNewRaw := ipInteger + offset + // Avoid overflows + if ipNewRaw < ipInteger { + return nil, fmt.Errorf("integer overflow while calculating ip address offset, %s + %d", ipFixed, offset) + } + ipNew := net.IPv4(byte(ipNewRaw>>24), byte(ipNewRaw>>16&0xFF), byte(ipNewRaw>>8)&0xFF, byte(ipNewRaw&0xFF)) + if !subnet.Contains(ipNew) { + return nil, fmt.Errorf("calculated ip address %s is not within given subnet %s", ipNew.String(), subnet.String()) + } + return &ipNew, nil +} + +func waitForSync(syncR *os.File, cmd *exec.Cmd, logFile io.ReadSeeker, timeout time.Duration) error { + prog := filepath.Base(cmd.Path) + if len(cmd.Args) > 0 { + prog = cmd.Args[0] + } + b := make([]byte, 16) + for { + if err := syncR.SetDeadline(time.Now().Add(timeout)); err != nil { + return fmt.Errorf("setting %s pipe timeout: %w", prog, err) + } + // FIXME: return err as soon as proc exits, without waiting for timeout + _, err := syncR.Read(b) + if err == nil { + break + } + if errors.Is(err, os.ErrDeadlineExceeded) { + // Check if the process is still running. + var status syscall.WaitStatus + pid, err := syscall.Wait4(cmd.Process.Pid, &status, syscall.WNOHANG, nil) + if err != nil { + return fmt.Errorf("failed to read %s process status: %w", prog, err) + } + if pid != cmd.Process.Pid { + continue + } + if status.Exited() { + // Seek at the beginning of the file and read all its content + if _, err := logFile.Seek(0, 0); err != nil { + logrus.Errorf("Could not seek log file: %q", err) + } + logContent, err := io.ReadAll(logFile) + if err != nil { + return fmt.Errorf("%s failed: %w", prog, err) + } + return fmt.Errorf("%s failed: %q", prog, logContent) + } + if status.Signaled() { + return fmt.Errorf("%s killed by signal", prog) + } + continue + } + return fmt.Errorf("failed to read from %s sync pipe: %w", prog, err) + } + return nil +} + +func SetupRootlessPortMappingViaRLK(opts *SetupOptions, slirpSubnet *net.IPNet, netStatus map[string]types.StatusBlock) error { + syncR, syncW, err := os.Pipe() + if err != nil { + return fmt.Errorf("failed to open pipe: %w", err) + } + defer closeQuiet(syncR) + defer closeQuiet(syncW) + + logPath := filepath.Join(opts.Config.Engine.TmpDir, fmt.Sprintf("rootlessport-%s.log", opts.ContainerID)) + logFile, err := os.Create(logPath) + if err != nil { + return fmt.Errorf("failed to open rootlessport log file %s: %w", logPath, err) + } + defer logFile.Close() + // Unlink immediately the file so we won't need to worry about cleaning it up later. + // It is still accessible through the open fd logFile. + if err := os.Remove(logPath); err != nil { + return fmt.Errorf("delete file %s: %w", logPath, err) + } + + childIP := GetRootlessPortChildIP(slirpSubnet, netStatus) + cfg := rootlessport.Config{ + Mappings: opts.Ports, + NetNSPath: opts.Netns, + ExitFD: 3, + ReadyFD: 4, + TmpDir: opts.Config.Engine.TmpDir, + ChildIP: childIP, + ContainerID: opts.ContainerID, + RootlessCNI: netStatus != nil, + } + cfgJSON, err := json.Marshal(cfg) + if err != nil { + return err + } + cfgR := bytes.NewReader(cfgJSON) + var stdout bytes.Buffer + path, err := opts.Config.FindHelperBinary(rootlessport.BinaryName, false) + if err != nil { + return err + } + cmd := exec.Command(path) + cmd.Args = []string{rootlessport.BinaryName} + + // Leak one end of the pipe in rootlessport process, the other will be sent to conmon + cmd.ExtraFiles = append(cmd.ExtraFiles, opts.RootlessPortExitPipeR, syncW) + cmd.Stdin = cfgR + // stdout is for human-readable error, stderr is for debug log + cmd.Stdout = &stdout + cmd.Stderr = io.MultiWriter(logFile, &logrusDebugWriter{"rootlessport: "}) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start rootlessport process: %w", err) + } + defer func() { + servicereaper.AddPID(cmd.Process.Pid) + if err := cmd.Process.Release(); err != nil { + logrus.Errorf("Unable to release rootlessport process: %q", err) + } + }() + if err := waitForSync(syncR, cmd, logFile, 3*time.Second); err != nil { + stdoutStr := stdout.String() + if stdoutStr != "" { + // err contains full debug log and too verbose, so return stdoutStr + logrus.Debug(err) + return fmt.Errorf("rootlessport " + strings.TrimSuffix(stdoutStr, "\n")) + } + return err + } + logrus.Debug("rootlessport is ready") + return nil +} + +func setupRootlessPortMappingViaSlirp(ports []types.PortMapping, cmd *exec.Cmd, apiSocket string) (err error) { + const pidWaitTimeout = 60 * time.Second + chWait := make(chan error) + go func() { + interval := 25 * time.Millisecond + for i := time.Duration(0); i < pidWaitTimeout; i += interval { + // Check if the process is still running. + var status syscall.WaitStatus + pid, err := syscall.Wait4(cmd.Process.Pid, &status, syscall.WNOHANG, nil) + if err != nil { + break + } + if pid != cmd.Process.Pid { + continue + } + if status.Exited() || status.Signaled() { + chWait <- fmt.Errorf("slirp4netns exited with status %d", status.ExitStatus()) + } + time.Sleep(interval) + } + }() + defer close(chWait) + + // wait that API socket file appears before trying to use it. + if _, err := util.WaitForFile(apiSocket, chWait, pidWaitTimeout); err != nil { + return fmt.Errorf("waiting for slirp4nets to create the api socket file %s: %w", apiSocket, err) + } + + // for each port we want to add we need to open a connection to the slirp4netns control socket + // and send the add_hostfwd command. + for _, port := range ports { + protocols := strings.Split(port.Protocol, ",") + for _, protocol := range protocols { + hostIP := port.HostIP + if hostIP == "" { + hostIP = "0.0.0.0" + } + for i := uint16(0); i < port.Range; i++ { + if err := openSlirp4netnsPort(apiSocket, protocol, hostIP, port.HostPort+i, port.ContainerPort+i); err != nil { + return err + } + } + } + } + logrus.Debug("slirp4netns port-forwarding setup via add_hostfwd is ready") + return nil +} + +// openSlirp4netnsPort sends the slirp4netns pai quey to the given socket +func openSlirp4netnsPort(apiSocket, proto, hostip string, hostport, guestport uint16) error { + conn, err := net.Dial("unix", apiSocket) + if err != nil { + return fmt.Errorf("cannot open connection to %s: %w", apiSocket, err) + } + defer func() { + if err := conn.Close(); err != nil { + logrus.Errorf("Unable to close slirp4netns connection: %q", err) + } + }() + apiCmd := slirp4netnsCmd{ + Execute: "add_hostfwd", + Args: slirp4netnsCmdArg{ + Proto: proto, + HostAddr: hostip, + HostPort: hostport, + GuestPort: guestport, + }, + } + // create the JSON payload and send it. Mark the end of request shutting down writes + // to the socket, as requested by slirp4netns. + data, err := json.Marshal(&apiCmd) + if err != nil { + return fmt.Errorf("cannot marshal JSON for slirp4netns: %w", err) + } + if _, err := conn.Write([]byte(fmt.Sprintf("%s\n", data))); err != nil { + return fmt.Errorf("cannot write to control socket %s: %w", apiSocket, err) + } + if err := conn.(*net.UnixConn).CloseWrite(); err != nil { + return fmt.Errorf("cannot shutdown the socket %s: %w", apiSocket, err) + } + buf := make([]byte, 2048) + readLength, err := conn.Read(buf) + if err != nil { + return fmt.Errorf("cannot read from control socket %s: %w", apiSocket, err) + } + // if there is no 'error' key in the received JSON data, then the operation was + // successful. + var y map[string]interface{} + if err := json.Unmarshal(buf[0:readLength], &y); err != nil { + return fmt.Errorf("parsing error status from slirp4netns: %w", err) + } + if e, found := y["error"]; found { + return fmt.Errorf("from slirp4netns while setting up port redirection: %v", e) + } + return nil +} + +func GetRootlessPortChildIP(slirpSubnet *net.IPNet, netStatus map[string]types.StatusBlock) string { + if slirpSubnet != nil { + slirp4netnsIP, err := GetIP(slirpSubnet) + if err != nil { + return "" + } + return slirp4netnsIP.String() + } + + var ipv6 net.IP + for _, status := range netStatus { + for _, netInt := range status.Interfaces { + for _, netAddress := range netInt.Subnets { + ipv4 := netAddress.IPNet.IP.To4() + if ipv4 != nil { + return ipv4.String() + } + ipv6 = netAddress.IPNet.IP + } + } + } + if ipv6 != nil { + return ipv6.String() + } + return "" +} + +// closeQuiet closes a file and logs any error. Should only be used within +// a defer. +func closeQuiet(f *os.File) { + if err := f.Close(); err != nil { + logrus.Errorf("Unable to close file %s: %q", f.Name(), err) + } +} diff --git a/pkg/rootlessport/rootlessport_linux.go b/vendor/github.com/containers/common/pkg/rootlessport/rootlessport_linux.go similarity index 64% rename from pkg/rootlessport/rootlessport_linux.go rename to vendor/github.com/containers/common/pkg/rootlessport/rootlessport_linux.go index 6918a7e8c2..7ea018a238 100644 --- a/pkg/rootlessport/rootlessport_linux.go +++ b/vendor/github.com/containers/common/pkg/rootlessport/rootlessport_linux.go @@ -1,15 +1,7 @@ //go:build linux // +build linux -// Package rootlessport provides reexec for RootlessKit-based port forwarder. -// -// init() contains reexec.Register() for ReexecKey . -// -// The reexec requires Config to be provided via stdin. -// -// The reexec writes human-readable error message on stdout on error. -// -// Debug log is printed on stderr. +// Rootlessport Config type for use in podman/cmd/rootlessport. package rootlessport import ( diff --git a/pkg/servicereaper/service.go b/vendor/github.com/containers/common/pkg/servicereaper/service.go similarity index 96% rename from pkg/servicereaper/service.go rename to vendor/github.com/containers/common/pkg/servicereaper/service.go index 61445bc197..54baef3137 100644 --- a/pkg/servicereaper/service.go +++ b/vendor/github.com/containers/common/pkg/servicereaper/service.go @@ -56,7 +56,7 @@ func reaper(sigc chan os.Signal) { if waitpid == 0 { continue } - if status.Exited() { + if status.Exited() || status.Signaled() { delete(s.pidMap, pid) } } diff --git a/vendor/github.com/containers/common/pkg/util/util.go b/vendor/github.com/containers/common/pkg/util/util.go index 86688ee2cc..e396f0fc08 100644 --- a/vendor/github.com/containers/common/pkg/util/util.go +++ b/vendor/github.com/containers/common/pkg/util/util.go @@ -3,11 +3,16 @@ package util import ( "bytes" "fmt" + "os" "os/exec" + "path/filepath" "regexp" "strings" + "time" "github.com/containers/common/libnetwork/types" + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" ) const ( @@ -131,3 +136,54 @@ func FilterID(id string, filters []string) bool { } return false } + +// WaitForFile waits until a file has been created or the given timeout has occurred +func WaitForFile(path string, chWait chan error, timeout time.Duration) (bool, error) { + var inotifyEvents chan fsnotify.Event + watcher, err := fsnotify.NewWatcher() + if err == nil { + if err := watcher.Add(filepath.Dir(path)); err == nil { + inotifyEvents = watcher.Events + } + defer func() { + if err := watcher.Close(); err != nil { + logrus.Errorf("Failed to close fsnotify watcher: %v", err) + } + }() + } + + var timeoutChan <-chan time.Time + + if timeout != 0 { + timeoutChan = time.After(timeout) + } + + for { + select { + case e := <-chWait: + return true, e + case <-inotifyEvents: + _, err := os.Stat(path) + if err == nil { + return false, nil + } + if !os.IsNotExist(err) { + return false, err + } + case <-time.After(25 * time.Millisecond): + // Check periodically for the file existence. It is needed + // if the inotify watcher could not have been created. It is + // also useful when using inotify as if for any reasons we missed + // a notification, we won't hang the process. + _, err := os.Stat(path) + if err == nil { + return false, nil + } + if !os.IsNotExist(err) { + return false, err + } + case <-timeoutChan: + return false, fmt.Errorf("timed out waiting for file %s", path) + } + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 6fc4d7e393..ae39d1148a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -125,7 +125,7 @@ github.com/containers/buildah/pkg/rusage github.com/containers/buildah/pkg/sshagent github.com/containers/buildah/pkg/util github.com/containers/buildah/util -# github.com/containers/common v0.53.1-0.20230621115248-a2cd3ea30337 +# github.com/containers/common v0.53.1-0.20230621174116-586a3be4e1fc ## explicit; go 1.18 github.com/containers/common/libimage github.com/containers/common/libimage/define @@ -137,6 +137,7 @@ github.com/containers/common/libnetwork/netavark github.com/containers/common/libnetwork/network github.com/containers/common/libnetwork/pasta github.com/containers/common/libnetwork/resolvconf +github.com/containers/common/libnetwork/slirp4netns github.com/containers/common/libnetwork/types github.com/containers/common/libnetwork/util github.com/containers/common/pkg/apparmor @@ -163,11 +164,13 @@ github.com/containers/common/pkg/report github.com/containers/common/pkg/report/camelcase github.com/containers/common/pkg/resize github.com/containers/common/pkg/retry +github.com/containers/common/pkg/rootlessport github.com/containers/common/pkg/seccomp github.com/containers/common/pkg/secrets github.com/containers/common/pkg/secrets/filedriver github.com/containers/common/pkg/secrets/passdriver github.com/containers/common/pkg/secrets/shelldriver +github.com/containers/common/pkg/servicereaper github.com/containers/common/pkg/signal github.com/containers/common/pkg/ssh github.com/containers/common/pkg/subscriptions