From e6b6f2aa11729bf2029b7718165fcc21c3b248be Mon Sep 17 00:00:00 2001 From: Nayana Bidari Date: Mon, 17 Mar 2025 10:06:04 -0700 Subject: [PATCH] Allow IPv6 addresses to be enabled/disabled in runsc based on the sysctl. By default in runsc, we have both IPv4 and IPv6 addresses enabled on all the interfaces. However, in runc this is based on the sysctl net.ipv6.conf.all.disable_ipv6. This CL will make runsc behave similar to runc. - If net.ipv6.conf.all.disable_ipv6 is > 0, then only IPv4 addresses will be enabled on the interfaces including loopback when network mode is "sandbox". PiperOrigin-RevId: 737656607 --- runsc/container/container.go | 8 +-- runsc/container/container_test.go | 85 +++++++++++++++++++++++++++++++ runsc/sandbox/network.go | 39 +++++++++----- runsc/sandbox/sandbox.go | 33 ++++++++++-- 4 files changed, 144 insertions(+), 21 deletions(-) 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) }