xdp: add a tunnel mode to avoid sharing UMEM among sandboxes

The existing redirect mode uses XDP to maximize performance but is not suitably
secure: packets are copied directly from the driver into a userspace buffer
(UMEM). But because the UMEM is shared among all processes with a socket open
on a particular NIC queue, sandboxes using redirect mode all map the same UMEM
and thus can read each other's packets.

Tunnel mode instead installs an eBPF program that copies packets from the
host's NIC driver into a per-sandbox NIC driver. This incurs an additional
copy, but there is no longer memory shared between sandboxes.

Benchmarking tunnel mode with redis-benchmark shows a performance gain over
standard Docker networking of 20%. Redirect mode showed a 30% improvement.

PiperOrigin-RevId: 595746162
This commit is contained in:
Kevin Krakauer
2024-01-04 10:45:43 -08:00
committed by gVisor bot
parent 5c41ffabdb
commit 9e2db2c131
13 changed files with 630 additions and 83 deletions
+12
View File
@@ -936,12 +936,19 @@ const (
// XDPModeRedirect uses an AF_XDP socket on the host NIC to bypass the
// Linux network stack.
XDPModeRedirect
// XDPModeTunnel uses XDP_REDIRECT to redirect packets directy from the
// host NIC to the VETH device inside the container's network
// namespace. Packets are read from the VETH via AF_XDP, as in
// XDPModeNS.
XDPModeTunnel
)
const (
xdpModeStrOff = "off"
xdpModeStrNS = "ns"
xdpModeStrRedirect = "redirect"
xdpModeStrTunnel = "tunnel"
)
var xdpConfig XDP
@@ -960,6 +967,8 @@ func (xd *XDP) String() string {
return xdpModeStrNS
case XDPModeRedirect:
return fmt.Sprintf("%s:%s", xdpModeStrRedirect, xd.IfaceName)
case XDPModeTunnel:
return fmt.Sprintf("%s:%s", xdpModeStrTunnel, xd.IfaceName)
default:
panic(fmt.Sprintf("unknown mode %d", xd.Mode))
}
@@ -982,6 +991,9 @@ func (xd *XDP) Set(input string) error {
case len(parts) == 2 && parts[0] == xdpModeStrRedirect && parts[1] != "":
xd.Mode = XDPModeRedirect
xd.IfaceName = parts[1]
case len(parts) == 2 && parts[0] == xdpModeStrTunnel && parts[1] != "":
xd.Mode = XDPModeTunnel
xd.IfaceName = parts[1]
default:
return fmt.Errorf("invalid --xdp value: %q", input)
}
+1 -1
View File
@@ -120,7 +120,7 @@ func RegisterFlags(flagSet *flag.FlagSet) {
flagSet.Var(queueingDisciplinePtr(QDiscFIFO), "qdisc", "specifies which queueing discipline to apply by default to the non loopback nics used by the sandbox.")
flagSet.Int("num-network-channels", 1, "number of underlying channels(FDs) to use for network link endpoints.")
flagSet.Bool("buffer-pooling", true, "enable allocation of buffers from a shared pool instead of the heap.")
flagSet.Var(&xdpConfig, "EXPERIMENTAL-xdp", `whether and how to use XDP. Can be one of: "off" (default), "ns", or "redirect:<device name>"`)
flagSet.Var(&xdpConfig, "EXPERIMENTAL-xdp", `whether and how to use XDP. Can be one of: "off" (default), "ns", "redirect:<device name>", or "tunnel:<device name>"`)
flagSet.Bool("EXPERIMENTAL-xdp-need-wakeup", true, "EXPERIMENTAL. Use XDP_USE_NEED_WAKEUP with XDP sockets.") // TODO(b/240191988): Figure out whether this helps and remove it as a flag.
flagSet.Bool("reproduce-nat", false, "Scrape the host netns NAT table and reproduce it in the sandbox.")
flagSet.Bool("reproduce-nftables", false, "Attempt to scrape and reproduce nftable rules inside the sandbox. Overrides reproduce-nat when true.")
+9
View File
@@ -10,6 +10,7 @@ go_library(
srcs = ["bpf.go"],
embedsrcs = [
"af_xdp_ebpf.o", # keep
"tunnel_veth_ebpf.o", # keep
],
visibility = ["//visibility:public"],
)
@@ -21,3 +22,11 @@ bpf_program(
bpf_object = "af_xdp_ebpf.o",
visibility = ["//:sandbox"],
)
bpf_program(
name = "tunnel_veth_ebpf",
src = "tunnel_veth.ebpf.c",
hdrs = [],
bpf_object = "tunnel_veth_ebpf.o",
visibility = ["//:sandbox"],
)
+6
View File
@@ -22,3 +22,9 @@ import _ "embed"
//
//go:embed af_xdp_ebpf.o
var AFXDPProgram []byte
// TunnelVethProgram is a BPF program that redirects all packets to exit via
// another device.
//
//go:embed tunnel_veth_ebpf.o
var TunnelVethProgram []byte
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2023 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.
#include <linux/bpf.h>
#define section(secname) __attribute__((section(secname), used))
char __license[] section("license") = "Apache-2.0";
// Helper functions are defined positionally in <linux/bpf.h>, and their
// signatures are scattered throughout the kernel. They can be found via the
// defining macro BPF_CALL_[0-5].
// TODO(b/240191988): Use vmlinux instead of this.
static int (*bpf_redirect_map)(void *bpf_map, __u32 iface_index,
__u64 flags) = (void *)51;
struct bpf_map_def {
unsigned int type;
unsigned int key_size;
unsigned int value_size;
unsigned int max_entries;
unsigned int map_flags;
};
struct bpf_map_def section("maps") dev_map = {
.type = BPF_MAP_TYPE_DEVMAP,
.key_size = sizeof(__u32),
.value_size = sizeof(__u32),
.max_entries = 1,
};
// Redirect all incoming traffic to go out another device.
section("xdp") int xdp_veth_prog(struct xdp_md *ctx) {
return bpf_redirect_map(&dev_map, ctx->rx_queue_index, XDP_PASS);
}
+39 -25
View File
@@ -128,6 +128,11 @@ func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *con
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)
}
@@ -318,31 +323,8 @@ func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *con
}
}
// 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
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)
}
args.NATBlob = true
args.FilePayload.Files = append(args.FilePayload.Files, f)
if err := pcapAndNAT(&args, conf); err != nil {
return err
}
log.Debugf("Setting up network, config: %+v", args)
@@ -543,6 +525,38 @@ func removeAddress(source netlink.Link, ipAndMask string) error {
return netlink.AddrDel(source, addr)
}
func pcapAndNAT(args *boot.CreateLinksAndRoutesArgs, conf *config.Config) error {
// 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)
}
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:
//
+248 -28
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"net"
"os"
"strings"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
@@ -50,7 +51,7 @@ import (
// TODO(b/240191988): Merge redundant code with CreateLinksAndRoutes once
// features are finalized.
func createRedirectInterfacesAndRoutes(conn *urpc.Client, conf *config.Config) error {
args, iface, err := prepareRedirectInterfaceArgs(conf)
args, iface, err := prepareRedirectInterfaceArgs(boot.BindRunsc, conf)
if err != nil {
return fmt.Errorf("failed to generate redirect interface args: %w", err)
}
@@ -68,31 +69,8 @@ func createRedirectInterfacesAndRoutes(conn *urpc.Client, conf *config.Config) e
}
args.FilePayload.Files = append(args.FilePayload.Files, xdpSock)
// 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
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)
}
args.NATBlob = true
args.FilePayload.Files = append(args.FilePayload.Files, f)
if err := pcapAndNAT(&args, conf); err != nil {
return err
}
log.Infof("Setting up network, config: %+v", args)
@@ -108,6 +86,8 @@ func createRedirectInterfacesAndRoutes(conn *urpc.Client, conf *config.Config) e
if err != nil {
return fmt.Errorf("failed to load pinned map %s: %w", mapPath, err)
}
// TODO(b/240191988): Updating of pinned maps should be sychronized and
// check for the existence of the key.
mapKey := uint32(0)
mapVal := uint32(xdpSockFD)
if err := pinnedMap.Update(&mapKey, &mapVal, ebpf.UpdateAny); err != nil {
@@ -128,7 +108,7 @@ func createRedirectInterfacesAndRoutes(conn *urpc.Client, conf *config.Config) e
// process two interfaces: the loopback and the interface we've been told to
// bind to. This all takes place in the netns where the runsc binary is run,
// *not* the netns passed to the container.
func prepareRedirectInterfaceArgs(conf *config.Config) (boot.CreateLinksAndRoutesArgs, net.Interface, error) {
func prepareRedirectInterfaceArgs(bind boot.BindOpt, conf *config.Config) (boot.CreateLinksAndRoutesArgs, net.Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return boot.CreateLinksAndRoutesArgs{}, net.Interface{}, fmt.Errorf("querying interfaces: %w", err)
@@ -237,7 +217,7 @@ func prepareRedirectInterfaceArgs(conf *config.Config) (boot.CreateLinksAndRoute
LinkAddress: linkAddress,
Addresses: []boot.IPWithPrefix{addr},
GvisorGROTimeout: conf.GvisorGROTimeout,
Bind: boot.BindRunsc,
Bind: bind,
}
args.XDPLinks = append(args.XDPLinks, xdplink)
netIface = iface
@@ -291,6 +271,8 @@ func createSocketXDP(iface net.Interface) ([]*os.File, error) {
// Insert our AF_XDP socket into the BPF map that dictates where
// packets are redirected to.
// TODO(b/240191988): Updating of pinned maps should be sychronized and
// check for the existence of the key.
key := uint32(0)
val := uint32(fd)
if err := objects.SockMap.Update(&key, &val, 0 /* flags */); err != nil {
@@ -319,3 +301,241 @@ func createSocketXDP(iface net.Interface) ([]*os.File, error) {
os.NewFile(uintptr(linkFD), "link-fd"), // The XDP link.
}, nil
}
// TODO(b/240191988): Merge redundant code with CreateLinksAndRoutes once
// features are finalized.
// TODO(b/240191988): Cleanup / GC of pinned BPF objects.
func createXDPTunnel(conn *urpc.Client, nsPath string, conf *config.Config) error {
// Get the setup for the sentry nic. We need the host neighbors and routes.
args, hostIface, err := prepareRedirectInterfaceArgs(boot.BindSentry, conf)
if err != nil {
return fmt.Errorf("failed to generate tunnel interface args: %w", err)
}
// Setup the XDP socket on the gVisor nic.
files, err := func() ([]*os.File, error) {
// Join the network namespace that we will be copying.
restore, err := joinNetNS(nsPath)
if err != nil {
return nil, err
}
defer restore()
// Create an XDP socket. The sentry will mmap memory for the various
// rings and bind to the device.
fd, err := unix.Socket(unix.AF_XDP, unix.SOCK_RAW, 0)
if err != nil {
return nil, fmt.Errorf("unable to create AF_XDP socket: %v", err)
}
// We also need to, before dropping privileges, attach a program to the
// device and insert our socket into its map.
// Load into the kernel.
spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(bpf.AFXDPProgram))
if err != nil {
return nil, fmt.Errorf("failed to load spec: %v", err)
}
var objects struct {
Program *ebpf.Program `ebpf:"xdp_prog"`
SockMap *ebpf.Map `ebpf:"sock_map"`
}
if err := spec.LoadAndAssign(&objects, nil); err != nil {
return nil, fmt.Errorf("failed to load program: %v", err)
}
// We assume there are two interfaces in the netns: a loopback and veth.
ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("querying interfaces in ns: %w", err)
}
var iface *net.Interface
for _, netIface := range ifaces {
if netIface.Flags&net.FlagLoopback == 0 {
iface = &netIface
break
}
}
if iface == nil {
return nil, fmt.Errorf("unable to find non-loopback interface in the ns")
}
args.XDPLinks[0].InterfaceIndex = iface.Index
rawLink, err := link.AttachRawLink(link.RawLinkOptions{
Program: objects.Program,
Attach: ebpf.AttachXDP,
Target: iface.Index,
// By not setting the Flag field, the kernel will choose the
// fastest mode. In order those are:
// - Offloaded onto the NIC.
// - Running directly in the driver.
// - Generic mode, which works with any NIC/driver but lacks
// much of the XDP performance boost.
})
if err != nil {
return nil, fmt.Errorf("failed to attach BPF program to interface %q: %v", iface.Name, err)
}
// Insert our AF_XDP socket into the BPF map that dictates where
// packets are redirected to.
// TODO(b/240191988): Updating of pinned maps should be
// sychronized and check for the existence of the key.
key := uint32(0)
val := uint32(fd)
if err := objects.SockMap.Update(&key, &val, 0 /* flags */); err != nil {
return nil, fmt.Errorf("failed to insert socket into BPF map: %v", err)
}
// We need to keep the Program, SockMap, and link FDs open until they
// can be passed to the sandbox process.
progFD, err := unix.Dup(objects.Program.FD())
if err != nil {
return nil, fmt.Errorf("failed to dup BPF program: %v", err)
}
sockMapFD, err := unix.Dup(objects.SockMap.FD())
if err != nil {
return nil, fmt.Errorf("failed to dup BPF map: %v", err)
}
linkFD, err := unix.Dup(rawLink.FD())
if err != nil {
return nil, fmt.Errorf("failed to dup BPF link: %v", err)
}
return []*os.File{
os.NewFile(uintptr(fd), "xdp-fd"), // The socket.
os.NewFile(uintptr(progFD), "program-fd"), // The XDP program.
os.NewFile(uintptr(sockMapFD), "sockmap-fd"), // The XDP map.
os.NewFile(uintptr(linkFD), "link-fd"), // The XDP link.
}, nil
}()
if err != nil {
return fmt.Errorf("failed to create AF_XDP socket for container: %w", err)
}
args.FilePayload.Files = append(args.FilePayload.Files, files...)
// We're back in the parent netns. Get all interfaces.
ifaces, err := net.Interfaces()
if err != nil {
return fmt.Errorf("querying interfaces: %w", err)
}
// TODO(b/240191988): Find a better way to identify the other end of the veth.
var vethIface *net.Interface
for _, iface := range ifaces {
if strings.HasPrefix(iface.Name, "veth") {
vethIface = &iface
break
}
}
if vethIface == nil {
return fmt.Errorf("unable to find veth interface")
}
// Insert veth into host eBPF map.
hostMapPath := xdpcmd.TunnelHostMapPath(hostIface.Name)
pinnedHostMap, err := ebpf.LoadPinnedMap(hostMapPath, nil)
if err != nil {
return fmt.Errorf("failed to load pinned host map %s: %w", hostMapPath, err)
}
// TODO(b/240191988): Updating of pinned maps should be sychronized and
// check for the existence of the key.
mapKey := uint32(0)
mapVal := uint32(vethIface.Index)
if err := pinnedHostMap.Update(&mapKey, &mapVal, ebpf.UpdateAny); err != nil {
return fmt.Errorf("failed to insert veth into host map %s: %w", hostMapPath, err)
}
// Attach a program to the veth.
spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(bpf.TunnelVethProgram))
if err != nil {
return fmt.Errorf("failed to load spec: %v", err)
}
var objects struct {
Program *ebpf.Program `ebpf:"xdp_veth_prog"`
DevMap *ebpf.Map `ebpf:"dev_map"`
}
if err := spec.LoadAndAssign(&objects, nil); err != nil {
return fmt.Errorf("failed to load program: %v", err)
}
defer func() {
if err := objects.Program.Close(); err != nil {
log.Infof("failed to close program: %v", err)
}
if err := objects.DevMap.Close(); err != nil {
log.Infof("failed to close sock map: %v", err)
}
}()
attached, err := link.AttachXDP(link.XDPOptions{
Program: objects.Program,
Interface: vethIface.Index,
// By not setting the Flag field, the kernel will choose the
// fastest mode. In order those are:
// - Offloaded onto the NIC.
// - Running directly in the driver.
// - Generic mode, which works with any NIC/driver but lacks
// much of the XDP performance boost.
})
if err != nil {
return fmt.Errorf("failed to attach: %w", err)
}
var (
vethPinDir = xdpcmd.RedirectPinDir(vethIface.Name)
vethMapPath = xdpcmd.TunnelVethMapPath(vethIface.Name)
vethProgramPath = xdpcmd.TunnelVethProgramPath(vethIface.Name)
vethLinkPath = xdpcmd.TunnelVethLinkPath(vethIface.Name)
)
// Create directory /sys/fs/bpf/<device name>/.
if err := os.Mkdir(vethPinDir, 0700); err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create directory for pinning at %s: %v", vethPinDir, err)
}
// Pin the map at /sys/fs/bpf/<device name>/tunnel_host_map.
if err := objects.DevMap.Pin(vethMapPath); err != nil {
return fmt.Errorf("failed to pin map at %s", vethMapPath)
}
log.Infof("Pinned map at %s", vethMapPath)
// Pin the program at /sys/fs/bpf/<device name>/tunnel_host_program.
if err := objects.Program.Pin(vethProgramPath); err != nil {
return fmt.Errorf("failed to pin program at %s", vethProgramPath)
}
log.Infof("Pinned program at %s", vethProgramPath)
// Make everything persistent by pinning the link. Otherwise, the XDP
// program would detach when this process exits.
if err := attached.Pin(vethLinkPath); err != nil {
return fmt.Errorf("failed to pin link at %s", vethLinkPath)
}
log.Infof("Pinned link at %s", vethLinkPath)
// Insert host into veth eBPF map.
// TODO(b/240191988): We should be able to use the existing map instead
// of opening a pinned copy.
pinnedVethMap, err := ebpf.LoadPinnedMap(vethMapPath, nil)
if err != nil {
return fmt.Errorf("failed to load pinned veth map %s: %w", vethMapPath, err)
}
// TODO(b/240191988): Updating of pinned maps should be sychronized and
// check for the existence of the key.
mapKey = uint32(0)
mapVal = uint32(hostIface.Index)
if err := pinnedVethMap.Update(&mapKey, &mapVal, ebpf.UpdateAny); err != nil {
return fmt.Errorf("failed to insert host into veth map %s: %w", vethMapPath, err)
}
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
}
+2
View File
@@ -13,12 +13,14 @@ go_library(
"pass.go",
"redirect_host.go",
"tcpdump.go",
"tunnel.go",
],
embedsrcs = [
"//tools/xdp/cmd/bpf:drop_ebpf.o", # keep
"//tools/xdp/cmd/bpf:pass_ebpf.o", # keep
"//tools/xdp/cmd/bpf:redirect_host_ebpf.o", # keep
"//tools/xdp/cmd/bpf:tcpdump_ebpf.o", # keep
"//tools/xdp/cmd/bpf:tunnel_host_ebpf.o", # keep
],
visibility = ["//:sandbox"],
deps = [
+8
View File
@@ -36,3 +36,11 @@ bpf_program(
bpf_object = "redirect_host_ebpf.o",
visibility = ["//:sandbox"],
)
bpf_program(
name = "tunnel_host_ebpf",
src = "tunnel_host.ebpf.c",
hdrs = [],
bpf_object = "tunnel_host_ebpf.o",
visibility = ["//:sandbox"],
)
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2023 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.
#include <bpf/bpf_endian.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#define section(secname) __attribute__((section(secname), used))
char __license[] section("license") = "Apache-2.0";
// Helper functions are defined positionally in <linux/bpf.h>, and their
// signatures are scattered throughout the kernel. They can be found via the
// defining macro BPF_CALL_[0-5].
// TODO(b/240191988): Use vmlinux instead of this.
static int (*bpf_redirect_map)(void *bpf_map, __u32 iface_index,
__u64 flags) = (void *)51;
struct bpf_map_def {
unsigned int type;
unsigned int key_size;
unsigned int value_size;
unsigned int max_entries;
unsigned int map_flags;
};
struct bpf_map_def section("maps") dev_map = {
.type = BPF_MAP_TYPE_DEVMAP,
.key_size = sizeof(__u32),
.value_size = sizeof(__u32),
.max_entries = 1,
};
// Redirect almost all incoming traffic to go out another device. Certain
// packets are allowed through to the Linux network stack:
//
// - SSH (IPv4 TCP port 22) traffic.
// - Some obviously broken packets.
section("xdp") int xdp_prog(struct xdp_md *ctx) {
void *cursor = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
// Ensure there's space for an ethernet header.
struct ethhdr *eth = cursor;
if ((void *)(eth + 1) > data_end) {
return XDP_PASS;
}
cursor += sizeof(*eth);
// Send all non-IPv4 traffic to the socket.
if (eth->h_proto != bpf_htons(ETH_P_IP)) {
return bpf_redirect_map(&dev_map, ctx->rx_queue_index, XDP_PASS);
}
// IP packets get inspected to allow SSH traffic to the host.
struct iphdr *ip = cursor;
if ((void *)(ip + 1) > data_end) {
return XDP_PASS;
}
cursor += sizeof(*ip);
if (ip->protocol != IPPROTO_TCP) {
return bpf_redirect_map(&dev_map, ctx->rx_queue_index, XDP_PASS);
}
struct tcphdr *tcp = cursor;
if ((void *)(tcp + 1) > data_end) {
return XDP_PASS;
}
// Allow port 22 traffic for SSH debugging.
if (tcp->th_dport == bpf_htons(22)) {
return XDP_PASS;
}
return bpf_redirect_map(&dev_map, ctx->rx_queue_index, XDP_PASS);
}
+40 -29
View File
@@ -21,13 +21,13 @@ import (
"errors"
"fmt"
"log"
"net"
"os"
"path/filepath"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/google/subcommands"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/runsc/flag"
)
@@ -43,19 +43,19 @@ func RedirectPinDir(iface string) string {
// RedirectMapPath returns the path where the eBPF map will be pinned when
// xdp_loader is run against iface.
func RedirectMapPath(iface string) string {
return filepath.Join(RedirectPinDir(iface), "ip_map")
return filepath.Join(RedirectPinDir(iface), "redirect_ip_map")
}
// RedirectProgramPath returns the path where the eBPF program will be pinned
// when xdp_loader is run against iface.
func RedirectProgramPath(iface string) string {
return filepath.Join(RedirectPinDir(iface), "program")
return filepath.Join(RedirectPinDir(iface), "redirect_program")
}
// RedirectLinkPath returns the path where the eBPF link will be pinned when
// xdp_loader is run against iface.
func RedirectLinkPath(iface string) string {
return filepath.Join(RedirectPinDir(iface), "link")
return filepath.Join(RedirectPinDir(iface), "redirect_link")
}
//go:embed bpf/redirect_host_ebpf.o
@@ -107,20 +107,35 @@ func (rc *RedirectHostCommand) execute() error {
return fmt.Errorf("%v", err)
}
var (
pinDir = RedirectPinDir(iface.Name)
mapPath = RedirectMapPath(iface.Name)
programPath = RedirectProgramPath(iface.Name)
linkPath = RedirectLinkPath(iface.Name)
)
return installProgramAndMap(installProgramAndMapOpts{
program: redirectProgram,
iface: iface,
unpin: rc.unpin,
pinDir: RedirectPinDir(iface.Name),
mapPath: RedirectMapPath(iface.Name),
programPath: RedirectProgramPath(iface.Name),
linkPath: RedirectLinkPath(iface.Name),
})
}
type installProgramAndMapOpts struct {
program []byte
iface *net.Interface
unpin bool
pinDir string
mapPath string
programPath string
linkPath string
}
func installProgramAndMap(opts installProgramAndMapOpts) error {
// User just wants to unpin things.
if rc.unpin {
return unpin(mapPath, programPath, linkPath)
if opts.unpin {
return unpin(opts.mapPath, opts.programPath, opts.linkPath)
}
// Load into the kernel.
spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(redirectProgram))
spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(opts.program))
if err != nil {
return fmt.Errorf("failed to load spec: %v", err)
}
@@ -141,39 +156,35 @@ func (rc *RedirectHostCommand) execute() error {
}
}()
attachedLink, cleanup, err := attach(objects.Program, iface)
attachedLink, cleanup, err := attach(objects.Program, opts.iface)
if err != nil {
return fmt.Errorf("failed to attach: %v", err)
}
defer cleanup()
// Create directory /sys/fs/bpf/<device name>/.
if err := os.Mkdir(pinDir, 0700); err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create directory for pinning at %s: %v", pinDir, err)
if err := os.Mkdir(opts.pinDir, 0700); err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create directory for pinning at %s: %v", opts.pinDir, err)
}
// Pin the map at /sys/fs/bpf/<device name>/ip_map.
if err := objects.SockMap.Pin(mapPath); err != nil {
return fmt.Errorf("failed to pin map at %s", mapPath)
if err := objects.SockMap.Pin(opts.mapPath); err != nil {
return fmt.Errorf("failed to pin map at %s", opts.mapPath)
}
log.Printf("Pinned map at %s", mapPath)
log.Printf("Pinned map at %s", opts.mapPath)
// Pin the program at /sys/fs/bpf/<device name>/program.
if err := objects.Program.Pin(programPath); err != nil {
return fmt.Errorf("failed to pin program at %s", programPath)
if err := objects.Program.Pin(opts.programPath); err != nil {
return fmt.Errorf("failed to pin program at %s", opts.programPath)
}
log.Printf("Pinned program at %s", programPath)
log.Printf("Pinned program at %s", opts.programPath)
// Make everything persistent by pinning the link. Otherwise, the XDP
// program would detach when this process exits.
if err := attachedLink.Pin(linkPath); err != nil {
return fmt.Errorf("failed to pin link at %s", linkPath)
}
log.Printf("Pinned link at %s", linkPath)
for false {
unix.Pause()
if err := attachedLink.Pin(opts.linkPath); err != nil {
return fmt.Errorf("failed to pin link at %s", opts.linkPath)
}
log.Printf("Pinned link at %s", opts.linkPath)
return nil
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright 2023 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 cmd
import (
"context"
_ "embed"
"fmt"
"path/filepath"
"github.com/google/subcommands"
"gvisor.dev/gvisor/runsc/flag"
)
// TunnelPinDir returns the directory to which eBPF objects will be pinned when
// xdp_loader is run against iface.
func TunnelPinDir(iface string) string {
return filepath.Join(bpffsDirPath, iface)
}
// TunnelHostMapPath returns the path where the eBPF map will be pinned when
// xdp_loader is run against iface.
func TunnelHostMapPath(iface string) string {
return filepath.Join(TunnelPinDir(iface), "tunnel_host_map")
}
// TunnelHostProgramPath returns the path where the eBPF program will be pinned
// when xdp_loader is run against iface.
func TunnelHostProgramPath(iface string) string {
return filepath.Join(TunnelPinDir(iface), "tunnel_host_program")
}
// TunnelHostLinkPath returns the path where the eBPF link will be pinned when
// xdp_loader is run against iface.
func TunnelHostLinkPath(iface string) string {
return filepath.Join(TunnelPinDir(iface), "tunnel_host_link")
}
// TunnelVethMapPath returns the path where the eBPF map should be pinned when
// xdp_loader is run against iface.
func TunnelVethMapPath(iface string) string {
return filepath.Join(TunnelPinDir(iface), "tunnel_veth_map")
}
// TunnelVethProgramPath returns the path where the eBPF program should be pinned
// when xdp_loader is run against iface.
func TunnelVethProgramPath(iface string) string {
return filepath.Join(TunnelPinDir(iface), "tunnel_veth_program")
}
// TunnelVethLinkPath returns the path where the eBPF link should be pinned when
// xdp_loader is run against iface.
func TunnelVethLinkPath(iface string) string {
return filepath.Join(TunnelPinDir(iface), "tunnel_veth_link")
}
//go:embed bpf/tunnel_host_ebpf.o
var tunnelHostProgram []byte
// TunnelCommand is a subcommand for tunneling traffic between two NICs. It is
// intended as a fast path between the host NIC and the veth of a container.
//
// SSH traffic is not tunneled. It is passed through to the Linux network stack.
type TunnelCommand struct {
device string
deviceIndex int
unpin bool
}
// Name implements subcommands.Command.Name.
func (*TunnelCommand) Name() string {
return "tunnel"
}
// Synopsis implements subcommands.Command.Synopsis.
func (*TunnelCommand) Synopsis() string {
return "Tunnel packets between two interfaces using AF_XDP. Pins eBPF objects in /sys/fs/bpf/<interface name>/."
}
// Usage implements subcommands.Command.Usage.
func (*TunnelCommand) Usage() string {
return "tunnel {-device <device> | -device-idx <device index>} [--unpin]"
}
// SetFlags implements subcommands.Command.SetFlags.
func (tn *TunnelCommand) SetFlags(fs *flag.FlagSet) {
fs.StringVar(&tn.device, "device", "", "which host device to attach to")
fs.IntVar(&tn.deviceIndex, "device-idx", 0, "which host device to attach to")
fs.BoolVar(&tn.unpin, "unpin", false, "unpin the map and program instead of pinning new ones; useful to reset state")
}
// Execute implements subcommands.Command.Execute.
func (tn *TunnelCommand) Execute(context.Context, *flag.FlagSet, ...any) subcommands.ExitStatus {
if err := tn.execute(); err != nil {
fmt.Printf("%v\n", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
func (tn *TunnelCommand) execute() error {
iface, err := getIface(tn.device, tn.deviceIndex)
if err != nil {
return fmt.Errorf("failed to get host iface: %v", err)
}
return installProgramAndMap(installProgramAndMapOpts{
program: tunnelHostProgram,
iface: iface,
unpin: tn.unpin,
pinDir: RedirectPinDir(iface.Name),
mapPath: TunnelHostMapPath(iface.Name),
programPath: TunnelHostProgramPath(iface.Name),
linkPath: TunnelHostLinkPath(iface.Name),
})
}
+1
View File
@@ -34,6 +34,7 @@ func main() {
subcommands.Register(new(cmd.PassCommand), "")
subcommands.Register(new(cmd.RedirectHostCommand), "")
subcommands.Register(new(cmd.TcpdumpCommand), "")
subcommands.Register(new(cmd.TunnelCommand), "")
flag.Parse()
ctx := context.Background()