From 18bfb726977a3999665daeee1cbae75392c25d1e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 8 Jun 2022 12:08:07 -0700 Subject: [PATCH] examples: add example for XDP Add an example for attaching an eBPF program to a network interface with XDP. The example program parses the IPv4 source address from packets (if available) and records packet counts by IP address in an LRU hash map. Issue: https://github.com/cilium/ebpf/issues/645 Signed-off-by: Will Daly --- examples/README.md | 2 + examples/headers/common.h | 17 ++++++ examples/xdp/bpf_bpfeb.go | 119 ++++++++++++++++++++++++++++++++++++++ examples/xdp/bpf_bpfeb.o | Bin 0 -> 3496 bytes examples/xdp/bpf_bpfel.go | 119 ++++++++++++++++++++++++++++++++++++++ examples/xdp/bpf_bpfel.o | Bin 0 -> 3504 bytes examples/xdp/main.go | 83 ++++++++++++++++++++++++++ examples/xdp/xdp.c | 76 ++++++++++++++++++++++++ 8 files changed, 416 insertions(+) create mode 100644 examples/xdp/bpf_bpfeb.go create mode 100644 examples/xdp/bpf_bpfeb.o create mode 100644 examples/xdp/bpf_bpfel.go create mode 100644 examples/xdp/bpf_bpfel.o create mode 100644 examples/xdp/main.go create mode 100644 examples/xdp/xdp.c 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..f68e532c5 100644 --- a/examples/headers/common.h +++ b/examples/headers/common.h @@ -56,6 +56,23 @@ 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; +}; + 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 0000000000000000000000000000000000000000..db4205bc2cdc8d280df4ef36a9820654eec8dc3b GIT binary patch literal 3496 zcmb_eJ!~9B6n?vy*bc-AC?JD`G)WQ8j(xF{KtU99XXQeSoN$UADM`V2bGPSPa<{kH z-3w?b2LdbR*XN$@lU|c`JV=Fe7$ua{LVCHqESJ#wY7DBa-V|P^sbhi z3q+4VyshmH$KL}k#@~4XTF%Go<9zpu`EFN~nM_{yVImhAoA)PkVz;%M`>E!1s_%8g z*`?JZFV&Zg3UT8+aBdhX&tG4u1bhY{=awGsA3(}MGlv6qu?l`;JRaf!!dXHVJf1Xi zg}E^D8gt&6@)@*?g$hSfGd730 zN^2L$cHrGSIHmit@iTSYS0~uOgmY0QI5Kfvw{2{=KWuXc9K^L~Kg;hhM-C?UeV%N- zkBwYm{TD`FWBFSn4_W@+c|Y9zZptAcPn_LD>gtsrdr1BwUoTHo*cQbT{cRm%Rcqtp+RU_JDkA_!AzqytWp zsyb2JTbHeV*vJr-kCcz_63Pu4zupU)Vj2s<8pKIa(weDKrx&hIaiZA;Ydw*7-`SPj zf|e*ny+j-q?Pi<=Z}?(C*ep}xP3_>P?b(I4J?`l7i ze#h^Y4&e(Vsh1`&&4@!mglMe$Ko+a2n~Xsiq%yN`)_Haxx2gLP@6O*bHS%wZpAn1o z)3SQ*5{hz#WFu_ImY+7-(hHl|X(_8UBj({1`)NN8#l=%~S+B0FoS;VAi%H7r`SaCF za%JgqO|D+5*Q8`!CusO#;?vpcMR~Sbm$h@N=a*_LWS86YTa<1@K3(-UBt8#a_d0yN zl-TtKd==6-@DswZwL0E=)uQ}cxhJ&XsSO(#Jzs|!v zZ5m}6T^BlV(2&OmM9o9Y;Sc-+QTrfs_yFH!hUYJM40+sThW=CHG34}|{}a5!GdT=< z+u&KEU(g?;9LD<`<}mCx48BZs55F-t4gQSiKJwi%_&ah&(3dtf=2_F9%!a(4pUD~H zHs_3K?Cv3F64>m=Jx0!qY2iAsIJ0xEO=i{)Su%Q+%No$ztlsLtmmkmcR_zFup5mWo ze2YBc`z^8_eKb$#dok;=|2&NtfHOmk{l++h9Ckipj9PYm->{5aCf?a(8U8lk49m!6 zaixIk1-w?kQ2`GN80*RBzgfVy3V5@Ce=Fd-1^oARtd1pDPkpyM76tz~i}&oEpmORD zQhtDbr@Xj&x(uLR-?oOP?9ib3Q@PW*-c{|Be~xsBdV+$d?5}KK^aC?R0);OdQ?=&d;8k zp=bEy{%<+ZbR*yo5yb$P&n6Z Z>%GE0h3{rljJA!m$sBvM>$dx!{1?JNq4NL$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..056c49021249d30c929beb02a3240ac753389d2f GIT binary patch literal 3504 zcmbtWO>9(E6h1>sX@M3BXn=)zK!Q_fhe6a0p-wABY6?zhNq~iy$INTz$@I-I?+s4X z#Kx%cXG2gEUHB8-VBC*tNCAO&Vy9i`(TxdyI=DuvT1|JgVoj5 z-Kpl^fsLy3vW0GP2Y|S=_x`%^cfZkY!v62Clkbk1t}ri3%0~TM%lq?o6Mx6rKQT~F zHvOu1W_s?(3&k0#7(0N$bR5K_FaOTSZX?QVG|KU(Gs2A``|m>-{kWEBRTEd3vWLM{ zqg=r7FeqQf$Xj_C{5dOs1%4I-)X!e#v?v+Mo{?E|5H-cefGmO0JD*Hw^jkNAPnmx8 zu0nrwC$RA0tVkkrP;A2_ir6~vy+Vdj}E;KLL7UNR=KVg%To3e zUq4h38Kf2isKRKW5yY_y=fkKRw4~8e*P1~yPzDCwR;8N9o|0x1hYL|q_QEKUQqn?_ zMAxY|Y71(<8I{^daz}DU&_b>x1J`R|**nBW*n@Y%%Nor{wpxo8Mv!Q1+*yxzx55G0kc@rpu!NzL1L!n;zRSCym|#o4L3)2gVZ zizC|iP*_)Sqon+DxxuP>z9aOGl3Jyj4eMj$F^wartc~8G`qf00YRxD?06iD|x~kS{ zSDSSeRD;FrLB4@F@spUQQSV?_XEgiX*N)ZIZN@MP6VruE5xYW%$W*0m(HS>4H zk9w2EQ)=SuMHWSccquBW`5-A(lpmEj(`>uesCSfJjUZ_@BJaYq|DYX9>?7M%(G3>zGfZyxDwBNA!68s;#FLKM`Pl5X$i2PvjcfbR^89|8752k4a z&t;5XVDIVA$aZkzLEH;H_!#kfM0^Z*!zjQbsK;E9T^=85)T8rs8g5!WN_G)HE;~B% zWUYM<^uzjX7+(Z&FL{&TD8rZ(c*amUF#GOv|Vh43fh)Y&~A~eD}93W<0aq z>%fd3#URm6QvrkIaM8dvzMR7K6kbkY*RRX>Rmy%lh3}>C?uI^^P{ zk@#067cVVpZqIo75^<$vW^yvGo;BehCq zF%{Y#-2&IWlMVwsD&CzJ7t`+q%bx^is|AZ4%Ueg!D%`)Ai{EbbC&5`iy#^IKQFs2V zyW36<$}2V=*GU((Y z@-`vYfvWWS@!!j~YCR3h+YM1j8PoN7C#_e1*%or=mG$|bny&vebUibxHLzujHr=rM gH~n5y?Z_2Gq~9Hr)Y%y5o_?A+vUgXX|N5l=0AG=z>Hq)$ literal 0 HcmV?d00001 diff --git a/examples/xdp/main.go b/examples/xdp/main.go new file mode 100644 index 000000000..bf4e2ddad --- /dev/null +++ b/examples/xdp/main.go @@ -0,0 +1,83 @@ +//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! +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 + var key, val uint32 + iter := m.Iterate() + for iter.Next(&key, &val) { + sourceIP := net.IPv4(byte(key>>24), byte(key>>16&0xFF), byte(key>>8)&0xFF, byte(key&0xFF)) + 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..0a41af50d --- /dev/null +++ b/examples/xdp/xdp.c @@ -0,0 +1,76 @@ +// +build ignore + +#include +#include +#include "bpf_endian.h" +#include "common.h" + +char __license[] SEC("license") = "Dual MIT/GPL"; + +const int MAX_MAP_ENTRIES = 16; + +/* Define an LRU hash map for storing packet count by source IPv4 address */ +struct bpf_map_def SEC("maps") xdp_stats_map = { + .type = BPF_MAP_TYPE_LRU_HASH, + .key_size = sizeof(__u32), // source IPv4 address + .value_size = sizeof(__u32), // packet count + .max_entries = MAX_MAP_ENTRIES, +}; + +/* +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; + void *pos = data; + + // First, parse the ethernet header. + struct ethhdr *eth = pos; + int hdrsize = sizeof(*eth); + if (pos + hdrsize > data_end) { + return 0; + } + + pos += hdrsize; + 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 = pos; + hdrsize = sizeof(*ip); + if (pos + hdrsize > data_end) { + return 0; + } + + // Return the source IP address in host byte order. + *ip_src_addr = (__u32)bpf_ntohl(ip->saddr); + return 1; +} + +SEC("xdp_prog") +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; +}