netlink-socket2 0.2.4

Type-safe Rust bindings for Netlink generated from YAML specifications
Documentation
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
## Netlink-bindings

Type-safe Rust bindings for encoding/decoding Netlink messages generated from
YAML [specifications].

[specifications]: https://docs.kernel.org/userspace-api/netlink/specs.html

## Overview

Netlink is a collection of APIs exposed by the kernel, unified by a similar
style of encoding data. The general list of Netlink families can be found in
[the kernel
documentation](https://docs.kernel.org/networking/netlink_spec/index.html), or
at least those families, that were possible to condense to a machine readable
description. This list very likely includes all the families that you would
want to interact with.

The goal of this project is to provide an easy-to-use type-safe interface,
while also being reasonably fast and supporting all properties of all
_sensible_ Netlink families.

## Features

- Streamlined type-safe interface.
- All the properties described in yaml specifications are available in Rust.
- Decoded Netlink messages can be Debug-printed, with enum values and flags
annotated.
- There is [a tool]#working-off-of-existing-tools to simplify working off of
existing tools by decoding Netlink messages of other programs.
- Unified interface for both generic and classic flavors of netlink. Family
resolution is automatic.
- Many quirks of netlink-legacy and netlink-raw protocols are supported,
including: binary structures, nested types, multi-attribute arrays, indexed
arrays, sub messages.
- Zero unsafe code in encoder/decoder logic.

## Installation

```toml
[dependencies]
netlink-bindings = { version = "0.2", features = [ "wireguard" ] }
netlink-socket2 = { version = "0.2", features = [ ] }
```

## Making requests

A typical Netlink family, say wireguard, support multiple operations, for
example, "get-device" and "set-device". Each operation may also be of "do" or
"dump" kind.

For example, to get info about a device you would use a "dump" kind of
"get-device" request. That's usually what it means, although different
subsystems may imply different things. A typical request looks like this:

```rust
use netlink_bindings::wireguard;
use netlink_socket2::NetlinkSocket;

let mut sock = NetlinkSocket::new();

let ifname = "wg0";

// All available requests are conveniently accessible using `family::Request`
let mut request = wireguard::Request::new()
    .op_get_device_dump_request();

// Add contents to the request
request.encode()
    .push_ifname_bytes(ifname.as_bytes());

let mut iter = sock.request(&request).unwrap();
while let Some(res) = iter.recv() {
    // Each request may return an error (literal error code), in some cases
    // with some additional info from the kernel, e.g. lacking a permission,
    // if you missing CAP_NET_ADMIN capability for querying wireguard info.
    let attrs = res.unwrap();

    // A simple approach to get a specific property from an attribute set is
    // following. Note that it's not guaranteed that the property was supplied,
    // nor that it can be parsed correctly. If either occurs, the error will
    // include error context, i.e. name of the attribute and it's parent set.
    let listen_port = attrs.get_listen_port().unwrap();
    println!("Interface {ifname:?} is listening on {listen_port}");

    // Print out all the attributes using the debug formatter.
    println!("{:#?}", attrs);
}
```

Your LSP should be able to nicely suggest appropriate methods both for encoding
and decoding as you type.

## More complicated requests

Let's say you have a network interface and want to assign an ip address to it.
This is the domain of "rt-addr" family. It was one of the first ones created,
inheriting some now-discouraged quirks, like a fixed-header - a struct that
always present and may also carry some relevant data in some cases or may be
just ignored (zeroed-out) for other requests.

The relevant operation is "newaddr" with "do" kind. You may also notice
".set_change()". This specifies an additional request flags. Similar to
fixed-header, theses flags may invoke some additional behavior in certain
operations, or do nothing in others.

```rust,should_panic
use std::net::IpAddr;
use netlink_bindings::rt_addr;
use netlink_socket2::NetlinkSocket;

let mut sock = NetlinkSocket::new();

let ifindex: u32 = 1234; // Acquired via "get-addr" request
let addr: IpAddr = "10.0.0.1".parse().unwrap();
let prefix: u8 = 32; // stands for "/32" in "10.0.0.1/32"

// Create fixed-header for the request
let mut header = rt_addr::PushIfaddrmsg::new();
header.set_ifa_index(ifindex);
header.set_ifa_family(libc::AF_INET as u8); // aka ipv4
header.set_ifa_prefixlen(prefix);

let mut request = rt_addr::Request::new()
    .set_change() // Don't fail if address already assigned
    .op_newaddr_do_request(&header);

request.encode()
    .push_local(addr);

sock.request(&request).unwrap()
    .recv_ack().unwrap();
```

See full code in the [example](./netlink-socket/examples/wireguard-setup.rs).

## Async sockets

Async functionality is available using the same interface, you just need to
enable it, and to add `.await` keyword in all places where async IO is expected.

```toml
[dependencies]
netlink-socket2 = { ... , features = [ "tokio" ] } # or "smol"
```

An earlier example, but using async, would look like this:

```rust,compile_fail
use netlink_bindings::wireguard;
use netlink_socket2::NetlinkSocket;

let mut sock = NetlinkSocket::new();

let mut request = wireguard::Request::new()
    .op_get_device_dump_request();

request.encode()
    .push_ifname_bytes(b"wg0");

let mut iter = sock.request(&request).await.unwrap();
while let Some(res) = iter.recv().await {
    println!("{:#?}", res);
}
```

## Other examples

- [wireguard-setup]./netlink-socket/examples/wireguard-setup.rs - Create and
configure wireguard interface.
- [conntrack]./netlink-socket/examples/conntrack.rs - Dump tracked network
connections, similar to `conntrack -L`.
- [extack]./netlink-socket/examples/extack.rs - Showcase handing of extended
ack attributes in error reporting.
- [nftables]./netlink-socket/examples/nftables.rs - Create nftables rules.
- [nftables-api]./netlink-socket/examples/nftables-api.rs - A high-level
wrapper for creating nftables rules.
- [nl80211]./netlink-socket/examples/nl80211.rs - Basic interactions with nl80211.
- [nl80211-raw]./netlink-socket/examples/nl80211-raw.rs - Same as nl80211,
but manually encoding/decoding netlink messages.
- [tc-prio]./netlink-socket/examples/tc-prio.rs - Add, show, and delete
traffic control queueing discipline.
- [tcp-rtt]./netlink-socket/examples/tcp-rtt.rs - Dump socket information,
including RTT of a TCP socket.

## Attribute encoding

Under the hood, calling `.encode()` is just a convenience to pass the lead to
the correct encoding struct. The ecoder's job is to actually write the
attributes directly into provided buffer. All types relevant for encoding are
prefixed with `Push`. For example, directly encoding a "do" request of
"set-device" operation looks like the following:

```rust
use netlink_bindings::wireguard::{PushOpSetDeviceDoRequest, WgdeviceFlags};

let mut vec = Vec::new();

// Do set-device (request)
PushOpSetDeviceDoRequest::new(&mut vec)
    .push_ifname(c"wg0") // &CStr
    // .push_ifname_bytes("wg0".as_bytes()) // &[u8]
    .push_flags(WgdeviceFlags::ReplacePeers as u32) // Remove existing peers
    .array_peers()
        .entry_nested()
        .push_public_key(&[/* ... */]) // &[u8]
        .push_endpoint("127.0.0.1:12345".parse().unwrap()) // SocketAddr
        .array_allowedips()
            .entry_nested()
            .push_family(libc::AF_INET as u16) // aka ipv4
            .push_ipaddr("0.0.0.0".parse().unwrap()) // IpAddr
            .push_cidr_mask(0) // stands for "/0" in "0.0.0.0/0"
            .end_nested()
            // More allowed ips...
        .end_array() // Explicitly closing isn't necessary
        .end_nested()
        // More peers...
    .end_array();
```

Additionally, check out [all available
methods](./netlink-bindings/src/wireguard/wireguard.md), along with the in-line
documentation.

## Attribute decoding

Similarly, under the hood, receiving a reply yield an attribute decoder. The
decoder itself is just a slice, therefore it can be cheaply cloned, copying
it's frame. The low-level interface is based on iterators, with nice-to-use
wrapper on top.

```rust,should_panic
use netlink_bindings::wireguard::OpGetDeviceDumpReply;

let buf = vec![/* ... */];

// Dump get-device (reply)
let attrs = OpGetDeviceDumpReply::new(&buf);

println!("Ifname: {:?}", attrs.get_ifname().unwrap()); // &CStr
for peer in attrs.get_peers().unwrap() {
    println!("Endpoint: {}", peer.get_endpoint().unwrap()); // SockAddr

    for addr in peer.get_allowedips().unwrap() {
        let ip = addr.get_ipaddr().unwrap(); // IpAddr
        let mask = addr.get_cidr_mask().unwrap(); // u8
        println!("Allowed ip: {ip}/{mask}");
    }
}
```

See full code in the [example](./netlink-bindings/examples/wireguard.rs). And
as previously, check out [all available
methods](./netlink-bindings/src/wireguard/wireguard.md), along with the in-line
documentation.

## Low-level decoding

A low-level decoding interface is exposed as an iterator, that yields enum
variants, containing either a target type, e.g. SockAddr, or another iterator,
in case of a nested attribute set. But as you can see, using it directly
quickly turns very ugly.

```rust
use netlink_bindings::wireguard::{OpGetDeviceDumpReply, Wgpeer};

let buf = vec![/* ... */];

for attr in OpGetDeviceDumpReply::new(&buf) {
    match attr.unwrap() {
        OpGetDeviceDumpReply::Ifname(n) => println!("Ifname: {n:?}"),
        OpGetDeviceDumpReply::Peers(iter) => {
            for entry in iter {
                for attr in entry.unwrap() {
                    match attr.unwrap() {
                        Wgpeer::Endpoint(e) => println!("Endpoint: {e:?}"),
                        _ => {}
                    }
                }
            }
        }
        _ => {}
    }
}
```

## Alternatives

- [Rust-netlink]https://github.com/rust-netlink.
- [Neli]https://github.com/jbaublitz/neli.

Both don't use codegen to generate bindings, hence many Netlink families are
not supported.

Another difference is that they represent netlink messages as lists of rust
enums, while this project works with the binary representation directly, with a
separate interfaces for encoding and decoding: a builder pattern-like interface
for encoding, and an iterator interface for decoding (internally).

## Support status

- ✅ - supported, has tests.
- ✔️ - compiles, testing needed.
- ? - not attempted.
- ❌ - doesn't compile (needs adaptations in codegen).

| subsystem | ? | comment |
| --- | --- | --- |
| [nlctrl]./netlink-bindings/src/nlctrl/nlctrl.md || |
| [conntrack]./netlink-socket/examples/conntrack.rs || |
| [inet-diag]./netlink-socket/examples/tcp-rtt.rs || |
| [nftables]./netlink-socket/examples/nftables.rs || |
| [nl80211]./netlink-socket/examples/nl80211.rs || |
| [rt-addr]./netlink-socket/examples/wireguard-setup.rs || |
| [rt-link]./netlink-socket/examples/wireguard-setup.rs || |
| [tc]./netlink-socket/examples/tc-prio.rs || |
| [wireguard]./netlink-socket/examples/wireguard-setup.rs || |
| devlink | ✔️ | |
| netdev | ✔️ | |
| rt-neigh | ✔️ | |
| rt-route | ✔️ | |
| rt-rule | ✔️ | |
| unix-diag | ✔️ | |
| ethtool | ? | |
| dpll | ? | |
| fou | ? | |
| handshake | ? | |
| lockd | ? | |
| mptcp_pm | ? | |
| net-shaper | ? | |
| nfsd | ? | |
| ovpn | ? | |
| ovs_datapath | ? | |
| ovs_flow | ? | |
| ovs_vport | ? | |
| tcp_metrics | ? | |
| team | ? | |

The following netlink features are not implemented (yet):

- Attributes denoting a C array (i.e. attributes with type binary and sub-type u32/u64).
- Events/notifications/multicast messages.
- Sub messages with a selector attribute outside of the parent attribute set.

## Working off of existing tools

If there's an existing tool using Netlink, you can use `reverse-lookup` binary
from this project to decipher it's Netlink communications, and work off of
that. Let's say you want to inspect what `wg` command does.

```sh
$ strace -o ./output_file --decode-fd=socket -e %network --{write,read}=$(seq -s, 0 100) -- wg
$ cargo run --bin reverse-lookup --features=wireguard,nlctrl,rt-link -- ./output_file
Decoding request in family ROUTE flags=[REQUEST,ACK,DUMP,REPLACE,EXCL] Raw { protonum: 0, request_type: 0 }
...
```

## Generate bindings

Update protocol bindings using the yaml description from the protocol
directory.

```sh
$ cargo run --bin codegen -- -d ./netlink-bindings/src/wireguard/
Writing "netlink-bindings/src/wireguard/mod.rs"
Dumping "netlink-bindings/src/wireguard/wireguard.md"
Dumping all "netlink-bindings/src/wireguard/wireguard-all.md"
```

Generate protocol bindings for a new family, copying yaml specification from
somewhere else.

```sh
$ cargo run --bin codegen -- -d ./netlink-bindings/src/ linux/Documentation/netlink/specs/wireguard.yaml
Writing "netlink-bindings/src/wireguard/mod.rs"
Dumping "netlink-bindings/src/wireguard/wireguard.md"
Dumping all "netlink-bindings/src/wireguard/wireguard-all.md"
```

## Codegen specific options

Yaml attributes specific to our codegen that may be helpful when dealing with
incomplete netlink specifications:

- `operations.fallback-attrs: <attrset>` - create a placeholder request type
with an operation type provided at runtime. Also, the provided attribute set is
used as a fallback in reverse lookup if operation type wasn't recognized.
- `operations.transparent: true` or `operations[].transparent: true` - make
request types use common encoding/decoding types, instead of generating new
ones that are narrowed down. Reduces generated code size.
- `operations[].request_type_at_runtime: true` - allow operation type to be
provided at runtime.
- `operations.all-attrs: true` or `operations[].all-attrs: true` - don't
narrow down attributes in generated request types.
- `operations[].no_ack: true` - operation doesn't support ack on success. This
option only affects chained requests.
- `definitions[].shrinkable: true` - C struct is padded with zeros or
truncated when needed, e.g. the struct was expanded between the kernel
versions. The default behavior is to return a decoding error.
- `operations[].{do,dump}.{request,reply}.attribute-set: <attrset>` -
attribute-set can be specific to request/response, needed for certain
netlink-raw families.
- `operations[].rust-filter` - Rust closure to differentiate between
netlink-raw operations when `value` isn't enough. Only used in reverse-lookup.
The closure is of type `fn(&[u8]) -> bool`, it checks message payload, which
usually starts with subsystem-specific header struct.
- `operations[].rust-filter-{request,reply}` - Same, but applied only for
requests and replies respectively.
- `definitions[].attributes[].display-hint: <type>[]` - display as a C-like
array, useful when it may encode data beyond a single type.
- `definitions[].attributes[].display-hint: string` - display bytes a raw
string.
- `definitions[].members.type: cbitfield` - a new type to support C bitfields.
Needs `sub-type: {u8,u16,u32}` and `bits: <n>`.

Experimental options:

- `experimental.struct-type: {buf,cstruct}` - how to represent a struct: an
opaque wrapper on \[u8; n\] buffer or a repr(C) struct.
- `experimental.struct-prefix: false` - disable "Push" prefix for structs.
- `experimental.struct-explicit-padding: true` - always add padding fields,
even if it would otherwise be silently inserted due to alignment.
- `experimental.attr-binary-write: true` - generate `.write_*() -> impl Write`
methods for attributes of binary type.

Feature flags:

- `--features=netlink-bindings/deny-unknown-attrs` - treat unknown attributes
as errors.

Additional attributes can specified in .override.yaml file located alongside
the main specification file.

## To-do

- Testing (for each sensible netlink family and for parsing primitives).
- Simplify codegen logic.
- Clean up unintentional panics in encoding/decoding.
- Optimize generated code size, e.g. leave out code encoding kernel replies by
default.
- Improve user interface (better error reporting, tooling, etc).
- Benchmarks, fuzzing.

## Contribute

If your want to contribute, you can, for example:

- Add testing: collect netlink messages and check that they are parsed
correctly. See wireguard [tests]./netlink-bindings/src/wireguard/tests.rs as
an example. Additional [examples]./netlink-socket/examples are also very
welcome.
- Implement yet unsupported netlink functionality.
- Improve compilation time, reduce the size of generated bindings.
- Experiment with a better Rust interface (for encoding/decoding and the sockets).
- Sponsor the project (contact the author for options).