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()