Files
visor/runsc/boot/network.go
T
zeekayandHanzo Dev 3da05638df State the ZAP wire of every urpc payload
urpc encodes with encoding/json, which walks the payload on every call: a
realistic control.ExecArgs costs 7084ns and 10 allocations to cross. ZAP
reads a field at a constant offset out of the buffer that arrived, so the
same payload is 740ns and one allocation -- but only if the offsets are
stated, and a stated offset that is wrong is silent corruption rather than
a compile error. So state them from the types themselves.

//tools/zap reflects over the payloads at build time and writes the wire
as Go source with the offsets as constants. It lives inside Bazel because
pkg/sentry/control cannot be imported from outside it: the *_go_proto
packages it depends on have no source in the tree.

Six payloads had no layout to state. A map, an interface and a type from
another module have no offsets, so each is answered in the type:

  SaveOpts.Metadata and StartContainerArgs.InitialCgroups/Limits cross as
  lists of pairs; boot.Route.Destination restates net.IPNet as Subnet;
  OpenMountArgs restates the OCI mount fields the call actually uses;
  StartArgs.Spec and SinkConfig.Config carry their documents as the bytes
  they arrived as; MetricsRegistrationResponse carries the protocol
  buffer the same way; and seccheck's session options become a type
  instead of a map of any.

ExecArgs and MetricsExportData still have none: ExecArgs holds live
kernel objects behind an interface, and MetricsExportData reaches a
prometheus snapshot whose labels are a map.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-22 23:10:33 -07:00

643 lines
18 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 boot
import (
"fmt"
"io"
"net"
"os"
"runtime"
"strings"
"syscall"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/socket/netfilter"
"gvisor.dev/gvisor/pkg/sentry/socket/plugin"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/link/ethernet"
"gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
"gvisor.dev/gvisor/pkg/tcpip/link/loopback"
"gvisor.dev/gvisor/pkg/tcpip/link/qdisc/fifo"
"gvisor.dev/gvisor/pkg/tcpip/link/qdisc/tbf"
"gvisor.dev/gvisor/pkg/tcpip/link/sniffer"
"gvisor.dev/gvisor/pkg/tcpip/link/xdp"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/urpc"
"gvisor.dev/gvisor/runsc/config"
)
// DefaultLoopbackLink contains IP addresses and routes of "127.0.0.1/8" and
// "::1/8" on "lo" interface.
var DefaultLoopbackLink = LoopbackLink{
Name: "lo",
Addresses: []IPWithPrefix{
{Address: net.IP("\x7f\x00\x00\x01"), PrefixLen: 8},
{Address: net.IPv6loopback, PrefixLen: 128},
},
Routes: []Route{
{
Destination: Subnet{
IP: net.IPv4(0x7f, 0, 0, 0),
Mask: net.IPv4Mask(0xff, 0, 0, 0),
},
},
{
Destination: Subnet{
IP: net.IPv6loopback,
Mask: net.IPMask(strings.Repeat("\xff", net.IPv6len)),
},
},
},
}
// Network exposes methods that can be used to configure a network stack.
type Network struct {
Stack *stack.Stack
Kernel *kernel.Kernel
// PluginStack is a third-party network stack to use in place of
// netstack when non-nil.
PluginStack plugin.PluginStack
}
// Subnet is an address and the mask that selects its network. It restates
// net.IPNet, which the RPC cannot carry: a stdlib type cannot state its own
// wire, and a conversion between the two is legal in either direction.
type Subnet struct {
IP net.IP
Mask net.IPMask
}
// Route represents a route in the network stack.
type Route struct {
Destination Subnet
Gateway net.IP
MTU uint32
}
// DefaultRoute represents a catch all route to the default gateway.
type DefaultRoute struct {
Route Route
Name string
}
type Neighbor struct {
IP net.IP
HardwareAddr net.HardwareAddr
}
// FDBasedLink configures an fd-based link.
type FDBasedLink struct {
Name string
InterfaceIndex int
MTU int
Addresses []IPWithPrefix
Routes []Route
GSOMaxSize uint32
GVisorGSOEnabled bool
GVisorGRO bool
TXChecksumOffload bool
RXChecksumOffload bool
LinkAddress net.HardwareAddr
QDisc config.QueueingDiscipline
TBFRate uint64
TBFBurst uint32
Neighbors []Neighbor
// NumChannels controls how many underlying FDs are to be used to
// create this endpoint.
NumChannels int
// ProcessorsPerChannel controls how many goroutines are used to handle
// packets on each channel.
ProcessorsPerChannel int
// IsPacket indicates whether each FD in this link is a packet socket.
IsPacket []bool
// PreConfigured indicates that getsockname and setsockopt(PACKET_FANOUT)
// have already been performed on the host FDs.
PreConfigured bool
}
// BindOpt indicates whether the sentry or runsc process is responsible for
// binding the AF_XDP socket.
type BindOpt int
const (
// BindSentry indicates the sentry process must call bind.
BindSentry BindOpt = iota
// BindRunsc indicates the runsc process must call bind.
BindRunsc
)
// XDPLink configures an XDP link.
type XDPLink struct {
Name string
InterfaceIndex int
MTU int
Addresses []IPWithPrefix
Routes []Route
TXChecksumOffload bool
RXChecksumOffload bool
LinkAddress net.HardwareAddr
QDisc config.QueueingDiscipline
TBFRate uint64
TBFBurst uint32
Neighbors []Neighbor
GVisorGRO bool
Bind BindOpt
// NumChannels controls how many underlying FDs are to be used to
// create this endpoint.
NumChannels int
}
// LoopbackLink configures a loopback link.
type LoopbackLink struct {
Name string
Addresses []IPWithPrefix
Routes []Route
GVisorGRO bool
}
// CreateLinksAndRoutesArgs are arguments to CreateLinkAndRoutes.
type CreateLinksAndRoutesArgs struct {
// FilePayload contains the fds associated with the FDBasedLinks. The
// number of fd's should match the sum of the NumChannels field of the
// FDBasedLink entries below.
urpc.FilePayload
LoopbackLinks []LoopbackLink
FDBasedLinks []FDBasedLink
XDPLinks []XDPLink
Defaultv4Gateway DefaultRoute
Defaultv6Gateway DefaultRoute
// PCAP indicates that FilePayload also contains a PCAP log file.
PCAP bool
// LogPackets indicates that packets should be logged.
LogPackets bool
// NATBlob indicates whether FilePayload also contains an iptables NAT
// ruleset.
NATBlob bool
// PauseExternalNetworking indicates whether external networking should be
// disabled initially.
PauseExternalNetworking bool
// AllowConnectedOnSave indicates whether connections should be allowed to
// remain connected during save.
AllowConnectedOnSave bool
// IsRestore indicates whether this is part of a restore flow.
IsRestore bool
}
// InitPluginStackArgs are arguments to InitPluginStack.
type InitPluginStackArgs struct {
urpc.FilePayload
InitStr string
}
// IPWithPrefix is an address with its subnet prefix length.
type IPWithPrefix struct {
// Address is a network address.
Address net.IP
// PrefixLen is the subnet prefix length.
PrefixLen int
}
func (ip IPWithPrefix) String() string {
return fmt.Sprintf("%s/%d", ip.Address, ip.PrefixLen)
}
// Empty returns true if route hasn't been set.
func (r *Route) Empty() bool {
return r.Destination.IP == nil && r.Destination.Mask == nil && r.Gateway == nil
}
func (r *Route) toTcpipRoute(id tcpip.NICID) (tcpip.Route, error) {
subnet, err := tcpip.NewSubnet(ipToAddress(r.Destination.IP), ipMaskToAddressMask(r.Destination.Mask))
if err != nil {
return tcpip.Route{}, err
}
return tcpip.Route{
Destination: subnet,
Gateway: ipToAddress(r.Gateway),
NIC: id,
MTU: r.MTU,
}, nil
}
// InitPluginStack initializes plugin network stack.
// It will invoke Init() that is registered by current plugin stack.
func (n *Network) InitPluginStack(args *InitPluginStackArgs, _ *struct{}) error {
pluginStack := n.PluginStack
if pluginStack == nil {
return fmt.Errorf("plugin stack is not registered")
}
fdNum := len(args.FilePayload.Files)
fds := make([]int, fdNum)
for i := 0; i < fdNum; i++ {
oldFD := args.FilePayload.Files[i].Fd()
if newFD, err := syscall.Dup(int(oldFD)); err != nil {
return fmt.Errorf("failed to dup FD")
} else {
fds[i] = newFD
}
}
return pluginStack.Init(&plugin.InitStackArgs{
InitStr: args.InitStr,
FDs: fds,
})
}
// CreateLinksAndRoutes creates links and routes in a network stack. It should
// only be called once.
func (n *Network) CreateLinksAndRoutes(args *CreateLinksAndRoutesArgs, _ *struct{}) error {
if len(args.FDBasedLinks) > 0 && len(args.XDPLinks) > 0 {
return fmt.Errorf("received both fdbased and XDP links, but only one can be used at a time")
}
wantFDs := 0
for _, l := range args.FDBasedLinks {
wantFDs += l.NumChannels
}
for _, link := range args.XDPLinks {
// We have to keep several FDs alive when the sentry is
// responsible for binding, but when runsc binds we only expect
// the AF_XDP socket itself.
switch v := link.Bind; v {
case BindSentry:
wantFDs += 4
case BindRunsc:
wantFDs++
default:
return fmt.Errorf("unknown bind value: %d", v)
}
}
if args.PCAP {
wantFDs++
}
if args.NATBlob {
wantFDs++
}
if got := len(args.FilePayload.Files); got != wantFDs {
return fmt.Errorf("args.FilePayload.Files has %d FDs but we need %d entries based on FDBasedLinks, XDPLinks, and PCAP", got, wantFDs)
}
nicids := make(map[string]tcpip.NICID)
// Collect routes from all links.
var routes []tcpip.Route
// Loopback normally appear before other interfaces.
for _, link := range args.LoopbackLinks {
nicID := n.Stack.NextNICID()
nicids[link.Name] = nicID
var linkEP stack.LinkEndpoint
linkEP = ethernet.New(loopback.New())
if args.LogPackets {
linkEP = sniffer.New(linkEP)
}
log.Infof("Enabling loopback interface %q with id %d on addresses %+v", link.Name, nicID, link.Addresses)
opts := stack.NICOptions{
Name: link.Name,
DeliverLinkPackets: true,
}
if err := n.createNICWithAddrs(nicID, linkEP, opts, link.Addresses); err != nil {
return err
}
// Collect the routes from this link.
for _, r := range link.Routes {
route, err := r.toTcpipRoute(nicID)
if err != nil {
return err
}
routes = append(routes, route)
}
}
// Setup fdbased or XDP links.
fdOffset := 0
if len(args.FDBasedLinks) > 0 {
// Choose a dispatch mode.
// Testing has shown that RecvMMsg is the fastest. Attempts to use
// the PacketMMap dispatcher have failed to result in higher throughput.
dispatchMode := fdbased.RecvMMsg
for _, link := range args.FDBasedLinks {
nicID := n.Stack.NextNICID()
nicids[link.Name] = nicID
FDs := make([]int, 0, link.NumChannels)
for j := 0; j < link.NumChannels; j++ {
// Copy the underlying FD.
oldFD := args.FilePayload.Files[fdOffset].Fd()
newFD, err := unix.Dup(int(oldFD))
if err != nil {
return fmt.Errorf("failed to dup FD %v: %v", oldFD, err)
}
FDs = append(FDs, newFD)
fdOffset++
}
mac := tcpip.LinkAddress(link.LinkAddress)
linkEP, err := fdbased.New(&fdbased.Options{
FDs: FDs,
MTU: uint32(link.MTU),
EthernetHeader: mac != "",
Address: mac,
PacketDispatchMode: dispatchMode,
GSOMaxSize: link.GSOMaxSize,
GVisorGSOEnabled: link.GVisorGSOEnabled,
TXChecksumOffload: link.TXChecksumOffload,
RXChecksumOffload: link.RXChecksumOffload,
GRO: link.GVisorGRO,
ProcessorsPerChannel: link.ProcessorsPerChannel,
IsPacketSocket: link.IsPacket,
PreConfigured: link.PreConfigured,
})
if err != nil {
return err
}
// Setup packet logging if requested.
if args.PCAP {
newFD, err := unix.Dup(int(args.FilePayload.Files[fdOffset].Fd()))
if err != nil {
return fmt.Errorf("failed to dup pcap FD: %v", err)
}
const packetTruncateSize = 4096
linkEP, err = sniffer.NewWithWriter(linkEP, os.NewFile(uintptr(newFD), "pcap-file"), packetTruncateSize)
if err != nil {
return fmt.Errorf("failed to create PCAP logger: %v", err)
}
fdOffset++
} else if args.LogPackets {
linkEP = sniffer.New(linkEP)
}
var qDisc stack.QueueingDiscipline
switch link.QDisc {
case config.QDiscNone:
case config.QDiscFIFO:
log.Infof("Enabling FIFO QDisc on %q", link.Name)
qDisc = fifo.New(linkEP, runtime.GOMAXPROCS(0), 1000)
case config.QDiscTBF:
log.Infof("Enabling TBF QDisc on %q rate=%d burst=%d", link.Name, link.TBFRate, link.TBFBurst)
var err error
qDisc, err = tbf.New(linkEP, n.Stack.Clock(), link.TBFRate, link.TBFBurst, 1000)
if err != nil {
return fmt.Errorf("creating TBF qdisc for %q: %w", link.Name, err)
}
}
log.Infof("Enabling interface %q with id %d on addresses %+v (%v) w/ %d channels", link.Name, nicID, link.Addresses, mac, link.NumChannels)
opts := stack.NICOptions{
Name: link.Name,
QDisc: qDisc,
DeliverLinkPackets: true,
}
if err := n.createNICWithAddrs(nicID, linkEP, opts, link.Addresses); err != nil {
return err
}
// Collect the routes from this link.
for _, r := range link.Routes {
route, err := r.toTcpipRoute(nicID)
if err != nil {
return err
}
routes = append(routes, route)
}
for _, neigh := range link.Neighbors {
proto, tcpipAddr := ipToAddressAndProto(neigh.IP)
n.Stack.AddStaticNeighbor(nicID, proto, tcpipAddr, tcpip.LinkAddress(neigh.HardwareAddr))
}
}
} else if len(args.XDPLinks) > 0 {
if nlinks := len(args.XDPLinks); nlinks > 1 {
return fmt.Errorf("XDP only supports one link device, but got %d", nlinks)
}
link := args.XDPLinks[0]
nicID := n.Stack.NextNICID()
nicids[link.Name] = nicID
// Get the AF_XDP socket.
oldFD := args.FilePayload.Files[fdOffset].Fd()
fd, err := unix.Dup(int(oldFD))
if err != nil {
return fmt.Errorf("failed to dup AF_XDP fd %v: %v", oldFD, err)
}
fdOffset++
// When the sentry is responsible for binding, the runsc
// process sends several other FDs in order to keep them open
// and alive. These are for BPF programs and maps that, if
// closed, will break the dispatcher.
if link.Bind == BindSentry {
for _, fdName := range []string{"program-fd", "sockmap-fd", "link-fd"} {
oldFD := args.FilePayload.Files[fdOffset].Fd()
if _, err := unix.Dup(int(oldFD)); err != nil {
return fmt.Errorf("failed to dup %s with FD %d: %v", fdName, oldFD, err)
}
fdOffset++
}
}
// Setup packet logging if requested.
mac := tcpip.LinkAddress(link.LinkAddress)
linkEP, err := xdp.New(&xdp.Options{
FD: fd,
Address: mac,
TXChecksumOffload: link.TXChecksumOffload,
RXChecksumOffload: link.RXChecksumOffload,
InterfaceIndex: link.InterfaceIndex,
Bind: link.Bind == BindSentry,
GRO: link.GVisorGRO,
})
if err != nil {
return err
}
if args.PCAP {
newFD, err := unix.Dup(int(args.FilePayload.Files[fdOffset].Fd()))
if err != nil {
return fmt.Errorf("failed to dup pcap FD: %v", err)
}
const packetTruncateSize = 4096
linkEP, err = sniffer.NewWithWriter(linkEP, os.NewFile(uintptr(newFD), "pcap-file"), packetTruncateSize)
if err != nil {
return fmt.Errorf("failed to create PCAP logger: %v", err)
}
fdOffset++
} else if args.LogPackets {
linkEP = sniffer.New(linkEP)
}
var qDisc stack.QueueingDiscipline
switch link.QDisc {
case config.QDiscNone:
case config.QDiscFIFO:
log.Infof("Enabling FIFO QDisc on %q", link.Name)
qDisc = fifo.New(linkEP, runtime.GOMAXPROCS(0), 1000)
case config.QDiscTBF:
log.Infof("Enabling TBF QDisc on %q rate=%d burst=%d", link.Name, link.TBFRate, link.TBFBurst)
var err error
qDisc, err = tbf.New(linkEP, n.Stack.Clock(), link.TBFRate, link.TBFBurst, 1000)
if err != nil {
return fmt.Errorf("creating TBF qdisc for %q: %w", link.Name, err)
}
}
log.Infof("Enabling interface %q with id %d on addresses %+v (%v) w/ %d channels", link.Name, nicID, link.Addresses, mac, link.NumChannels)
opts := stack.NICOptions{
Name: link.Name,
QDisc: qDisc,
DeliverLinkPackets: true,
}
if err := n.createNICWithAddrs(nicID, linkEP, opts, link.Addresses); err != nil {
return err
}
// Collect the routes from this link.
for _, r := range link.Routes {
route, err := r.toTcpipRoute(nicID)
if err != nil {
return err
}
routes = append(routes, route)
}
for _, neigh := range link.Neighbors {
proto, tcpipAddr := ipToAddressAndProto(neigh.IP)
n.Stack.AddStaticNeighbor(nicID, proto, tcpipAddr, tcpip.LinkAddress(neigh.HardwareAddr))
}
}
if !args.Defaultv4Gateway.Route.Empty() {
nicID, ok := nicids[args.Defaultv4Gateway.Name]
if !ok {
return fmt.Errorf("invalid interface name %q for default route", args.Defaultv4Gateway.Name)
}
route, err := args.Defaultv4Gateway.Route.toTcpipRoute(nicID)
if err != nil {
return err
}
routes = append(routes, route)
}
if !args.Defaultv6Gateway.Route.Empty() {
nicID, ok := nicids[args.Defaultv6Gateway.Name]
if !ok {
return fmt.Errorf("invalid interface name %q for default route", args.Defaultv6Gateway.Name)
}
route, err := args.Defaultv6Gateway.Route.toTcpipRoute(nicID)
if err != nil {
return err
}
routes = append(routes, route)
}
log.Infof("Setting routes %+v", routes)
n.Stack.SetRouteTable(routes)
// Set NAT table rules if necessary.
if args.NATBlob {
log.Infof("Replacing NAT table")
iptReplaceBlob, err := io.ReadAll(args.FilePayload.Files[fdOffset])
if err != nil {
return fmt.Errorf("failed to read iptables blob: %v", err)
}
fdOffset++
if err := netfilter.SetEntries(n.Kernel.RootUserNamespace(), n.Stack, iptReplaceBlob, false); err != nil {
return fmt.Errorf("failed to SetEntries: %v", err)
}
}
if args.PauseExternalNetworking {
n.Stack.DisableAllNonLoopbackNICs()
}
n.Stack.SetAllowConnectedOnSave(args.AllowConnectedOnSave)
return nil
}
// createNICWithAddrs creates a NIC in the network stack and adds the given
// addresses.
func (n *Network) createNICWithAddrs(id tcpip.NICID, ep stack.LinkEndpoint, opts stack.NICOptions, addrs []IPWithPrefix) error {
if err := n.Stack.CreateNICWithOptions(id, ep, opts); err != nil {
return fmt.Errorf("CreateNICWithOptions(%d, _, %+v) failed: %v", id, opts, err)
}
for _, addr := range addrs {
proto, tcpipAddr := ipToAddressAndProto(addr.Address)
protocolAddr := tcpip.ProtocolAddress{
Protocol: proto,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: tcpipAddr,
PrefixLen: addr.PrefixLen,
},
}
if err := n.Stack.AddProtocolAddress(id, protocolAddr, stack.AddressProperties{}); err != nil {
return fmt.Errorf("AddProtocolAddress(%d, %+v, {}) failed: %s", id, protocolAddr, err)
}
}
return nil
}
// ipToAddressAndProto converts IP to tcpip.Address and a protocol number.
//
// Note: don't use 'len(ip)' to determine IP version because length is always 16.
func ipToAddressAndProto(ip net.IP) (tcpip.NetworkProtocolNumber, tcpip.Address) {
if i4 := ip.To4(); i4 != nil {
return ipv4.ProtocolNumber, tcpip.AddrFromSlice(i4)
}
return ipv6.ProtocolNumber, tcpip.AddrFromSlice(ip)
}
// ipToAddress converts IP to tcpip.Address, ignoring the protocol.
func ipToAddress(ip net.IP) tcpip.Address {
_, addr := ipToAddressAndProto(ip)
return addr
}
// ipMaskToAddressMask converts IPMask to tcpip.AddressMask, ignoring the
// protocol.
func ipMaskToAddressMask(ipMask net.IPMask) tcpip.AddressMask {
addr := ipToAddress(net.IP(ipMask))
return tcpip.MaskFromBytes(addr.AsSlice())
}