mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
xdp: add a program that redirects packets from a NIC to an AF_XDP socket
Usage is something like: ``` $ xdp_loader redirect --devidx 2 --unpin # Clear xdp_loader state on device 2 $ xdp_loader redirect --devidx 2 # Add and pin a map and program on device 2 ``` When the map and program are pinned, a socket can be inserted so that packets are routed directly from the NIC to that socket. SSH packets (IPv4 TCP port 22) packets are allowed through for debugging. Also, make xdp_loader fully static for ease of deployment. PiperOrigin-RevId: 590955764
This commit is contained in:
committed by
gVisor bot
parent
1c6f0fe9e1
commit
f6d380ad8c
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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 <linux/bpf.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/ip.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") 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
|
||||
// <bpf/bpf_endian.h>. 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 <netinet/in.h>.
|
||||
// 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
|
||||
// <bpf/bpf_endian.h>. 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);
|
||||
}
|
||||
+5
-4
@@ -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() {
|
||||
|
||||
@@ -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/<interface name>/."
|
||||
}
|
||||
|
||||
// Usage implements subcommands.Command.Usage.
|
||||
func (*RedirectHostCommand) Usage() string {
|
||||
return "redirect {-device <device> | -device-idx <device index>} [--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/<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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
log.Printf("Pinned map at %s", 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user