mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
When running:
docker network create --ipv6 b46
docker run --rm -ti \
--network name=b46,driver-opt=com.docker.network.endpoint.sysctls=net.ipv6.conf.IFNAME.disable_ipv6=1 \
busybox
IPv6 is enabled in the container and the network, so an IPv6 address
will be allocated for the endpoint.
But, when the sysctl is applied, the IPv6 address will be removed
from the interface ... so, no unsolicited neighbour advertisement
should be (or can be) sent and, the endpoint should not be treated
as dual-stack when selecting a gateway endpoint and, if it is
selected as the gateway endpoint, setting up an IPv6 route via the
network will fail.
So, if the IPv6 address disappears after sysctls have been applied,
release the address and remove it from the endpoint's config.
Signed-off-by: Rob Murray <rob.murray@docker.com>
332 lines
7.9 KiB
Go
332 lines
7.9 KiB
Go
//go:build linux || freebsd
|
|
|
|
package osl
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"slices"
|
|
|
|
"github.com/moby/moby/v2/daemon/libnetwork/types"
|
|
"github.com/vishvananda/netlink"
|
|
)
|
|
|
|
// Gateway returns the IPv4 gateway for the sandbox.
|
|
func (n *Namespace) Gateway() net.IP {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
return n.gw
|
|
}
|
|
|
|
// GatewayIPv6 returns the IPv6 gateway for the sandbox.
|
|
func (n *Namespace) GatewayIPv6() net.IP {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
return n.gwv6
|
|
}
|
|
|
|
// StaticRoutes returns additional static routes for the sandbox. Note that
|
|
// directly connected routes are stored on the particular interface they
|
|
// refer to.
|
|
func (n *Namespace) StaticRoutes() []*types.StaticRoute {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
routes := make([]*types.StaticRoute, len(n.staticRoutes))
|
|
for i, route := range n.staticRoutes {
|
|
routes[i] = route.Copy()
|
|
}
|
|
|
|
return routes
|
|
}
|
|
|
|
// SetGateway sets the default IPv4 gateway for the sandbox. It is a no-op
|
|
// if the given gateway is empty.
|
|
func (n *Namespace) SetGateway(gw net.IP) error {
|
|
if len(gw) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := n.programGateway(gw, true); err != nil {
|
|
return err
|
|
}
|
|
n.mu.Lock()
|
|
n.gw = gw
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// UnsetGateway the previously set default IPv4 gateway in the sandbox.
|
|
// It is a no-op if no gateway was set.
|
|
func (n *Namespace) UnsetGateway() error {
|
|
gw := n.Gateway()
|
|
if len(gw) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := n.programGateway(gw, false); err != nil {
|
|
return err
|
|
}
|
|
n.mu.Lock()
|
|
n.gw = net.IP{}
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (n *Namespace) programGateway(gw net.IP, isAdd bool) error {
|
|
gwRoutes, err := n.nlHandle.RouteGet(gw)
|
|
if err != nil {
|
|
return fmt.Errorf("route for the gateway %s could not be found: %v", gw, err)
|
|
}
|
|
|
|
var linkIndex int
|
|
for _, gwRoute := range gwRoutes {
|
|
if gwRoute.Gw == nil {
|
|
linkIndex = gwRoute.LinkIndex
|
|
break
|
|
}
|
|
}
|
|
|
|
if linkIndex == 0 {
|
|
return fmt.Errorf("direct route for the gateway %s could not be found", gw)
|
|
}
|
|
|
|
if isAdd {
|
|
return n.nlHandle.RouteAdd(&netlink.Route{
|
|
Scope: netlink.SCOPE_UNIVERSE,
|
|
LinkIndex: linkIndex,
|
|
Gw: gw,
|
|
})
|
|
}
|
|
|
|
return n.nlHandle.RouteDel(&netlink.Route{
|
|
Scope: netlink.SCOPE_UNIVERSE,
|
|
LinkIndex: linkIndex,
|
|
Gw: gw,
|
|
})
|
|
}
|
|
|
|
// Program a route in to the namespace routing table.
|
|
func (n *Namespace) programRoute(dest *net.IPNet, nh net.IP) error {
|
|
gwRoutes, err := n.nlHandle.RouteGet(nh)
|
|
if err != nil {
|
|
return fmt.Errorf("route for the next hop %s could not be found: %v", nh, err)
|
|
}
|
|
|
|
return n.nlHandle.RouteAdd(&netlink.Route{
|
|
Scope: netlink.SCOPE_UNIVERSE,
|
|
LinkIndex: gwRoutes[0].LinkIndex,
|
|
Gw: nh,
|
|
Dst: dest,
|
|
})
|
|
}
|
|
|
|
// Delete a route from the namespace routing table.
|
|
func (n *Namespace) removeRoute(dest *net.IPNet, nh net.IP) error {
|
|
gwRoutes, err := n.nlHandle.RouteGet(nh)
|
|
if err != nil {
|
|
return fmt.Errorf("route for the next hop could not be found: %v", err)
|
|
}
|
|
|
|
return n.nlHandle.RouteDel(&netlink.Route{
|
|
Scope: netlink.SCOPE_UNIVERSE,
|
|
LinkIndex: gwRoutes[0].LinkIndex,
|
|
Gw: nh,
|
|
Dst: dest,
|
|
})
|
|
}
|
|
|
|
// SetGatewayIPv6 sets the default IPv6 gateway for the sandbox. It is a no-op
|
|
// if the given gateway is empty.
|
|
func (n *Namespace) SetGatewayIPv6(gwv6 net.IP) error {
|
|
if len(gwv6) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := n.programGateway(gwv6, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
n.mu.Lock()
|
|
n.gwv6 = gwv6
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// UnsetGatewayIPv6 unsets the previously set default IPv6 gateway in the sandbox.
|
|
// It is a no-op if no gateway was set.
|
|
func (n *Namespace) UnsetGatewayIPv6() error {
|
|
gwv6 := n.GatewayIPv6()
|
|
if len(gwv6) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := n.programGateway(gwv6, false); err != nil {
|
|
return err
|
|
}
|
|
|
|
n.mu.Lock()
|
|
n.gwv6 = net.IP{}
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// AddStaticRoute adds a static route to the sandbox.
|
|
func (n *Namespace) AddStaticRoute(r *types.StaticRoute) error {
|
|
if err := n.programRoute(r.Destination, r.NextHop); err != nil {
|
|
return err
|
|
}
|
|
|
|
n.mu.Lock()
|
|
n.staticRoutes = append(n.staticRoutes, r)
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// RemoveStaticRoute removes a static route from the sandbox.
|
|
func (n *Namespace) RemoveStaticRoute(r *types.StaticRoute) error {
|
|
if err := n.removeRoute(r.Destination, r.NextHop); err != nil {
|
|
return err
|
|
}
|
|
|
|
n.mu.Lock()
|
|
lastIndex := len(n.staticRoutes) - 1
|
|
for i, v := range n.staticRoutes {
|
|
if v == r {
|
|
// Overwrite the route we're removing with the last element
|
|
n.staticRoutes[i] = n.staticRoutes[lastIndex]
|
|
// Shorten the slice to trim the extra element
|
|
n.staticRoutes = n.staticRoutes[:lastIndex]
|
|
break
|
|
}
|
|
}
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// SetDefaultRouteIPv4 sets up a connected route to 0.0.0.0 via the Interface
|
|
// with srcName, if that Interface has a route to 0.0.0.0. Otherwise, it
|
|
// returns an error.
|
|
func (n *Namespace) SetDefaultRouteIPv4(srcName string) error {
|
|
if err := n.setDefaultRoute(srcName, func(ipNet *net.IPNet) bool {
|
|
return ipNet.IP.IsUnspecified() && ipNet.IP.To4() != nil
|
|
}); err != nil {
|
|
return fmt.Errorf("setting IPv4 default route to interface with srcName '%s': %w", srcName, err)
|
|
}
|
|
|
|
n.mu.Lock()
|
|
n.defRoute4SrcName = srcName
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// SetDefaultRouteIPv6 sets up a connected route to [::] via the Interface
|
|
// with srcName, if that Interface has a route to [::]. Otherwise, it
|
|
// returns an error.
|
|
func (n *Namespace) SetDefaultRouteIPv6(srcName string) error {
|
|
if err := n.setDefaultRoute(srcName, func(ipNet *net.IPNet) bool {
|
|
return ipNet.IP.IsUnspecified() && ipNet.IP.To4() == nil
|
|
}); err != nil {
|
|
return fmt.Errorf("setting IPv6 default route to interface with srcName '%s': %w", srcName, err)
|
|
}
|
|
|
|
n.mu.Lock()
|
|
n.defRoute6SrcName = srcName
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (n *Namespace) setDefaultRoute(srcName string, routeMatcher func(*net.IPNet) bool) error {
|
|
iface := n.InterfaceBySrcName(srcName)
|
|
if iface == nil {
|
|
return errors.New("no interface")
|
|
}
|
|
|
|
ridx := slices.IndexFunc(iface.routes, routeMatcher)
|
|
if ridx == -1 {
|
|
return errors.New("no default route")
|
|
}
|
|
|
|
link, err := n.nlHandle.LinkByName(iface.dstName)
|
|
if err != nil {
|
|
return fmt.Errorf("no link src:%s dst:%s", srcName, iface.dstName)
|
|
}
|
|
|
|
if err := n.nlHandle.RouteAdd(&netlink.Route{
|
|
Scope: netlink.SCOPE_LINK,
|
|
LinkIndex: link.Attrs().Index,
|
|
Dst: iface.routes[ridx],
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnsetDefaultRouteIPv4 unsets the previously set default IPv4 default route
|
|
// in the sandbox. It is a no-op if no gateway was set.
|
|
func (n *Namespace) UnsetDefaultRouteIPv4() error {
|
|
n.mu.Lock()
|
|
srcName := n.defRoute4SrcName
|
|
n.mu.Unlock()
|
|
|
|
if err := n.unsetDefaultRoute(srcName, func(ipNet *net.IPNet) bool {
|
|
return ipNet.IP.IsUnspecified() && ipNet.IP.To4() != nil
|
|
}); err != nil {
|
|
return fmt.Errorf("removing IPv4 default route to interface with srcName '%s': %w", srcName, err)
|
|
}
|
|
|
|
n.mu.Lock()
|
|
n.defRoute4SrcName = ""
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// UnsetDefaultRouteIPv6 unsets the previously set default IPv6 default route
|
|
// in the sandbox. It is a no-op if no gateway was set.
|
|
func (n *Namespace) UnsetDefaultRouteIPv6() error {
|
|
n.mu.Lock()
|
|
srcName := n.defRoute6SrcName
|
|
n.mu.Unlock()
|
|
|
|
if err := n.unsetDefaultRoute(srcName, func(ipNet *net.IPNet) bool {
|
|
return ipNet.IP.IsUnspecified() && ipNet.IP.To4() == nil
|
|
}); err != nil {
|
|
return fmt.Errorf("removing IPv6 default route to interface with srcName '%s': %w", srcName, err)
|
|
}
|
|
|
|
n.mu.Lock()
|
|
n.defRoute6SrcName = ""
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (n *Namespace) unsetDefaultRoute(srcName string, routeMatcher func(*net.IPNet) bool) error {
|
|
if srcName == "" {
|
|
return nil
|
|
}
|
|
|
|
iface := n.InterfaceBySrcName(srcName)
|
|
if iface == nil {
|
|
return nil
|
|
}
|
|
|
|
ridx := slices.IndexFunc(iface.routes, routeMatcher)
|
|
if ridx == -1 {
|
|
return errors.New("no default route")
|
|
}
|
|
|
|
link, err := n.nlHandle.LinkByName(iface.dstName)
|
|
if err != nil {
|
|
return errors.New("no link")
|
|
}
|
|
|
|
return n.nlHandle.RouteDel(&netlink.Route{
|
|
Scope: netlink.SCOPE_LINK,
|
|
LinkIndex: link.Attrs().Index,
|
|
Dst: iface.routes[ridx],
|
|
})
|
|
}
|