diff --git a/examples/README.md b/examples/README.md index 53fad305d..6021865c6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,8 @@ Like kprobes, but with better performance and usability, for kernels 5.5 and later. * [tcp_connect](fentry/) - Trace outgoing IPv4 TCP connections. * [tcp_close](tcprtt/) - Log RTT of IPv4 TCP connections using eBPF CO-RE helpers. +* XDP - Attach a program to a network interface to process incoming packets. + * [xdp](xdp/) - Print packet counts by IPv4 source address. * Add your use case(s) here! ## How to run diff --git a/examples/headers/common.h b/examples/headers/common.h index e333df33d..75e72415a 100644 --- a/examples/headers/common.h +++ b/examples/headers/common.h @@ -56,6 +56,47 @@ enum bpf_map_type { BPF_MAP_TYPE_INODE_STORAGE = 28, }; +enum xdp_action { + XDP_ABORTED = 0, + XDP_DROP = 1, + XDP_PASS = 2, + XDP_TX = 3, + XDP_REDIRECT = 4, +}; + +struct xdp_md { + __u32 data; + __u32 data_end; + __u32 data_meta; + __u32 ingress_ifindex; + __u32 rx_queue_index; + __u32 egress_ifindex; +}; + +typedef __u16 __sum16; + +#define ETH_P_IP 0x0800 + +struct ethhdr { + unsigned char h_dest[6]; + unsigned char h_source[6]; + __be16 h_proto; +}; + +struct iphdr { + __u8 ihl: 4; + __u8 version: 4; + __u8 tos; + __be16 tot_len; + __be16 id; + __be16 frag_off; + __u8 ttl; + __u8 protocol; + __sum16 check; + __be32 saddr; + __be32 daddr; +}; + enum { BPF_ANY = 0, BPF_NOEXIST = 1, diff --git a/examples/xdp/bpf_bpfeb.go b/examples/xdp/bpf_bpfeb.go new file mode 100644 index 000000000..543f476b6 --- /dev/null +++ b/examples/xdp/bpf_bpfeb.go @@ -0,0 +1,119 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 +// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64 + +package main + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +// loadBpf returns the embedded CollectionSpec for bpf. +func loadBpf() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_BpfBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load bpf: %w", err) + } + + return spec, err +} + +// loadBpfObjects loads bpf and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *bpfObjects +// *bpfPrograms +// *bpfMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadBpf() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// bpfSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfSpecs struct { + bpfProgramSpecs + bpfMapSpecs +} + +// bpfSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfProgramSpecs struct { + XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"` +} + +// bpfMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfMapSpecs struct { + XdpStatsMap *ebpf.MapSpec `ebpf:"xdp_stats_map"` +} + +// bpfObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfObjects struct { + bpfPrograms + bpfMaps +} + +func (o *bpfObjects) Close() error { + return _BpfClose( + &o.bpfPrograms, + &o.bpfMaps, + ) +} + +// bpfMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfMaps struct { + XdpStatsMap *ebpf.Map `ebpf:"xdp_stats_map"` +} + +func (m *bpfMaps) Close() error { + return _BpfClose( + m.XdpStatsMap, + ) +} + +// bpfPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfPrograms struct { + XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"` +} + +func (p *bpfPrograms) Close() error { + return _BpfClose( + p.XdpProgFunc, + ) +} + +func _BpfClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +//go:embed bpf_bpfeb.o +var _BpfBytes []byte diff --git a/examples/xdp/bpf_bpfeb.o b/examples/xdp/bpf_bpfeb.o new file mode 100644 index 000000000..2a4b9158f Binary files /dev/null and b/examples/xdp/bpf_bpfeb.o differ diff --git a/examples/xdp/bpf_bpfel.go b/examples/xdp/bpf_bpfel.go new file mode 100644 index 000000000..60d56a236 --- /dev/null +++ b/examples/xdp/bpf_bpfel.go @@ -0,0 +1,119 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 +// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64 + +package main + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +// loadBpf returns the embedded CollectionSpec for bpf. +func loadBpf() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_BpfBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load bpf: %w", err) + } + + return spec, err +} + +// loadBpfObjects loads bpf and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *bpfObjects +// *bpfPrograms +// *bpfMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadBpf() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// bpfSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfSpecs struct { + bpfProgramSpecs + bpfMapSpecs +} + +// bpfSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfProgramSpecs struct { + XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"` +} + +// bpfMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfMapSpecs struct { + XdpStatsMap *ebpf.MapSpec `ebpf:"xdp_stats_map"` +} + +// bpfObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfObjects struct { + bpfPrograms + bpfMaps +} + +func (o *bpfObjects) Close() error { + return _BpfClose( + &o.bpfPrograms, + &o.bpfMaps, + ) +} + +// bpfMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfMaps struct { + XdpStatsMap *ebpf.Map `ebpf:"xdp_stats_map"` +} + +func (m *bpfMaps) Close() error { + return _BpfClose( + m.XdpStatsMap, + ) +} + +// bpfPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfPrograms struct { + XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"` +} + +func (p *bpfPrograms) Close() error { + return _BpfClose( + p.XdpProgFunc, + ) +} + +func _BpfClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +//go:embed bpf_bpfel.o +var _BpfBytes []byte diff --git a/examples/xdp/bpf_bpfel.o b/examples/xdp/bpf_bpfel.o new file mode 100644 index 000000000..a4ecd465f Binary files /dev/null and b/examples/xdp/bpf_bpfel.o differ diff --git a/examples/xdp/main.go b/examples/xdp/main.go new file mode 100644 index 000000000..0a29d8501 --- /dev/null +++ b/examples/xdp/main.go @@ -0,0 +1,87 @@ +//go:build linux +// +build linux + +// This program demonstrates attaching an eBPF program to a network interface +// with XDP (eXpress Data Path). The program parses the IPv4 source address +// from packets and writes the packet count by IP to an LRU hash map. +// The userspace program (Go code in this file) prints the contents +// of the map to stdout every second. +// It is possible to modify the XDP program to drop or redirect packets +// as well -- give it a try! +// This example depends on bpf_link, available in Linux kernel version 5.7 or newer. +package main + +import ( + "fmt" + "log" + "net" + "os" + "strings" + "time" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" +) + +// $BPF_CLANG and $BPF_CFLAGS are set by the Makefile. +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf xdp.c -- -I../headers + +func main() { + if len(os.Args) < 2 { + log.Fatalf("Please specify a network interface") + } + + // Look up the network interface by name. + ifaceName := os.Args[1] + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + log.Fatalf("lookup network iface %q: %s", ifaceName, err) + } + + // Load pre-compiled programs into the kernel. + objs := bpfObjects{} + if err := loadBpfObjects(&objs, nil); err != nil { + log.Fatalf("loading objects: %s", err) + } + defer objs.Close() + + // Attach the program. + l, err := link.AttachXDP(link.XDPOptions{ + Program: objs.XdpProgFunc, + Interface: iface.Index, + }) + if err != nil { + log.Fatalf("could not attach XDP program: %s", err) + } + defer l.Close() + + log.Printf("Attached XDP program to iface %q (index %d)", iface.Name, iface.Index) + log.Printf("Press Ctrl-C to exit and remove the program") + + // Print the contents of the BPF hash map (source IP address -> packet count). + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for range ticker.C { + s, err := formatMapContents(objs.XdpStatsMap) + if err != nil { + log.Printf("Error reading map: %s", err) + continue + } + log.Printf("Map contents:\n%s", s) + } +} + +func formatMapContents(m *ebpf.Map) (string, error) { + var ( + sb strings.Builder + key []byte + val uint32 + ) + iter := m.Iterate() + for iter.Next(&key, &val) { + sourceIP := net.IP(key) // IPv4 source address in network byte order. + packetCount := val + sb.WriteString(fmt.Sprintf("\t%s => %d\n", sourceIP, packetCount)) + } + return sb.String(), iter.Err() +} diff --git a/examples/xdp/xdp.c b/examples/xdp/xdp.c new file mode 100644 index 000000000..6c5797ca3 --- /dev/null +++ b/examples/xdp/xdp.c @@ -0,0 +1,70 @@ +// +build ignore + +#include "bpf_endian.h" +#include "common.h" + +char __license[] SEC("license") = "Dual MIT/GPL"; + +#define MAX_MAP_ENTRIES 16 + +/* Define an LRU hash map for storing packet count by source IPv4 address */ +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, MAX_MAP_ENTRIES); + __type(key, __u32); // source IPv4 address + __type(value, __u32); // packet count +} xdp_stats_map SEC(".maps"); + +/* +Attempt to parse the IPv4 source address from the packet. +Returns 0 if there is no IPv4 header field; otherwise returns non-zero. +*/ +static __always_inline int parse_ip_src_addr(struct xdp_md *ctx, __u32 *ip_src_addr) { + void *data_end = (void *)(long)ctx->data_end; + void *data = (void *)(long)ctx->data; + + // First, parse the ethernet header. + struct ethhdr *eth = data; + if ((void *)(eth + 1) > data_end) { + return 0; + } + + if (eth->h_proto != bpf_htons(ETH_P_IP)) { + // The protocol is not IPv4, so we can't parse an IPv4 source address. + return 0; + } + + // Then parse the IP header. + struct iphdr *ip = (void *)(eth + 1); + if ((void *)(ip + 1) > data_end) { + return 0; + } + + // Return the source IP address in network byte order. + *ip_src_addr = (__u32)(ip->saddr); + return 1; +} + +SEC("xdp") +int xdp_prog_func(struct xdp_md *ctx) { + __u32 ip; + if (!parse_ip_src_addr(ctx, &ip)) { + // Not an IPv4 packet, so don't count it. + goto done; + } + + __u32 *pkt_count = bpf_map_lookup_elem(&xdp_stats_map, &ip); + if (!pkt_count) { + // No entry in the map for this IP address yet, so set the initial value to 1. + __u32 init_pkt_count = 1; + bpf_map_update_elem(&xdp_stats_map, &ip, &init_pkt_count, BPF_ANY); + } else { + // Entry already exists for this IP address, + // so increment it atomically using an LLVM built-in. + __sync_fetch_and_add(pkt_count, 1); + } + +done: + // Try changing this to XDP_DROP and see what happens! + return XDP_PASS; +}