|
| 1 | +// This program demonstrates attaching an eBPF program to a network interface |
| 2 | +// with XDP (eXpress Data Path). The program parses the IPv4 source address |
| 3 | +// from packets and pushes the address alongside the computed packet arrival timestamp |
| 4 | +// into a Queue. This is just an example and probably does not represent the most |
| 5 | +// efficient way to perform such a task. Another potential solution would be to use |
| 6 | +// an HashMap with a small __u64 arrays associated to each IPv4 address (key). |
| 7 | +// In both the two ways it is possible to lose some packet if (a) queue is not large |
| 8 | +// enough or the packet processing time is slow or (b) if the associated array is |
| 9 | +// smaller than the actual received packet from an address. |
| 10 | +// The userspace program (Go code in this file) prints the contents |
| 11 | +// of the map to stdout every second, parsing the raw structure into a human-readable |
| 12 | +// IPv4 address and Unix timestamp. |
| 13 | +// It is possible to modify the XDP program to drop or redirect packets |
| 14 | +// as well -- give it a try! |
| 15 | +// This example depends on bpf_link, available in Linux kernel version 5.7 or newer. |
| 16 | +package main |
| 17 | + |
| 18 | +import ( |
| 19 | + "encoding/binary" |
| 20 | + "fmt" |
| 21 | + "log" |
| 22 | + "net" |
| 23 | + "net/netip" |
| 24 | + "os" |
| 25 | + "strings" |
| 26 | + "syscall" |
| 27 | + "time" |
| 28 | + |
| 29 | + "github.com/cilium/ebpf" |
| 30 | + "github.com/cilium/ebpf/link" |
| 31 | +) |
| 32 | + |
| 33 | +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf xdp.c -- -I../headers |
| 34 | + |
| 35 | +func main() { |
| 36 | + if len(os.Args) < 2 { |
| 37 | + log.Fatalf("Please specify a network interface") |
| 38 | + } |
| 39 | + |
| 40 | + // Look up the network interface by name. |
| 41 | + ifaceName := os.Args[1] |
| 42 | + iface, err := net.InterfaceByName(ifaceName) |
| 43 | + if err != nil { |
| 44 | + log.Fatalf("lookup network iface %q: %s", ifaceName, err) |
| 45 | + } |
| 46 | + |
| 47 | + // Load pre-compiled programs into the kernel. |
| 48 | + objs := bpfObjects{} |
| 49 | + if err := loadBpfObjects(&objs, nil); err != nil { |
| 50 | + log.Fatalf("loading objects: %s", err) |
| 51 | + } |
| 52 | + defer objs.Close() |
| 53 | + |
| 54 | + // Attach the program. |
| 55 | + l, err := link.AttachXDP(link.XDPOptions{ |
| 56 | + Program: objs.XdpProgFunc, |
| 57 | + Interface: iface.Index, |
| 58 | + }) |
| 59 | + if err != nil { |
| 60 | + log.Fatalf("could not attach XDP program: %s", err) |
| 61 | + } |
| 62 | + defer l.Close() |
| 63 | + |
| 64 | + log.Printf("Attached XDP program to iface %q (index %d)", iface.Name, iface.Index) |
| 65 | + log.Printf("Press Ctrl-C to exit and remove the program") |
| 66 | + |
| 67 | + // Retrieve boot time once, and use it for every later ktime conversions |
| 68 | + bootTime := getSysBoot() |
| 69 | + |
| 70 | + // Print the contents of the BPF queue (packet source IP address and timestamp). |
| 71 | + ticker := time.NewTicker(1 * time.Second) |
| 72 | + defer ticker.Stop() |
| 73 | + for range ticker.C { |
| 74 | + s, err := formatMapContents(objs.QueueWithData, bootTime) |
| 75 | + if err != nil { |
| 76 | + log.Printf("Error reading map: %s", err) |
| 77 | + continue |
| 78 | + } |
| 79 | + log.Printf("Map contents:\n%s", s) |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +// formatMapContents formats an output string with the content of the provided map. |
| 84 | +// |
| 85 | +// For each entry, the function outputs a line containing the human-readable IPv4 address |
| 86 | +// retrieved from the packet structure formatted and the converted ktime_ns into Unix Time |
| 87 | +// |
| 88 | +// In case of error or empty map, the function returns the corresponding error. |
| 89 | +func formatMapContents(m *ebpf.Map, bootTime time.Time) (string, error) { |
| 90 | + var ( |
| 91 | + sb strings.Builder |
| 92 | + val bpfPacketData |
| 93 | + ) |
| 94 | + iter := m.Iterate() |
| 95 | + for iter.Next(nil, &val) { |
| 96 | + // Convert the __u32 into human-readable IPv4 |
| 97 | + a4 := [4]byte{} |
| 98 | + binary.LittleEndian.PutUint32(a4[:], val.SrcIp) |
| 99 | + addr := netip.AddrFrom4(a4) |
| 100 | + |
| 101 | + // Convert ktime timestamp into Time struct, adding the retrieved |
| 102 | + // timestamp to the previously computer boot time |
| 103 | + t := bootTime.Add(time.Duration(val.Timestamp) * time.Nanosecond) |
| 104 | + |
| 105 | + sb.WriteString(fmt.Sprintf("\t%s - %s\n", addr, t)) |
| 106 | + } |
| 107 | + return sb.String(), iter.Err() |
| 108 | +} |
| 109 | + |
| 110 | +// Retrieve system boot time and convert it into Time struct |
| 111 | +func getSysBoot() time.Time { |
| 112 | + sysInfo := &syscall.Sysinfo_t{} |
| 113 | + syscall.Sysinfo(sysInfo) |
| 114 | + return time.Now().Add(-time.Duration(sysInfo.Uptime) * time.Second) |
| 115 | +} |
0 commit comments