mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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
704 lines
23 KiB
Go
704 lines
23 KiB
Go
// Copyright 2018 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/vishvananda/netlink"
|
|
"golang.org/x/sys/unix"
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/sentry/socket/plugin"
|
|
"gvisor.dev/gvisor/pkg/tcpip/header"
|
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
|
"gvisor.dev/gvisor/pkg/urpc"
|
|
"gvisor.dev/gvisor/runsc/boot"
|
|
"gvisor.dev/gvisor/runsc/config"
|
|
"gvisor.dev/gvisor/runsc/specutils"
|
|
)
|
|
|
|
// setupNetwork configures the network stack to mimic the local network
|
|
// configuration. Docker uses network namespaces with vnets to configure the
|
|
// network for the container. The untrusted app expects to see the same network
|
|
// inside the sandbox. Routing and port mapping is handled directly by docker
|
|
// with most of network information not even available to the runtime.
|
|
//
|
|
// Netstack inside the sandbox speaks directly to the device using a raw socket.
|
|
// All IP addresses assigned to the NIC, are removed and passed on to netstack's
|
|
// device.
|
|
//
|
|
// If 'conf.Network' is NoNetwork, skips local configuration and creates a
|
|
// loopback interface only.
|
|
//
|
|
// 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, disableIPv6 bool) error {
|
|
log.Infof("Setting up network")
|
|
|
|
switch conf.Network {
|
|
case config.NetworkNone:
|
|
log.Infof("Network is disabled, create loopback interface only")
|
|
if err := createDefaultLoopbackInterface(conf, conn); err != nil {
|
|
return fmt.Errorf("creating default loopback interface: %v", err)
|
|
}
|
|
case config.NetworkSandbox:
|
|
// 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, disableIPv6); err != nil {
|
|
return fmt.Errorf("creating interfaces from net namespace %q: %v", nsPath, err)
|
|
}
|
|
case config.NetworkHost:
|
|
// Nothing to do here.
|
|
case config.NetworkPlugin:
|
|
if err := initPluginStack(conn, pid, conf); err != nil {
|
|
return fmt.Errorf("failed to initialize external stack, error: %v", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("invalid network type: %v", conf.Network)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createDefaultLoopbackInterface(conf *config.Config, conn *urpc.Client) error {
|
|
link := boot.DefaultLoopbackLink
|
|
link.GVisorGRO = conf.GVisorGRO
|
|
if err := conn.Call(boot.NetworkCreateLinksAndRoutes, &boot.CreateLinksAndRoutesArgs{
|
|
LoopbackLinks: []boot.LoopbackLink{link},
|
|
DisconnectOk: conf.NetDisconnectOk,
|
|
}, nil); err != nil {
|
|
return fmt.Errorf("creating loopback link and routes: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func joinNetNS(nsPath string) (func(), error) {
|
|
runtime.LockOSThread()
|
|
restoreNS, err := specutils.ApplyNS(specs.LinuxNamespace{
|
|
Type: specs.NetworkNamespace,
|
|
Path: nsPath,
|
|
})
|
|
if err != nil {
|
|
runtime.UnlockOSThread()
|
|
return nil, fmt.Errorf("joining net namespace %q: %v", nsPath, err)
|
|
}
|
|
return func() {
|
|
restoreNS()
|
|
runtime.UnlockOSThread()
|
|
}, nil
|
|
}
|
|
|
|
// isRootNetNS determines whether we are running in the root net namespace.
|
|
// /proc/sys/net/core/dev_weight only exists in root network namespace.
|
|
func isRootNetNS() (bool, error) {
|
|
err := unix.Access("/proc/sys/net/core/dev_weight", unix.F_OK)
|
|
switch err {
|
|
case nil:
|
|
return true, nil
|
|
case unix.ENOENT:
|
|
return false, nil
|
|
default:
|
|
return false, fmt.Errorf("failed to access /proc/sys/net/core/dev_weight: %v", err)
|
|
}
|
|
}
|
|
|
|
// 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, disableIPv6 bool) error {
|
|
switch conf.XDP.Mode {
|
|
case config.XDPModeOff:
|
|
case config.XDPModeNS:
|
|
case config.XDPModeRedirect:
|
|
if err := createRedirectInterfacesAndRoutes(conn, conf); err != nil {
|
|
return fmt.Errorf("failed to create XDP redirect interface: %w", err)
|
|
}
|
|
return nil
|
|
case config.XDPModeTunnel:
|
|
if err := createXDPTunnel(conn, nsPath, conf); err != nil {
|
|
return fmt.Errorf("failed to create XDP tunnel: %w", err)
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown XDP mode: %v", conf.XDP.Mode)
|
|
}
|
|
|
|
// Join the network namespace that we will be copying.
|
|
restore, err := joinNetNS(nsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer restore()
|
|
|
|
// Get all interfaces in the namespace.
|
|
ifaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return fmt.Errorf("querying interfaces: %w", err)
|
|
}
|
|
|
|
isRoot, err := isRootNetNS()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isRoot {
|
|
return fmt.Errorf("cannot run with network enabled in root network namespace")
|
|
}
|
|
|
|
// Collect addresses and routes from the interfaces.
|
|
args := boot.CreateLinksAndRoutesArgs{
|
|
DisconnectOk: conf.NetDisconnectOk,
|
|
}
|
|
|
|
for _, iface := range ifaces {
|
|
if iface.Flags&net.FlagUp == 0 {
|
|
log.Infof("Skipping down interface: %+v", iface)
|
|
continue
|
|
}
|
|
|
|
allAddrs, err := iface.Addrs()
|
|
if err != nil {
|
|
return fmt.Errorf("fetching interface addresses for %q: %w", iface.Name, err)
|
|
}
|
|
|
|
// We build our own loopback device.
|
|
if iface.Flags&net.FlagLoopback != 0 {
|
|
link, err := loopbackLink(conf, iface, allAddrs, disableIPv6)
|
|
if err != nil {
|
|
return fmt.Errorf("getting loopback link for iface %q: %w", iface.Name, err)
|
|
}
|
|
args.LoopbackLinks = append(args.LoopbackLinks, link)
|
|
continue
|
|
}
|
|
|
|
var ipAddrs []*net.IPNet
|
|
for _, ifaddr := range allAddrs {
|
|
ipNet, ok := ifaddr.(*net.IPNet)
|
|
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 {
|
|
log.Warningf("No usable IP addresses found for interface %q, skipping", iface.Name)
|
|
continue
|
|
}
|
|
|
|
// Collect data from the ARP table.
|
|
dump, err := netlink.NeighList(iface.Index, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("fetching ARP table for %q: %w", iface.Name, err)
|
|
}
|
|
|
|
var neighbors []boot.Neighbor
|
|
for _, n := range dump {
|
|
// There are only two "good" states NUD_PERMANENT and NUD_REACHABLE,
|
|
// but NUD_REACHABLE is fully dynamic and will be re-probed anyway.
|
|
if n.State == netlink.NUD_PERMANENT {
|
|
log.Debugf("Copying a static ARP entry: %+v %+v", n.IP, n.HardwareAddr)
|
|
// No flags are copied because Stack.AddStaticNeighbor does not support flags right now.
|
|
neighbors = append(neighbors, boot.Neighbor{IP: n.IP, HardwareAddr: n.HardwareAddr})
|
|
}
|
|
}
|
|
|
|
// Scrape the routes before removing the address, since that
|
|
// will remove the routes as well.
|
|
routes, defv4, defv6, err := routesForIface(iface, disableIPv6)
|
|
if err != nil {
|
|
return fmt.Errorf("getting routes for interface %q: %v", iface.Name, err)
|
|
}
|
|
if defv4 != nil {
|
|
if !args.Defaultv4Gateway.Route.Empty() {
|
|
return fmt.Errorf("more than one default route found, interface: %v, route: %v, default route: %+v", iface.Name, defv4, args.Defaultv4Gateway)
|
|
}
|
|
args.Defaultv4Gateway.Route = *defv4
|
|
args.Defaultv4Gateway.Name = iface.Name
|
|
}
|
|
|
|
if defv6 != nil {
|
|
if !args.Defaultv6Gateway.Route.Empty() {
|
|
return fmt.Errorf("more than one default route found, interface: %v, route: %v, default route: %+v", iface.Name, defv6, args.Defaultv6Gateway)
|
|
}
|
|
args.Defaultv6Gateway.Route = *defv6
|
|
args.Defaultv6Gateway.Name = iface.Name
|
|
}
|
|
|
|
// Get the link for the interface.
|
|
ifaceLink, err := netlink.LinkByName(iface.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("getting link for interface %q: %w", iface.Name, err)
|
|
}
|
|
linkAddress := ifaceLink.Attrs().HardwareAddr
|
|
|
|
// Collect the addresses for the interface, enable forwarding,
|
|
// and remove them from the host.
|
|
var addresses []boot.IPWithPrefix
|
|
for _, addr := range ipAddrs {
|
|
prefix, _ := addr.Mask.Size()
|
|
addresses = append(addresses, boot.IPWithPrefix{Address: addr.IP, PrefixLen: prefix})
|
|
|
|
// Steal IP address from NIC.
|
|
if err := removeAddress(ifaceLink, addr.String()); err != nil {
|
|
// If we encounter an error while deleting the ip,
|
|
// verify the ip is still present on the interface.
|
|
if present, err := isAddressOnInterface(iface.Name, addr); err != nil {
|
|
return fmt.Errorf("checking if address %v is on interface %q: %w", addr, iface.Name, err)
|
|
} else if !present {
|
|
continue
|
|
}
|
|
return fmt.Errorf("removing address %v from device %q: %w", addr, iface.Name, err)
|
|
}
|
|
}
|
|
|
|
if conf.XDP.Mode == config.XDPModeNS {
|
|
xdpSockFDs, err := createSocketXDP(iface)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create XDP socket: %v", err)
|
|
}
|
|
args.FilePayload.Files = append(args.FilePayload.Files, xdpSockFDs...)
|
|
args.XDPLinks = append(args.XDPLinks, boot.XDPLink{
|
|
Name: iface.Name,
|
|
InterfaceIndex: iface.Index,
|
|
Routes: routes,
|
|
TXChecksumOffload: conf.TXChecksumOffload,
|
|
RXChecksumOffload: conf.RXChecksumOffload,
|
|
NumChannels: conf.NumNetworkChannels,
|
|
QDisc: conf.QDisc,
|
|
Neighbors: neighbors,
|
|
LinkAddress: linkAddress,
|
|
Addresses: addresses,
|
|
GVisorGRO: conf.GVisorGRO,
|
|
})
|
|
} else {
|
|
link := boot.FDBasedLink{
|
|
Name: iface.Name,
|
|
MTU: iface.MTU,
|
|
Routes: routes,
|
|
TXChecksumOffload: conf.TXChecksumOffload,
|
|
RXChecksumOffload: conf.RXChecksumOffload,
|
|
NumChannels: conf.NumNetworkChannels,
|
|
ProcessorsPerChannel: conf.NetworkProcessorsPerChannel,
|
|
QDisc: conf.QDisc,
|
|
Neighbors: neighbors,
|
|
LinkAddress: linkAddress,
|
|
Addresses: addresses,
|
|
}
|
|
|
|
log.Debugf("Setting up network channels")
|
|
// Create the socket for the device.
|
|
for i := 0; i < link.NumChannels; i++ {
|
|
log.Debugf("Creating Channel %d", i)
|
|
socketEntry, err := createSocket(iface, ifaceLink, conf.HostGSO)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to createSocket for %s : %w", iface.Name, err)
|
|
}
|
|
if i == 0 {
|
|
link.GSOMaxSize = socketEntry.gsoMaxSize
|
|
} else {
|
|
if link.GSOMaxSize != socketEntry.gsoMaxSize {
|
|
return fmt.Errorf("inconsistent gsoMaxSize %d and %d when creating multiple channels for same interface: %s",
|
|
link.GSOMaxSize, socketEntry.gsoMaxSize, iface.Name)
|
|
}
|
|
}
|
|
args.FilePayload.Files = append(args.FilePayload.Files, socketEntry.deviceFile)
|
|
}
|
|
|
|
if link.GSOMaxSize == 0 && conf.GVisorGSO {
|
|
// Host GSO is disabled. Let's enable gVisor GSO.
|
|
link.GSOMaxSize = stack.GVisorGSOMaxSize
|
|
link.GVisorGSOEnabled = true
|
|
}
|
|
link.GVisorGRO = conf.GVisorGRO
|
|
|
|
args.FDBasedLinks = append(args.FDBasedLinks, link)
|
|
}
|
|
}
|
|
|
|
if err := pcapAndNAT(&args, conf); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debugf("Setting up network, config: %+v", args)
|
|
if err := conn.Call(boot.NetworkCreateLinksAndRoutes, &args, nil); err != nil {
|
|
return fmt.Errorf("creating links and routes: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func initPluginStack(conn *urpc.Client, pid int, conf *config.Config) error {
|
|
pluginStack := plugin.GetPluginStack()
|
|
if pluginStack == nil {
|
|
return fmt.Errorf("plugin stack is not registered")
|
|
}
|
|
|
|
initStr, fds, err := pluginStack.PreInit(&plugin.PreInitStackArgs{Pid: pid})
|
|
if err != nil {
|
|
return fmt.Errorf("plugin stack PreInit failed: %v", err)
|
|
}
|
|
var args boot.InitPluginStackArgs
|
|
args.InitStr = initStr
|
|
for _, fd := range fds {
|
|
args.FilePayload.Files = append(args.FilePayload.Files, os.NewFile(uintptr(fd), ""))
|
|
}
|
|
|
|
log.Debugf("Initializing plugin network stack, config: %+v", args)
|
|
if err := conn.Call(boot.NetworkInitPluginStack, &args, nil); err != nil {
|
|
return fmt.Errorf("error initializing plugin netstack: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isAddressOnInterface checks if an address is on an interface
|
|
func isAddressOnInterface(ifaceName string, addr *net.IPNet) (bool, error) {
|
|
iface, err := net.InterfaceByName(ifaceName)
|
|
if err != nil {
|
|
return false, fmt.Errorf("getting interface by name %q: %w", ifaceName, err)
|
|
}
|
|
ifaceAddrs, err := iface.Addrs()
|
|
if err != nil {
|
|
return false, fmt.Errorf("fetching interface addresses for %q: %w", iface.Name, err)
|
|
}
|
|
for _, ifaceAddr := range ifaceAddrs {
|
|
ipNet, ok := ifaceAddr.(*net.IPNet)
|
|
if !ok {
|
|
log.Warningf("Can't cast address to *net.IPNet, skipping: %+v", ifaceAddr)
|
|
continue
|
|
}
|
|
if ipNet.String() == addr.String() {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
type socketEntry struct {
|
|
deviceFile *os.File
|
|
gsoMaxSize uint32
|
|
}
|
|
|
|
// createSocket creates an underlying AF_PACKET socket and configures it for
|
|
// use by the sentry and returns an *os.File that wraps the underlying socket
|
|
// fd.
|
|
func createSocket(iface net.Interface, ifaceLink netlink.Link, enableGSO bool) (*socketEntry, error) {
|
|
// Create the socket.
|
|
const protocol = 0x0300 // htons(ETH_P_ALL)
|
|
fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, 0) // pass protocol 0 to avoid slow bind()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create raw socket: %v", err)
|
|
}
|
|
deviceFile := os.NewFile(uintptr(fd), "raw-device-fd")
|
|
// Bind to the appropriate device.
|
|
ll := unix.SockaddrLinklayer{
|
|
Protocol: protocol,
|
|
Ifindex: iface.Index,
|
|
}
|
|
if err := unix.Bind(fd, &ll); err != nil {
|
|
return nil, fmt.Errorf("unable to bind to %q: %v", iface.Name, err)
|
|
}
|
|
|
|
gsoMaxSize := uint32(0)
|
|
if enableGSO {
|
|
gso, err := isGSOEnabled(fd, iface.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting GSO for interface %q: %v", iface.Name, err)
|
|
}
|
|
if gso {
|
|
if err := unix.SetsockoptInt(fd, unix.SOL_PACKET, unix.PACKET_VNET_HDR, 1); err != nil {
|
|
return nil, fmt.Errorf("unable to enable the PACKET_VNET_HDR option: %v", err)
|
|
}
|
|
gsoMaxSize = ifaceLink.Attrs().GSOMaxSize
|
|
} else {
|
|
log.Infof("GSO not available in host.")
|
|
}
|
|
}
|
|
|
|
// Use SO_RCVBUFFORCE/SO_SNDBUFFORCE because on linux the receive/send buffer
|
|
// for an AF_PACKET socket is capped by "net.core.rmem_max/wmem_max".
|
|
// wmem_max/rmem_max default to a unusually low value of 208KB. This is too
|
|
// low for gVisor to be able to receive packets at high throughputs without
|
|
// incurring packet drops.
|
|
const bufSize = 4 << 20 // 4MB.
|
|
|
|
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUFFORCE, bufSize); err != nil {
|
|
_ = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF, bufSize)
|
|
sz, _ := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF)
|
|
|
|
if sz < bufSize {
|
|
log.Warningf("Failed to increase rcv buffer to %d on SOCK_RAW on %s. Current buffer %d: %v", bufSize, iface.Name, sz, err)
|
|
}
|
|
}
|
|
|
|
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SNDBUFFORCE, bufSize); err != nil {
|
|
_ = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SNDBUF, bufSize)
|
|
sz, _ := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SNDBUF)
|
|
if sz < bufSize {
|
|
log.Warningf("Failed to increase snd buffer to %d on SOCK_RAW on %s. Current buffer %d: %v", bufSize, iface.Name, sz, err)
|
|
}
|
|
}
|
|
|
|
return &socketEntry{deviceFile, gsoMaxSize}, nil
|
|
}
|
|
|
|
// loopbackLink returns the link with addresses and routes for a loopback
|
|
// interface.
|
|
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,
|
|
}
|
|
for _, addr := range addrs {
|
|
ipNet, ok := addr.(*net.IPNet)
|
|
if !ok {
|
|
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,
|
|
PrefixLen: prefix,
|
|
})
|
|
|
|
dst := *ipNet
|
|
dst.IP = dst.IP.Mask(dst.Mask)
|
|
link.Routes = append(link.Routes, boot.Route{
|
|
Destination: dst,
|
|
})
|
|
}
|
|
return link, nil
|
|
}
|
|
|
|
// 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, disableIPv6 bool) ([]boot.Route, *boot.Route, *boot.Route, error) {
|
|
link, err := netlink.LinkByIndex(iface.Index)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
rs, err := netlink.RouteList(link, netlink.FAMILY_ALL)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("getting routes from %q: %v", iface.Name, err)
|
|
}
|
|
|
|
var defv4, defv6 *boot.Route
|
|
var routes []boot.Route
|
|
for _, r := range rs {
|
|
// Is it a default route?
|
|
if r.Dst == nil {
|
|
if r.Gw == nil {
|
|
return nil, nil, nil, fmt.Errorf("default route with no gateway %q: %+v", iface.Name, r)
|
|
}
|
|
// Create a catch all route to the gateway.
|
|
switch len(r.Gw) {
|
|
case header.IPv4AddressSize:
|
|
if defv4 != nil {
|
|
return nil, nil, nil, fmt.Errorf("more than one default route found %q, def: %+v, route: %+v", iface.Name, defv4, r)
|
|
}
|
|
defv4 = &boot.Route{
|
|
Destination: net.IPNet{
|
|
IP: net.IPv4zero,
|
|
Mask: net.IPMask(net.IPv4zero),
|
|
},
|
|
Gateway: r.Gw,
|
|
}
|
|
case header.IPv6AddressSize:
|
|
if defv6 != nil {
|
|
return nil, nil, nil, fmt.Errorf("more than one default route found %q, def: %+v, route: %+v", iface.Name, defv6, r)
|
|
}
|
|
|
|
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)
|
|
}
|
|
continue
|
|
}
|
|
|
|
dst := *r.Dst
|
|
if disableIPv6 && dst.IP.To4() == nil {
|
|
continue
|
|
}
|
|
dst.IP = dst.IP.Mask(dst.Mask)
|
|
routes = append(routes, boot.Route{
|
|
Destination: dst,
|
|
Gateway: r.Gw,
|
|
})
|
|
}
|
|
return routes, defv4, defv6, nil
|
|
}
|
|
|
|
// removeAddress removes IP address from network device. It's equivalent to:
|
|
//
|
|
// ip addr del <ipAndMask> dev <name>
|
|
func removeAddress(source netlink.Link, ipAndMask string) error {
|
|
addr, err := netlink.ParseAddr(ipAndMask)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return netlink.AddrDel(source, addr)
|
|
}
|
|
|
|
func pcapAndNAT(args *boot.CreateLinksAndRoutesArgs, conf *config.Config) error {
|
|
// Possibly enable packet logging.
|
|
args.LogPackets = conf.LogPackets
|
|
|
|
// Pass PCAP log file if present.
|
|
if conf.PCAP != "" {
|
|
args.PCAP = true
|
|
pcap, err := os.OpenFile(conf.PCAP, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open PCAP file %s: %v", conf.PCAP, err)
|
|
}
|
|
args.FilePayload.Files = append(args.FilePayload.Files, pcap)
|
|
}
|
|
|
|
// Pass the host's NAT table if requested.
|
|
if conf.ReproduceNftables || conf.ReproduceNAT {
|
|
var f *os.File
|
|
var err error
|
|
if conf.ReproduceNftables {
|
|
log.Infof("reproing nftables")
|
|
f, err = checkNftables()
|
|
} else if conf.ReproduceNAT {
|
|
log.Infof("reproing legacy tables")
|
|
f, err = writeNATBlob()
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write NAT blob: %v", err)
|
|
}
|
|
if f != nil {
|
|
args.NATBlob = true
|
|
args.FilePayload.Files = append(args.FilePayload.Files, f)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// The below is a work around to generate iptables-legacy rules on machines
|
|
// that use iptables-nftables. The logic goes something like this:
|
|
//
|
|
// start
|
|
// |
|
|
// v no
|
|
// are legacy tables empty? -----> scrape rules -----> done <----+
|
|
// | ^ |
|
|
// | yes | |
|
|
// v yes | |
|
|
// are nft tables empty? -------------------------------+ |
|
|
// | |
|
|
// | no |
|
|
// v |
|
|
// pipe iptables-nft-save -t nat to iptables-legacy-restore |
|
|
// scrape rules |
|
|
// delete iptables-legacy rules |
|
|
// | |
|
|
// +---------------------------------------------------+
|
|
//
|
|
// If we fail at some point (e.g. to find a binary), we just try to scrape the
|
|
// legacy rules.
|
|
|
|
const emptyNatRules = `-P PREROUTING ACCEPT
|
|
-P INPUT ACCEPT
|
|
-P OUTPUT ACCEPT
|
|
-P POSTROUTING ACCEPT
|
|
`
|
|
|
|
// checkNftables can return a nil file and error if it finds only
|
|
// emptyNatRules.
|
|
func checkNftables() (*os.File, error) {
|
|
// Use iptables (not iptables-save) to test table emptiness because it
|
|
// gives predictable results: no counters and no comments.
|
|
|
|
// Is the legacy table empty?
|
|
if out, err := exec.Command("iptables-legacy", "-t", "nat", "-S").Output(); err != nil || string(out) != emptyNatRules {
|
|
return writeNATBlob()
|
|
}
|
|
|
|
// Is the nftables table empty?
|
|
if out, err := exec.Command("iptables-nft", "-t", "nat", "-S").Output(); err != nil || string(out) == emptyNatRules {
|
|
return nil, nil
|
|
}
|
|
|
|
// Get the current (empty) legacy rules.
|
|
currLegacy, err := exec.Command("iptables-legacy-save", "-t", "nat").Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to save existing rules with error (%v) and output: %s", err, currLegacy)
|
|
}
|
|
|
|
// Restore empty legacy rules.
|
|
defer func() {
|
|
cmd := exec.Command("iptables-legacy-restore")
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
log.Warningf("failed to get stdin pipe: %v", err)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
defer stdin.Close()
|
|
stdin.Write(currLegacy)
|
|
}()
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
log.Warningf("failed to restore iptables error (%v) with output: %s", err, out)
|
|
}
|
|
}()
|
|
|
|
// Pipe the output of iptables-nft-save to iptables-legacy-restore.
|
|
nftOut, err := exec.Command("iptables-nft-save", "-t", "nat").Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to run iptables-nft-save: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("iptables-legacy-restore")
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get stdin pipe: %v", err)
|
|
}
|
|
|
|
go func() {
|
|
defer stdin.Close()
|
|
stdin.Write(nftOut)
|
|
}()
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return nil, fmt.Errorf("failed to restore iptables error (%v) with output: %s", err, out)
|
|
}
|
|
|
|
return writeNATBlob()
|
|
}
|