diff --git a/runsc/container/container.go b/runsc/container/container.go index 306b408c9..f3c60a105 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -422,13 +422,13 @@ func (c *Container) Start(conf *config.Config) error { func (c *Container) Restore(conf *config.Config, imagePath string, direct, background bool) error { log.Debugf("Restore container, cid: %s", c.ID) - restore := func(conf *config.Config) error { - return c.Sandbox.Restore(conf, c.ID, imagePath, direct, background) + restore := func(conf *config.Config, spec *specs.Spec) error { + return c.Sandbox.Restore(conf, spec, c.ID, imagePath, direct, background) } return c.startImpl(conf, "restore", restore, c.Sandbox.RestoreSubcontainer) } -func (c *Container) startImpl(conf *config.Config, action string, startRoot func(conf *config.Config) error, startSub func(spec *specs.Spec, conf *config.Config, cid string, stdios, goferFiles, goferFilestores []*os.File, devIOFile *os.File, goferConfs []boot.GoferMountConf) error) error { +func (c *Container) startImpl(conf *config.Config, action string, startRoot func(conf *config.Config, spec *specs.Spec) error, startSub func(spec *specs.Spec, conf *config.Config, cid string, stdios, goferFiles, goferFilestores []*os.File, devIOFile *os.File, goferConfs []boot.GoferMountConf) error) error { if err := c.Saver.lock(BlockAcquire); err != nil { return err } @@ -446,7 +446,7 @@ func (c *Container) startImpl(conf *config.Config, action string, startRoot func } if isRoot(c.Spec) { - if err := startRoot(conf); err != nil { + if err := startRoot(conf, c.Spec); err != nil { return err } } else { diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index 9eab25efb..9fe69d7df 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -18,6 +18,7 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "math" "math/rand" "os" @@ -4077,3 +4078,87 @@ func TestCheckpointResume(t *testing.T) { }) } } + +func TestIPv6DisableAllSysctl(t *testing.T) { + tests := []struct { + name string + ipv6Disabled bool + }{ + {"IPv6Disabled", true}, + {"IPv6Enabled", false}, + } + for name, conf := range configs(t, true /* noOverlay */) { + for _, test := range tests { + t.Run(test.name+name, func(t *testing.T) { + dir, err := os.MkdirTemp(testutil.TmpDir(), "ipv6-test") + if err != nil { + t.Fatalf("os.MkdirTemp failed: %v", err) + } + defer os.RemoveAll(dir) + if err := os.Chmod(dir, 0777); err != nil { + t.Fatalf("error chmoding file: %q, %v", dir, err) + } + + outputPath := filepath.Join(dir, "output") + outputFile, err := createWriteableOutputFile(outputPath) + if err != nil { + t.Fatalf("error creating output file: %v", err) + } + defer outputFile.Close() + + script := fmt.Sprintf("ip addr >> %q", outputPath) + spec := testutil.NewSpecWithArgs("bash", "-c", script) + conf.Network = config.NetworkSandbox + if test.ipv6Disabled { + spec.Linux = &specs.Linux{} + spec.Linux.Sysctl = make(map[string]string) + spec.Linux.Sysctl["net.ipv6.conf.all.disable_ipv6"] = "1" + } + _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer cleanup() + + // Create and start the container. + args := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir, + } + cont, err := New(conf, args) + if err != nil { + t.Fatalf("error creating container: %v", err) + } + if err := cont.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + // Wait until application has ran. + if err := waitForFileNotEmpty(outputFile); err != nil { + // This can happen when the network does not + // have any network interfaces configured. + // We cannot test whether the sysctl works + // properly in this case. Log a warning and + // return. + t.Logf("No network interfaces are configured: %v", err) + return + } + + content, err := ioutil.ReadFile(outputPath) + if err != nil { + fmt.Println("Error reading file:", err) + return + } + res := strings.Contains(string(content), "inet6") + if test.ipv6Disabled && res { + t.Fatalf("IPv6 address present when IPv6 is disabled on all interfaces") + } + + if !test.ipv6Disabled && !res { + t.Fatalf("IPv6 address not present when IPv6 is enabled on all interfaces") + } + }) + } + } +} diff --git a/runsc/sandbox/network.go b/runsc/sandbox/network.go index 7d4a3b5c6..e8c804893 100644 --- a/runsc/sandbox/network.go +++ b/runsc/sandbox/network.go @@ -52,7 +52,7 @@ import ( // Run the following container to test it: // // docker run -di --runtime=runsc -p 8080:80 -v $PWD:/usr/local/apache2/htdocs/ httpd:2.4 -func setupNetwork(conn *urpc.Client, pid int, conf *config.Config) error { +func setupNetwork(conn *urpc.Client, pid int, conf *config.Config, disableIPv6 bool) error { log.Infof("Setting up network") switch conf.Network { @@ -65,7 +65,7 @@ func setupNetwork(conn *urpc.Client, pid int, conf *config.Config) error { // Build the path to the net namespace of the sandbox process. // This is what we will copy. nsPath := filepath.Join("/proc", strconv.Itoa(pid), "ns/net") - if err := createInterfacesAndRoutesFromNS(conn, nsPath, conf); err != nil { + if err := createInterfacesAndRoutesFromNS(conn, nsPath, conf, disableIPv6); err != nil { return fmt.Errorf("creating interfaces from net namespace %q: %v", nsPath, err) } case config.NetworkHost: @@ -125,7 +125,7 @@ func isRootNetNS() (bool, error) { // createInterfacesAndRoutesFromNS scrapes the interface and routes from the // net namespace with the given path, creates them in the sandbox, and removes // them from the host. -func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *config.Config) error { +func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *config.Config, disableIPv6 bool) error { switch conf.XDP.Mode { case config.XDPModeOff: case config.XDPModeNS: @@ -168,6 +168,7 @@ func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *con args := boot.CreateLinksAndRoutesArgs{ DisconnectOk: conf.NetDisconnectOk, } + for _, iface := range ifaces { if iface.Flags&net.FlagUp == 0 { log.Infof("Skipping down interface: %+v", iface) @@ -181,7 +182,7 @@ func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *con // We build our own loopback device. if iface.Flags&net.FlagLoopback != 0 { - link, err := loopbackLink(conf, iface, allAddrs) + link, err := loopbackLink(conf, iface, allAddrs, disableIPv6) if err != nil { return fmt.Errorf("getting loopback link for iface %q: %w", iface.Name, err) } @@ -195,6 +196,10 @@ func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *con if !ok { return fmt.Errorf("address is not IPNet: %+v", ifaddr) } + // Do not add IPv6 addresses when IPv6 is disabled. + if disableIPv6 && ipNet.IP.To4() == nil { + continue + } ipAddrs = append(ipAddrs, ipNet) } if len(ipAddrs) == 0 { @@ -221,7 +226,7 @@ func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *con // Scrape the routes before removing the address, since that // will remove the routes as well. - routes, defv4, defv6, err := routesForIface(iface) + routes, defv4, defv6, err := routesForIface(iface, disableIPv6) if err != nil { return fmt.Errorf("getting routes for interface %q: %v", iface.Name, err) } @@ -460,7 +465,7 @@ func createSocket(iface net.Interface, ifaceLink netlink.Link, enableGSO bool) ( // loopbackLink returns the link with addresses and routes for a loopback // interface. -func loopbackLink(conf *config.Config, iface net.Interface, addrs []net.Addr) (boot.LoopbackLink, error) { +func loopbackLink(conf *config.Config, iface net.Interface, addrs []net.Addr, disableIPv6 bool) (boot.LoopbackLink, error) { link := boot.LoopbackLink{ Name: iface.Name, GVisorGRO: conf.GVisorGRO, @@ -471,6 +476,9 @@ func loopbackLink(conf *config.Config, iface net.Interface, addrs []net.Addr) (b return boot.LoopbackLink{}, fmt.Errorf("address is not IPNet: %+v", addr) } + if disableIPv6 && ipNet.IP.To4() == nil { + continue + } prefix, _ := ipNet.Mask.Size() link.Addresses = append(link.Addresses, boot.IPWithPrefix{ Address: ipNet.IP, @@ -488,7 +496,7 @@ func loopbackLink(conf *config.Config, iface net.Interface, addrs []net.Addr) (b // routesForIface iterates over all routes for the given interface and converts // them to boot.Routes. It also returns the a default v4/v6 route if found. -func routesForIface(iface net.Interface) ([]boot.Route, *boot.Route, *boot.Route, error) { +func routesForIface(iface net.Interface, disableIPv6 bool) ([]boot.Route, *boot.Route, *boot.Route, error) { link, err := netlink.LinkByIndex(iface.Index) if err != nil { return nil, nil, nil, err @@ -524,12 +532,14 @@ func routesForIface(iface net.Interface) ([]boot.Route, *boot.Route, *boot.Route return nil, nil, nil, fmt.Errorf("more than one default route found %q, def: %+v, route: %+v", iface.Name, defv6, r) } - defv6 = &boot.Route{ - Destination: net.IPNet{ - IP: net.IPv6zero, - Mask: net.IPMask(net.IPv6zero), - }, - Gateway: r.Gw, + if !disableIPv6 { + defv6 = &boot.Route{ + Destination: net.IPNet{ + IP: net.IPv6zero, + Mask: net.IPMask(net.IPv6zero), + }, + Gateway: r.Gw, + } } default: return nil, nil, nil, fmt.Errorf("unexpected address size for gateway: %+v for route: %+v", r.Gw, r) @@ -538,6 +548,9 @@ func routesForIface(iface net.Interface) ([]boot.Route, *boot.Route, *boot.Route } dst := *r.Dst + if disableIPv6 && dst.IP.To4() == nil { + continue + } dst.IP = dst.IP.Mask(dst.Mask) routes = append(routes, boot.Route{ Destination: dst, diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index a56c2d656..f4986d2cf 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -407,8 +407,23 @@ func (s *Sandbox) CreateSubcontainer(conf *config.Config, cid string, tty *os.Fi return nil } +func getDisableIPv6(spec *specs.Spec) (bool, error) { + if spec.Linux == nil || spec.Linux.Sysctl == nil { + return false, nil + } + val, ok := spec.Linux.Sysctl["net.ipv6.conf.all.disable_ipv6"] + if !ok { + return false, nil + } + valInt, err := strconv.Atoi(val) + if err != nil { + return false, fmt.Errorf("getting net.ipv6.conf.all.disable_ipv6=%s: %w", val, err) + } + return valInt != 0, nil +} + // StartRoot starts running the root container process inside the sandbox. -func (s *Sandbox) StartRoot(conf *config.Config) error { +func (s *Sandbox) StartRoot(conf *config.Config, spec *specs.Spec) error { if err := hostsettings.Handle(conf); err != nil { return fmt.Errorf("host settings: %w (use --host-settings=ignore to bypass)", err) } @@ -420,8 +435,13 @@ func (s *Sandbox) StartRoot(conf *config.Config) error { } defer conn.Close() + var disableIPv6 bool + disableIPv6, err = getDisableIPv6(spec) + if err != nil { + return err + } // Configure the network. - if err := setupNetwork(conn, pid, conf); err != nil { + if err := setupNetwork(conn, pid, conf, disableIPv6); err != nil { return fmt.Errorf("setting up network: %w", err) } @@ -472,7 +492,7 @@ func (s *Sandbox) StartSubcontainer(spec *specs.Spec, conf *config.Config, cid s } // Restore sends the restore call for a container in the sandbox. -func (s *Sandbox) Restore(conf *config.Config, cid string, imagePath string, direct, background bool) error { +func (s *Sandbox) Restore(conf *config.Config, spec *specs.Spec, cid string, imagePath string, direct, background bool) error { if err := hostsettings.Handle(conf); err != nil { return fmt.Errorf("host settings: %w (use --host-settings=ignore to bypass)", err) } @@ -535,8 +555,13 @@ func (s *Sandbox) Restore(conf *config.Config, cid string, imagePath string, dir } defer conn.Close() + var disableIPv6 bool + disableIPv6, err = getDisableIPv6(spec) + if err != nil { + return err + } // Configure the network. - if err := setupNetwork(conn, s.Pid.load(), conf); err != nil { + if err := setupNetwork(conn, s.Pid.load(), conf, disableIPv6); err != nil { return fmt.Errorf("setting up network: %v", err) }