diff --git a/tools/xdp/BUILD b/tools/xdp/BUILD index d3d1706ab..f4c152170 100644 --- a/tools/xdp/BUILD +++ b/tools/xdp/BUILD @@ -11,13 +11,16 @@ go_binary( "drop.go", "main.go", "pass.go", + "redirect_host.go", "tcpdump.go", ], embedsrcs = [ "//tools/xdp/bpf:drop_ebpf.o", # keep "//tools/xdp/bpf:pass_ebpf.o", # keep + "//tools/xdp/bpf:redirect_host_ebpf.o", # keep "//tools/xdp/bpf:tcpdump_ebpf.o", # keep ], + pure = True, visibility = ["//:sandbox"], deps = [ "//pkg/buffer", diff --git a/tools/xdp/bpf/BUILD b/tools/xdp/bpf/BUILD index aedba5c63..c0fbcbba3 100644 --- a/tools/xdp/bpf/BUILD +++ b/tools/xdp/bpf/BUILD @@ -28,3 +28,11 @@ bpf_program( bpf_object = "tcpdump_ebpf.o", visibility = ["//:sandbox"], ) + +bpf_program( + name = "redirect_host_ebpf", + src = "redirect_host.ebpf.c", + hdrs = [], + bpf_object = "redirect_host_ebpf.o", + visibility = ["//:sandbox"], +) diff --git a/tools/xdp/bpf/redirect_host.ebpf.c b/tools/xdp/bpf/redirect_host.ebpf.c new file mode 100644 index 000000000..cd99a4e21 --- /dev/null +++ b/tools/xdp/bpf/redirect_host.ebpf.c @@ -0,0 +1,100 @@ +// 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 + +#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") sock_map = { + .type = BPF_MAP_TYPE_XSKMAP, // Note: "XSK" means AF_XDP socket. + .key_size = sizeof(__u32), + .value_size = sizeof(__u32), + .max_entries = 1, +}; + +// TODO(b/240191988): It would be better to use bfp_htons() in +// . However, we test on ARM64 and AMD64 with the same Docker +// image, and each architecture requires different packages to get that header. +const __u16 swapped_eth_p_ip = (__u16)((ETH_P_IP << 8) | (ETH_P_IP >> 8)); + +// Redirect almost all incoming traffic to an AF_XDP socket. 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 != swapped_eth_p_ip) { + return bpf_redirect_map(&sock_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); + + // TODO(b/240191988): It would be better to use IPPROTO_TCP in . + // However, we test on ARM64 and AMD64 with the same Docker image, and each + // architecture requires different packages to get that header. + if (ip->protocol != 6 /* IPPROTO_TCP */) { + return bpf_redirect_map(&sock_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. + // TODO(b/240191988): It would be better to use bfp_ntohs() in + // . However, we test on ARM64 and AMD64 with the same + // Docker image, and each architecture requires different packages to get that + // header. + if (__builtin_bswap16(tcp->th_dport) == 22) { + return XDP_PASS; + } + + return bpf_redirect_map(&sock_map, ctx->rx_queue_index, XDP_PASS); +} diff --git a/tools/xdp/main.go b/tools/xdp/main.go index cfe2667a1..429edd686 100644 --- a/tools/xdp/main.go +++ b/tools/xdp/main.go @@ -38,6 +38,7 @@ import ( func main() { subcommands.Register(new(DropCommand), "") subcommands.Register(new(PassCommand), "") + subcommands.Register(new(RedirectHostCommand), "") subcommands.Register(new(TcpdumpCommand), "") flag.Parse() @@ -69,7 +70,7 @@ func runBasicProgram(progData []byte, device string, deviceIndex int) error { } }() - cleanup, err := attach(objects.Program, iface) + _, cleanup, err := attach(objects.Program, iface) if err != nil { return fmt.Errorf("failed to attach: %v", err) } @@ -100,7 +101,7 @@ func getIface(device string, deviceIndex int) (*net.Interface, error) { } } -func attach(program *ebpf.Program, iface *net.Interface) (func(), error) { +func attach(program *ebpf.Program, iface *net.Interface) (link.Link, func(), error) { // Attach the program to the XDP hook on the device. Fallback from best // to worst mode. modes := []struct { @@ -126,9 +127,9 @@ func attach(program *ebpf.Program, iface *net.Interface) (func(), error) { log.Printf("failed to attach with mode %q: %v", mode.name, err) } if attached == nil { - return nil, fmt.Errorf("failed to attach program") + return nil, nil, fmt.Errorf("failed to attach program") } - return func() { attached.Close() }, nil + return attached, func() { attached.Close() }, nil } func waitForever() { diff --git a/tools/xdp/redirect_host.go b/tools/xdp/redirect_host.go new file mode 100644 index 000000000..202847bf2 --- /dev/null +++ b/tools/xdp/redirect_host.go @@ -0,0 +1,191 @@ +// 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 main + +import ( + "bytes" + "context" + _ "embed" + "errors" + "fmt" + "log" + "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" +) + +//go:embed bpf/redirect_host_ebpf.o +var redirectProgram []byte + +// RedirectHostCommand is a subcommand for redirecting incoming packets based +// on a pinned eBPF map. It redirects all non-SSH traffic to a single AF_XDP +// socket. +type RedirectHostCommand struct { + device string + deviceIndex int + unpin bool +} + +// Name implements subcommands.Command.Name. +func (*RedirectHostCommand) Name() string { + return "redirect" +} + +// Synopsis implements subcommands.Command.Synopsis. +func (*RedirectHostCommand) Synopsis() string { + return "Redirect incoming packets to an AF_XDP socket. Pins eBPF objects in /sys/fs/bpf//." +} + +// Usage implements subcommands.Command.Usage. +func (*RedirectHostCommand) Usage() string { + return "redirect {-device | -device-idx } [--unpin]" +} + +// SetFlags implements subcommands.Command.SetFlags. +func (rc *RedirectHostCommand) SetFlags(fs *flag.FlagSet) { + fs.StringVar(&rc.device, "device", "", "which device to attach to") + fs.IntVar(&rc.deviceIndex, "device-idx", 0, "which device to attach to") + fs.BoolVar(&rc.unpin, "unpin", false, "unpin the map and program instead of pinning new ones; useful to reset state") +} + +// Execute implements subcommands.Command.Execute. +func (rc *RedirectHostCommand) Execute(context.Context, *flag.FlagSet, ...any) subcommands.ExitStatus { + if err := rc.execute(); err != nil { + fmt.Printf("%v\n", err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +func (rc *RedirectHostCommand) execute() error { + iface, err := getIface(rc.device, rc.deviceIndex) + if err != nil { + return fmt.Errorf("%v", err) + } + + const dirName = "/sys/fs/bpf/" + var ( + pinDir = filepath.Join(dirName, iface.Name) + mapPath = filepath.Join(pinDir, "ip_map") + programPath = filepath.Join(pinDir, "program") + linkPath = filepath.Join(pinDir, "link") + ) + + // User just wants to unpin things. + if rc.unpin { + return unpin(mapPath, programPath, linkPath) + } + + // Load into the kernel. + spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(redirectProgram)) + if err != nil { + return 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 fmt.Errorf("failed to load program: %v", err) + } + defer func() { + if err := objects.Program.Close(); err != nil { + log.Printf("failed to close program: %v", err) + } + if err := objects.SockMap.Close(); err != nil { + log.Printf("failed to close sock map: %v", err) + } + }() + + attachedLink, cleanup, err := attach(objects.Program, 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) + } + + // 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) + } + log.Printf("Pinned map at %s", 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) + } + log.Printf("Pinned program at %s", 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() + } + + return nil +} + +func unpin(mapPath, programPath, linkPath string) error { + // Try to unpin both the map and program even if only one is found. + mapErr := func() error { + pinnedMap, err := ebpf.LoadPinnedMap(mapPath, nil) + if err != nil { + return fmt.Errorf("failed to load pinned map at %s for unpinning: %v", mapPath, err) + } + if err := pinnedMap.Unpin(); err != nil { + return fmt.Errorf("failed to unpin map %s: %v", mapPath, err) + } + log.Printf("Unpinned map at %s", mapPath) + return nil + }() + programErr := func() error { + pinnedProgram, err := ebpf.LoadPinnedProgram(programPath, nil) + if err != nil { + return fmt.Errorf("failed to load pinned program at %s for unpinning: %v", programPath, err) + } + if err := pinnedProgram.Unpin(); err != nil { + return fmt.Errorf("failed to unpin program %s: %v", programPath, err) + } + log.Printf("Unpinned program at %s", programPath) + return nil + }() + linkErr := func() error { + pinnedLink, err := link.LoadPinnedLink(linkPath, nil) + if err != nil { + return fmt.Errorf("failed to load pinned link at %s for unpinning: %v", linkPath, err) + } + if err := pinnedLink.Unpin(); err != nil { + return fmt.Errorf("failed to unpin link %s: %v", linkPath, err) + } + log.Printf("Unpinned link at %s", linkPath) + return nil + }() + return errors.Join(mapErr, programErr, linkErr) +} diff --git a/tools/xdp/tcpdump.go b/tools/xdp/tcpdump.go index bdf91e5b5..4d87dbec9 100644 --- a/tools/xdp/tcpdump.go +++ b/tools/xdp/tcpdump.go @@ -100,7 +100,7 @@ func (pc *TcpdumpCommand) execute() error { } }() - cleanup, err := attach(objects.Program, iface) + _, cleanup, err := attach(objects.Program, iface) if err != nil { return fmt.Errorf("failed to attach: %v", err) }