File: getting-started.md

package info (click to toggle)
golang-github-cilium-ebpf 0.17.3%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 4,684 kB
  • sloc: ansic: 1,259; makefile: 127; python: 113; awk: 29; sh: 24
file content (268 lines) | stat: -rw-r--r-- 12,092 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# Getting Started with eBPF in Go

In this guide, we'll walk you through building a new eBPF-powered Go application
from scratch. We'll introduce the toolchain, write a minimal eBPF C example and
compile it using bpf2go. Then, we'll put together a Go
application that loads the eBPF program into the kernel and periodically
displays its output.

The application attaches an eBPF program to an XDP hook that counts the number
of packets received by a physical interface. Filtering and modifying packets is
a major use case for eBPF, so you'll see a lot of its features being geared
towards it. However, eBPF's capabilities are ever-growing, and it has been
adopted for tracing, systems and application observability, security and much
more.

## eBPF C program

!!! abstract "Dependencies"
    To follow along with the example, you'll need:

    * Linux kernel version 5.7 or later, for bpf_link support
    * LLVM 11 or later [^1] (`clang` and `llvm-strip`)
    * libbpf headers [^2]
    * Linux kernel headers [^3]
    * Go compiler version supported by {{ proj }}'s Go module

[^1]:
    Use `clang --version` to check which version of LLVM you have installed.
    Refer to your distribution's package index to finding the right packages to
    install, as this tends to vary wildly across distributions. Some
    distributions ship `clang` and `llvm-strip` in separate packages.

[^2]:
    For Debian/Ubuntu, you'll typically need `libbpf-dev`. On Fedora, it's
    `libbpf-devel`.

[^3]:
    On AMD64 Debian/Ubuntu, install `linux-headers-amd64`. On Fedora, install
    `kernel-devel`.

    On Debian, you may also need `ln -sf /usr/include/asm-generic/
    /usr/include/asm` since the example expects to find `<asm/types.h>`.

Let's begin by writing our eBPF C program, as its structure will be used as the
basis for generating Go boilerplate.

Click the :material-plus-circle: annotations in the code snippet for a detailed
explanation of the individual components.

{{ c_example('getting_started_counter', title='counter.c') }}

1. When putting C files alongside Go files, they need to be excluded by a Go
   build tag, otherwise `go build` will complain with `C source files not
   allowed when not using cgo or SWIG`. The Go toolchain can safely ignore our
   eBPF C files.

2. Include headers containing the C macros used in the example. Identifiers such
   as `__u64` and `BPF_MAP_TYPE_ARRAY` are shipped by the Linux kernel, with
   `__uint`, `__type`, `SEC` and BPF helper definitions being provided by
   libbpf.

3. Declare a BPF map called `pkt_count`, an Array-type Map holding a single
   u64 value. See `man bpf` or the online [bpf man
   pages](https://man7.org/linux/man-pages/man2/bpf.2.html) for an overview of
   all available map types.<br/><br/>
   For this example, we went with an array since it's a well-known data
   structure you're likely familiar with. In BPF, arrays are preallocated and
   zeroed, making them safe and ready to use without any initialization.

4. The Map definition is placed in the `.maps` ELF section, which is where {{
   proj }} expects to find it.

5. In BPF, not all programs are equal. Some act on raw packets, some execute
   within the context of kernel or user space functions, while others expect to
   be run against an `__sk_buff`. These differences are encoded in the Program
   Type. libbpf introduced a set of conventions around which ELF sections
   correspond to which type. In this example, we've chosen `xdp` since we'll
   attach this program to the XDP hook later.

6. There's only one possible element in `pkt_count` since we've specified a
   `max_entries` value of 1. We'll always access the 0th element of the array.

7. Here, we're asking the BPF runtime for a pointer to the 0th element of the
   `pkt_count` Map. <br/><br/>
   `bpf_map_lookup_elem` is a BPF helper declared in `docs.h`. Helpers are small
   pieces of logic provided by the kernel that enable a BPF program to interact
   with its context or other parts of the kernel. Discover all BPF helpers
    supported by your kernel using `man bpf-helpers` or the online [bpf-helpers
    man pages](https://man7.org/linux/man-pages/man7/bpf-helpers.7.html).

8. All Map lookups can fail. If there's no element for the requested `key` in
   the Map, `count` will hold a null pointer. The BPF verifier is very strict
   about checking access to potential null pointers, so any further access
   to `count` needs to be gated by a null check.

9. Atomically increase the value pointed to by `count` by 1. It's important to
   note that on systems with SMP enabled (most systems nowadays), the same BPF
   program can be executed concurrently.<br/><br/>
   Even though we're loading only one 'copy' of our Program, accompanied by a
   single `pkt_count` Map, the kernel may need to process incoming packets on
   multiple receive queues in parallel, leading to multiple instances of the
   program being executed, and `pkt_count` effectively becoming a piece of
   shared memory. Use atomics to avoid dirty reads/writes.

10. XDP allows for dropping packets early, way before it's passed to the
   kernel's networking stack where routing, firewalling (ip/nftables) and things
   like TCP and sockets are implemented. We issue the `XDP_PASS` verdict to
   avoid ever interfering with the kernel's network stack.

11. Since some BPF helpers allow calling kernel code licensed under GPLv2, BPF
   programs using specific helpers need to declare they're (at least partially)
   licensed under GPL. Dual-licensing is possible, which we've opted for here
   with `Dual MIT/GPL`, since {{ proj }} is MIT-licensed.

Create an empty directory and save this file as `counter.c`. In the next step,
we'll set up the necessary bits to compile our eBPF C program using `bpf2go`.

## Compile eBPF C and generate scaffolding using bpf2go

With the `counter.c` source file in place, create another file called `gen.go`
containing a `//go:generate` statement. This invokes `bpf2go` when running `go
generate` in the project directory.

Aside from compiling our eBPF C program, bpf2go will also generate some
scaffolding code we'll use to load our eBPF program into the kernel and interact
with its various components. This greatly reduces the amount of code we need to
write to get up and running.

{{ go_example('getting_started_gen', title='gen.go') }}

!!! tip ""
    Using a dedicated file for your package's `//go:generate` statement(s) is
    neat for keeping them separated from application logic. At this point in the
    guide, we don't have a `main.go` file yet. Feel free to include it in
    existing Go source files if you prefer.

Before using the Go toolchain, Go wants us to declare a Go module. This command
should take care of that:

```{ .shell-session data-copy="go mod init ebpf-test && go mod tidy" }
% go mod init ebpf-test
go: creating new go.mod: module ebpf-test
go: to add module requirements and sums:
    go mod tidy
% go mod tidy
```

We also need to manually add a dependency on `bpf2go` since it's not explicitly
imported by a `.go` source file:

```{ .shell-session data-copy="go get github.com/cilium/ebpf/cmd/bpf2go" }
% go get github.com/cilium/ebpf/cmd/bpf2go
go: added github.com/cilium/ebpf v0.11.0
go: added golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
go: added golang.org/x/sys v0.6.0
```

Now we're ready to run `go generate`:

```{ .shell-session data-copy="go generate" }
% go generate
Compiled /home/timo/getting_started/counter_bpfel.o
Stripped /home/timo/getting_started/counter_bpfel.o
Wrote /home/timo/getting_started/counter_bpfel.go
Compiled /home/timo/getting_started/counter_bpfeb.o
Stripped /home/timo/getting_started/counter_bpfeb.o
Wrote /home/timo/getting_started/counter_bpfeb.go
```

`bpf2go` built `counter.c` into `counter_bpf*.o` behind the scenes using
`clang`. It generated two object files and two corresponding Go source files
based on the contents of the object files. Do not remove any of these, we'll
need them later.

Let's inspect one of the generated .go files:

{{ go_example('counterPrograms', title='counter_bpfel.go', signature=True) }}

Neat! Looks like bpf2go automatically generated a scaffolding for interacting
with our `count_packets` Program from Go. In the next step, we'll explore how to
load our program into the kernel and put it to work by attaching it to an XDP
hook!

## The Go application

Finally, with our eBPF C code compiled and Go scaffolding generated, all that's
left is writing the Go code responsible for loading and attaching the program to
a hook in the Linux kernel.

Click the :material-plus-circle: annotations in the code snippet for some of the
more intricate details. Note that we won't cover anything related to the Go
standard library here.

{{ go_example('getting_started_main', title='main.go') }}

1. Linux kernels before 5.11 use RLIMIT_MEMLOCK to control the maximum amount of
   memory allocated for a process' eBPF resources. By default, it's set to a
   relatively low value. See [Resource Limits](../concepts/rlimit.md) for a deep
   dive.

1. `counterObjects` is a struct containing nil pointers to Map and Program
   objects. A subsequent call to `loadCounterObjects` populates these fields
   based on the struct tags declared on them. This mechanism saves a lot of
   repetition that would occur by checking a Collection for Map and Program
   objects by string.<br/><br/>
   As an added bonus, `counterObjects` adds type safety by turning these into
   compile-time lookups. If a Map or Program doesn't appear in the ELF, it won't
   appear as a struct field and your Go application won't compile, eliminating
   a whole class of runtime errors.

1. Close all file descriptors held by `objs` right before the Go application
   terminates. See [Object Lifecycle](../concepts/object-lifecycle.md) for a
   deep dive.

1. Associate the `count_packets` (stylized in the Go scaffolding as
   `CountPackets`) eBPF program with `eth0`. This returns a {{
   godoc('link/Link') }} abstraction.

1. Close the file descriptor of the Program-to-interface association. Note that
   this will stop the Program from executing on incoming packets if the Link was
   not {{ godoc('link/Link.Pin') }}ed to the bpf file system.

1. Load a uint64 stored at index 0 from the `pkt_count` Map (stylized in the Go
   scaffolding as `PktCount`). This corresponds to the logic in `counter.c`.

Save this file as `main.go` in the same directory alongside `counter.c` and
`gen.go`.

## Building and running the Go application

Now `main.go` is in place, we can finally compile and run our Go application!

```{ .shell-session data-copy="go build && sudo ./ebpf-test" }
% go build && sudo ./ebpf-test
2023/09/20 17:18:43 Counting incoming packets on eth0..
2023/09/20 17:18:47 Received 0 packets
2023/09/20 17:18:48 Received 4 packets
2023/09/20 17:18:49 Received 11 packets
2023/09/20 17:18:50 Received 15 packets
```

Generate some traffic on eth0 and you should see the counter increase.

### Iteration Workflow

When iterating on the C code, make sure to keep generated files up-to-date.
Without re-running bpf2go, the eBPF C won't be recompiled, and any changes made
to the C program structure won't be reflected in the Go scaffolding.

```{ .shell-session data-copy="go generate && go build && sudo ./ebpf-test" }
% go generate && go build && sudo ./ebpf-test
```

## What's Next?

Congratulations, you've just built your (presumably) first eBPF-powered Go app!
Hopefully, this guide piqued your interest and gave you a better sense of what
eBPF can do and how it works.

With XDP, we've only barely scratched the surface of eBPF's many use cases and
applications. For more easily-accessible examples, see [the main repository's
examples/ folder](https://github.com/cilium/ebpf/tree/main/examples). It
demonstrates use cases like tracing user space applications, extracting
information from the kernel, attaching eBPF programs to network sockets and
more.

Follow our other guides to continue on your journey of shipping a portable
eBPF-powered application to your users.