From 9e2db2c1315cda3b0911bd8c982cdbc532158498 Mon Sep 17 00:00:00 2001 From: Kevin Krakauer Date: Thu, 4 Jan 2024 10:42:25 -0800 Subject: [PATCH] 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 --- runsc/config/config.go | 12 ++ runsc/config/flags.go | 2 +- runsc/sandbox/bpf/BUILD | 9 + runsc/sandbox/bpf/bpf.go | 6 + runsc/sandbox/bpf/tunnel_veth.ebpf.c | 46 +++++ runsc/sandbox/network.go | 64 ++++--- runsc/sandbox/xdp.go | 276 ++++++++++++++++++++++++--- tools/xdp/cmd/BUILD | 2 + tools/xdp/cmd/bpf/BUILD | 8 + tools/xdp/cmd/bpf/tunnel_host.ebpf.c | 90 +++++++++ tools/xdp/cmd/redirect_host.go | 69 ++++--- tools/xdp/cmd/tunnel.go | 128 +++++++++++++ tools/xdp/main.go | 1 + 13 files changed, 630 insertions(+), 83 deletions(-) create mode 100644 runsc/sandbox/bpf/tunnel_veth.ebpf.c create mode 100644 tools/xdp/cmd/bpf/tunnel_host.ebpf.c create mode 100644 tools/xdp/cmd/tunnel.go diff --git a/runsc/config/config.go b/runsc/config/config.go index 998fd94eb..0ff0e894d 100644 --- a/runsc/config/config.go +++ b/runsc/config/config.go @@ -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) } diff --git a/runsc/config/flags.go b/runsc/config/flags.go index 76a9ce540..0a2b7b9f6 100644 --- a/runsc/config/flags.go +++ b/runsc/config/flags.go @@ -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:"`) + flagSet.Var(&xdpConfig, "EXPERIMENTAL-xdp", `whether and how to use XDP. Can be one of: "off" (default), "ns", "redirect:", or "tunnel:"`) 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.") diff --git a/runsc/sandbox/bpf/BUILD b/runsc/sandbox/bpf/BUILD index acd0ce8e7..4d5d6420f 100644 --- a/runsc/sandbox/bpf/BUILD +++ b/runsc/sandbox/bpf/BUILD @@ -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"], +) diff --git a/runsc/sandbox/bpf/bpf.go b/runsc/sandbox/bpf/bpf.go index 59ec6f00c..5da26a2da 100644 --- a/runsc/sandbox/bpf/bpf.go +++ b/runsc/sandbox/bpf/bpf.go @@ -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 diff --git a/runsc/sandbox/bpf/tunnel_veth.ebpf.c b/runsc/sandbox/bpf/tunnel_veth.ebpf.c new file mode 100644 index 000000000..a6013206c --- /dev/null +++ b/runsc/sandbox/bpf/tunnel_veth.ebpf.c @@ -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 + +#define section(secname) __attribute__((section(secname), used)) + +char __license[] section("license") = "Apache-2.0"; + +// Helper functions are defined positionally in , 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); +} diff --git a/runsc/sandbox/network.go b/runsc/sandbox/network.go index 27b121756..afbbde5c0 100644 --- a/runsc/sandbox/network.go +++ b/runsc/sandbox/network.go @@ -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: // diff --git a/runsc/sandbox/xdp.go b/runsc/sandbox/xdp.go index 7cfe07304..7b6eb7388 100644 --- a/runsc/sandbox/xdp.go +++ b/runsc/sandbox/xdp.go @@ -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//. + 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//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//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 +} diff --git a/tools/xdp/cmd/BUILD b/tools/xdp/cmd/BUILD index ba4e6b186..7fb613d0e 100644 --- a/tools/xdp/cmd/BUILD +++ b/tools/xdp/cmd/BUILD @@ -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 = [ diff --git a/tools/xdp/cmd/bpf/BUILD b/tools/xdp/cmd/bpf/BUILD index c0fbcbba3..c94ec3b25 100644 --- a/tools/xdp/cmd/bpf/BUILD +++ b/tools/xdp/cmd/bpf/BUILD @@ -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"], +) diff --git a/tools/xdp/cmd/bpf/tunnel_host.ebpf.c b/tools/xdp/cmd/bpf/tunnel_host.ebpf.c new file mode 100644 index 000000000..29b00c61f --- /dev/null +++ b/tools/xdp/cmd/bpf/tunnel_host.ebpf.c @@ -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 +#include +#include +#include +#include +#include + +#define section(secname) __attribute__((section(secname), used)) + +char __license[] section("license") = "Apache-2.0"; + +// Helper functions are defined positionally in , 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); +} diff --git a/tools/xdp/cmd/redirect_host.go b/tools/xdp/cmd/redirect_host.go index 206499cb9..a00512549 100644 --- a/tools/xdp/cmd/redirect_host.go +++ b/tools/xdp/cmd/redirect_host.go @@ -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//. - 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//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//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 } diff --git a/tools/xdp/cmd/tunnel.go b/tools/xdp/cmd/tunnel.go new file mode 100644 index 000000000..c2b23ded5 --- /dev/null +++ b/tools/xdp/cmd/tunnel.go @@ -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//." +} + +// Usage implements subcommands.Command.Usage. +func (*TunnelCommand) Usage() string { + return "tunnel {-device | -device-idx } [--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), + }) +} diff --git a/tools/xdp/main.go b/tools/xdp/main.go index c3394e937..3a954dd85 100644 --- a/tools/xdp/main.go +++ b/tools/xdp/main.go @@ -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()