Skip to main content

Crate netlink_bindings

Crate netlink_bindings 

Source
Expand description

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

§Overview

Netlink is a structured way for various kernel subsystems to expose their userspace API using hierarchical format with binary-encoded messages exchanged over a socket. Many kernel subsystems already have a machine-readable API descriptions, which we use to generate Rust bindings.

This project provides easy-to-use type-safe interface, while being reasonably fast and supporting all properties of all sensible Netlink families.

§Features

  • Simple type-safe interface, see below Making requests and Examples.
  • Support for all documented netlink subsystems, see Support status.
  • Netlink messages can be Debug-printed, with enum variants and flags annotated.
  • Examine netlink messages of existing programs using reverse-lookup.

§Support status

All upstream specifications are supported as of Linux 7.2.

  • ✅ supported, has tests: conntrack, inet-diag, nftables, nl80211, nlctrl, rt-addr, rt-route, rt-link, tc, wireguard.
  • ✔️ compiles, testing needed: binder, dev-energymodel, devlink, dpll, drm-ras, ethtool, fou, handshake, lockd, mptcp_pm, netdev, net-shaper, nfsd, ovpn, ovs_datapath, ovs_flow, ovs_packet, ovs_vport, psp, rt-neigh, rt-rule, sunrpc, tcp_metrics, team, unix-diag.

§Installation

[dependencies]
netlink-bindings = { version = "0.3", features = [ "wireguard" ] }
netlink-socket2 = { version = "0.3", features = [ ] }

§Making requests

A typical Netlink family, say wireguard, supports multiple operations: “get-device”, “set-device”, etc. Each operation may be of kind “do” or “dump”.

As an example, to gather info about a device you would use a “dump” kind (returning multiple replies) of “get-device” request. That’s usually what it means, although different subsystems may imply different things. A typical request looks like this:

use netlink_bindings::wireguard;
use netlink_socket2::NetlinkSocket;

let mut sock = NetlinkSocket::new();

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

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

let mut iter = sock.request(&request).unwrap();
while let Some(attrs) = iter.recv().transpose().unwrap() {
    // An attribute may be missing or failing to parse
    let listen_port = attrs.get_listen_port().unwrap();
    println!("Interface 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 you want to assign it an ip address. This is domain of “rt-addr” family. It was one of the first subsystems created, inheriting some now-discouraged quirks like a fixed-header - a struct that’s always present in a message. It’s use depends on the request type, with unused fields usually zeroed-out.

The relevant operation is “newaddr” of kind “do” (only returning an acknowledgment).

use std::net::IpAddr;
use netlink_bindings::rt_addr;
use netlink_socket2::NetlinkSocket;

let mut sock = NetlinkSocket::new();

let addr: IpAddr = "10.0.0.1".parse().unwrap();
let ifindex = unsafe { libc::if_nametoindex(c"wg0".as_ptr()) };

// Create fixed-header for the request
let header = rt_addr::Ifaddrmsg {
    ifa_index: ifindex,
    ifa_family: libc::AF_INET as u8, // aka ipv4
    ifa_prefixlen: 32, // stands for "/32" in "10.0.0.1/32"
    ..Default::default()
};

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

request.encode()
    .push_local(addr);

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

You may also notice “.set_change()” setting a flag. Similar to the fixed-header, these flags may trigger additional behavior in certain operations, or do nothing in others.

See full code in the example.

§Async sockets

Generally, Netlink requests resolve immediately, which is to say it’s safe to use the “blocking” NetlinkSocket in the async context, which is recommended.

The only exceptions are a multicast socket, receiving notification asynchronously, or a hypothetical subsystem, choosing to deliberately delay replies.

Netlink-socket crate allows the facilities for different runtimes to coexist under different paths, i.e. netlink_socket2::tokio::{NetlinkSocket, MulticastSocketRaw}, with async functionality available with the exactly same structure as the “blocking” one.

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

§Other examples

See netlink-socket/examples:

  • wireguard-setup - Create and configure wireguard interface.
  • ip-route-show - Dump routing entries.
  • conntrack - Dump tracked network connections, similar to conntrack -L.
  • extack - Showcase handing of extended ack attributes in error reporting.
  • nftables - Create nftables rules.
  • nftables-api - A high-level wrapper for creating nftables rules.
  • nl80211 - Basic interactions with nl80211.
  • nl80211-raw - Same as nl80211, but manually encoding/decoding netlink messages.
  • tc-prio - Add, show, and delete traffic control queueing discipline.
  • tcp-rtt - Dump socket information, including RTT of a TCP socket.
  • multicast-simple - Listen for multicast notifications emitted when a network device is created, changed, or deleted.
  • multicast-generic - Listen for all multicast notifications on a generic netlink subsystem.
  • multicast-raw - Listen for multicast notifications on legacy rtnetlink subsystem.

§Advanced usage

§Working off of existing tools

If there’s an existing tool using Netlink, you can use reverse-lookup tool to decipher it’s Netlink communications and work off of that.

Let’s say you want to see what wg command does:

$ cargo install --git https://github.com/one-d-wide/netlink-bindings reverse-lookup --features all-subsystems
$ ~/.cargo/bin/reverse-lookup -- wg
Decoding request in family ROUTE flags=[REQUEST,ACK,DUMP,REPLACE,EXCL] Raw { protonum: 0, request_type: 0 }
...
Decoding reply in genl family wireguard flags=[MULTI] Generic("wireguard")
Wgdevice {
    ListenPort: 0,
    Fwmark: 0,
    Ifindex: 10,
    Ifname: "wg0",
}
...

This way you can study the structure of the real messages the kernel expects and replies with. In order to translate it into code, you simply need to convert attributes from CamelCase to snake_case, adding occasional .get_*(), .push_*(), or .nested_*() prefix.

This tool is merely interpreting the output of strace(1), see reverse-lookup --help for more details.

§Attribute encoding

Under the hood, calling .encode() is just a convenience to switch to a correct Push* wrapper for encoding, which is given an internal buffer. For example, directly encoding a “do” request of “set-device” operation looks like this:

use netlink_bindings::wireguard as wg;

let mut vec = Vec::new();

// Do set-device (request)
wg::OpSetDeviceDo::encode_request(&mut vec)
    .push_ifname(c"wg0") // &CStr
    // .push_ifname_bytes("wg0".as_bytes()) // &[u8]
    .push_flags(wg::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, along with the in-line documentation.

§Attribute decoding

Similarly, under the hood, receiving a reply yields an attribute decoder. The decoder itself is just a wrapper on a slice, therefore it can be cheaply cloned, copying it’s frame. The low-level interface is based on iterators, with nicer helper functions on top.

use netlink_bindings::traits::NetlinkRequest;
use netlink_bindings::wireguard::OpGetDeviceDump;

// Dump get-device (reply)
let attrs = OpGetDeviceDump::decode_reply(&[/* ... */]);

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. And as previously, check out all available methods, along with the in-line documentation.

§Cloning attrset contents

In some cases, it’s infeasible to construct the message in one go, for example when you want to be able to freely alternate between encoding two different attribute sets.

The kernel only expects a single nested attribute set of a certain kind to appear at the same level of nesting, so you have to first encode them in separate temporary buffers before cloning them into the final message. This trick applies to reusing parts of an already encoded message received from the kernel.

Writing raw attributes is possible using .as_vec_mut() method method of Pusher trait, implemented for all encoding Push* structs, giving access to the internal &mut Vec<u8> buffer. And a corresponding .get_buf() method available on all decoding structs returning their &[u8] frame.

use netlink_bindings::{traits::Pusher, rt_link};

let mut link_attrs = Vec::new();
let mut bridge_attrs = Vec::new();

rt_link::PushLinkinfoBridgeAttrs::new(&mut bridge_attrs)
    .push_priority(1000);

rt_link::PushLinkAttrs::new(&mut link_attrs)
    .push_link(42);

// ... 

let mut req = rt_link::Request::new()
    .op_getlink_do(&Default::default());

req.encode()
    .as_vec_mut()
    .extend_from_slice(&link_attrs);

req.encode()
    .nested_linkinfo()
        .nested_data_bridge()
        .as_vec_mut()
        .extend_from_slice(&bridge_attrs);

§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.

use netlink_bindings::traits::NetlinkRequest;
use netlink_bindings::wireguard::{OpGetDeviceDump, Wgdevice, Wgpeer};

for attr in OpGetDeviceDump::decode_reply(&[/* ... */]) {
    match attr.unwrap() {
        Wgdevice::Ifname(n) => println!("Ifname: {n:?}"),
        Wgdevice::Peers(iter) => {
            for entry in iter {
                for attr in entry.unwrap() {
                    match attr.unwrap() {
                        Wgpeer::Endpoint(e) => println!("Endpoint: {e:?}"),
                        _ => {}
                    }
                }
            }
        }
        _ => {}
    }
}

§Alternatives

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).

§Contribute

See CONTRIBUTING.md for information on how to work with this repo, its structure, codegen stuff, etc.

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

  • Just use netlink-bindings. If you encounter a shortcoming of the current API, report it.
  • Write straightforward higher-level abstractions on top of netlink-bindings.
  • Add testing: collect netlink messages and check that they are parsed correctly. See wireguard tests as an example. Additional 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).

Modules§

binderbinder
Binder interface over generic netlink
builtin
Auxilary types not porovided by any particular family
conntrackconntrack
Netfilter connection tracking subsystem over nfnetlink
consts
dev_energymodeldev-energymodel
Energy model netlink interface to notify its changes.
devlinkdevlink
Partial family for Devlink.
dplldpll
DPLL subsystem.
drm_rasdrm-ras
DRM RAS (Reliability, Availability, Serviceability) over Generic Netlink. Provides a standardized mechanism for DRM drivers to register "nodes" representing hardware/software components capable of reporting error counters. Userspace tools can query the list of nodes or individual error counters via the Generic Netlink interface.
ethtoolethtool
Partial family for Ethtool Netlink.
foufou
Foo-over-UDP.
handshakehandshake
Netlink protocol to request a transport layer security handshake.
inet_diaginet-diag
Internet socket diagnostics
lockdlockd
lockd configuration over generic netlink
mptcp_pmmptcp_pm
Multipath TCP.
net_shapernet-shaper
Networking HW rate limiting configuration.
netdevnetdev
netdev configuration over generic netlink.
nfsdnfsd
NFSD configuration over generic netlink.
nftablesnftables
Netfilter nftables configuration over netlink.
nl80211nl80211
Netlink API for 802.11 wireless devices
nlctrlnlctrl
genetlink meta-family that exposes information about all genetlink families registered in the kernel (including itself).
ovpnovpn
Netlink protocol to control OpenVPN network devices
ovs_datapathovs_datapath
OVS datapath configuration over generic netlink.
ovs_flowovs_flow
OVS flow configuration over generic netlink.
ovs_packetovs_packet
OVS packet execution over generic netlink.
ovs_vportovs_vport
OVS vport configuration over generic netlink.
psppsp
PSP Security Protocol Generic Netlink family.
rt_addrrt-addr
Address configuration over rtnetlink.
rt_linkrt-link
Link configuration over rtnetlink.
rt_neighrt-neigh
IP neighbour management over rtnetlink.
rt_routert-route
Route configuration over rtnetlink.
rt_rulert-rule
FIB rule management over rtnetlink.
sunrpcsunrpc
SUNRPC cache upcall support over generic netlink.
tctc
Netlink raw family for tc qdisc, chain, class and filter configuration over rtnetlink.
tcp_metricstcp_metrics
Management interface for TCP metrics.
teamteam
Network team device driver.
traits
unix_diagunix-diag
UNIX socket diagnostics
utils
wireguardwireguard
Netlink protocol to control WireGuard network devices.