Linux, Netlink, and Go — Part 3: packages netlink, genetlink, and wifi

In Part 1 and Part 2 of this series, I described some of the fundamental concepts of netlink and generic netlink. It is assumed that readers are already familiar with netlink and generic netlink from the previous posts in this series.

In this post, I will dive into high level concepts and usage of my Go packages:

  • netlink: provides low-level access to Linux netlink sockets.
  • genetlink: generic netlink interactions and data types.
  • wifi: provides access to IEEE 802.11 WiFi device actions and statistics.

The series is split into parts as follows:

  • Part 1: netlink: an introduction to netlink.
  • Part 2: generic netlink: an introduction to generic netlink, a netlink family meant to simplify creation of new families.
  • Part 3 (this post): packages netlink, genetlink, and wifi: using Go to drive interactions with netlink, generic netlink, and nl80211.

Package netlink

When I first decided to look into retrieving WiFi device information on Linux, I was pointed to netlink by a colleague as a possible solution. I was familiar with several existing netlink packages for Go, but I was unable to find one that I felt suited my needs.

Several of the existing, popular packages made decisions I wasn’t happy with:

  • lack of documentation
  • lack of tests
  • bloated API
  • a “C-like” Go API

In addition, many of these packages conflated the concepts of “netlink”, “route netlink”, and the iproute2 family of utilities. For these reasons, I decided to start building my own package.

It is important to note that package netlink is meant to be used as a building block for other netlink family packages. Typically, you will use a higher level package like genetlink instead. To enable maximum code re-use, create dedicated packages for any high level netlink families, instead of building them on top of package netlink in your application.

This section will discuss some common use-cases of package netlink, but you may wish to reference the source code and documentation for further information.

netlink.Conn: a connection to netlink

The netlink.Conn type is used to create connections to netlink. A netlink family is specified in the call to netlink.Dial along with additional configuration, if needed.

It is important to note that when a netlink.Conn is no longer needed, netlink.Conn.Close must be called to close the socket and avoid leaking file descriptors.

Once a connection is established, it can be used to send a request, receive a response, and validate the response against the request. Many of the request header fields can be omitted to allow package netlink to calculate and assign those values automatically.

Error checking omitted for brevity. Please check all errors in your code.

// Dial generic netlink.
const genetlink = 16
conn, _ := netlink.Dial(genetlink, nil)
defer conn.Close()
// Ask netlink to echo back an acknowledgement to our request.
flags := netlink.HeaderFlagsRequest | netlink.HeaderFlagsAcknowledge
m := netlink.Message{
Header: netlink.Header{
Flags: flags,
// Other fields assigned automatically by package netlink.
},
}
// Perform a send, receive, and validate cycle.
req, _ := conn.Send(m)
replies, _:= conn.Receive()
err := netlink.Validate(req, replies)

For convenience, the Conn.Execute method can be used as a shortcut for Conn.Send, Conn.Receive, and Validate.

msgs, _ := conn.Execute(m)

To listen to multicast groups, use the Conn.JoinGroup and Conn.LeaveGroup methods. Messages can be received using Conn.Receive as usual.

// Listen to rtnetlink for modification of network interfaces
const rtnetlink = 0
const rtmGroupLink = 0x1
conn, _ := netlink.Dial(rtnetlink, nil)
defer conn.Close()
// Join multicast group: Receive will block until messages arrive.
_ = conn.JoinGroup(rtmGroupLink)
msgs, _ := conn.Receive()
_ = conn.LeaveGroup(rtmGroupLink)

Finally, when dealing with a high throughput netlink application, one may wish to make use of BPF filters to explicitly accept or reject packets based on their contents.

BPF filters can be attached by calling Conn.SetBPF with a filter assembled using x/net/bpf. See the BPF integration tests for an example of using BPF filters with netlink.

Package genetlink

Package genetlink is the reference example of a netlink family package built using package netlink. It exposes a very similar API, but handles some common generic netlink operations on behalf of its user.

This package enables sending and receiving netlink messages but also offers a Conn.Family helper using the “service” pattern popularized by go-github.

If you’d like to dive in, you my reference the source code or documentation as you read this section.

genetlink.Conn: a connection to generic netlink

A genetlink.Conn is essentially a specialized wrapper around the netlink.Conn type for interacting with generic. It transparently adds and removes netlink.Message types where needed, and enables the caller to only use genetlink.Message in most cases.

As with a netlink.Conn, when a genetlink.Conn is no longer needed, genetlink.Conn.Close must be called to close the socket and avoid leaking file descriptors.

The methods Conn.Send, Conn.Receive, and Conn.Execute in package genetlink are used to work with generic netlink messages. In fact, when using Conn.Execute, there is no need to deal with any netlink.Message types at all.

conn, _ := genetlink.Dial(nil)
defer conn.Close()
const (
ctrlVersion = 1
ctrlCommandGetFamily = 3
)
// Ask nlctrl to list all known families.
req := genetlink.Message{
Header: genetlink.Header{
Command: ctrlCommandGetFamily,
Version: ctrlVersion,
},
}
flags := netlink.HeaderFlagsRequest | netlink.HeaderFlagsDump
msgs, _ := conn.Execute(req, genetlink.Controller, flags)

Finally, the methods Conn.JoinGroup, Conn.LeaveGroup, and Conn.SetBPF are all available with genetlink.Conn. They perform the same actions as they do with a netlink.Conn.

genetlink.Family: generic netlink families

Because querying family information from the generic netlink controller is so common, genetlink.Conn provides specialized methods and types for doing so.

The genetlink.Family type provides information about a given generic netlink family, including its ID, version, name, and multicast groups.

The Conn.Family helper is used to provide access to these methods:

conn, _ := genetlink.Dial(nil)
defer conn.Close()
// Ask if nl80211 is available on this system. If it is not,
// an error compatible with os.IsNotExist is returned.
if _, err := conn.Family.Get("nl80211"); os.IsNotExist(err) {
fmt.Println("nl80211 not available")
return
}

Two methods are available:

  • Conn.Family.Get: look up a single family by its name.
  • Conn.Family.List: look up all families on the system.

As discussed in Part 2 of this series, communicating with a generic netlink family involves specifying its family ID and version in a request:

conn, _ := genetlink.Dial(nil)
defer conn.Close()
f, _ := conn.Family.Get("nl80211")
// Ask nl80211 for a list of all WiFi interfaces.
req := genetlink.Message{
Header: genetlink.Header{
Command: nl80211.CmdGetInterface,
// Specify the version of nl80211 we are speaking.
Version: uint8(f.Version)
},
}
// Specify the ID of nl80211 in call to Execute.
flags := netlink.HeaderFlagsRequest | netlink.HeaderFlagsDump
msgs, _ := conn.Execute(req, f.ID, flags)

Package wifi

Package wifi provides access to IEEE 802.11 WiFi device actions and statistics. At this time, it only works with Linux, though I’d love to incorporate support for more operating systems in the future.

You may wish to reference the source code or documentation for this package as you read this section.

On Linux, package wifi works using nl80211: a generic netlink family that provides C kernel header definitions for all of its commands and netlink attributes. To easily create a Go package from these constants, I made use of the excellent cgogen tool by Maxim Kupriianov. Maxim was even kind enough to provide the initial generated code for working with nl80211 from Go.

wifi.Client: accessing WiFi devices from Go

Because of the foundation provided by packages netlink and genetlink, usage of package wifi on Linux is rather straightforward. Keep in mind that Client.Close must be called once it is no longer needed, to clean up the underlying netlink socket.

This example retrieves a list of all WiFi-enabled network interfaces, and fetches the SSID associated with each device:

client, _ := wifi.New()
defer client.Close()
ifis, _ := client.Interfaces()
for _, ifi := range ifis {
// For more information about what a "BSS" is, see:
// https://en.wikipedia.org/wiki/Service_set_(802.11_network).
bss, _ := client.BSS(ifi)
fmt.Printf("%s: %q\n", ifi.Name, bss.SSID)
}

That’s really all there is to it! Netlink, generic netlink, and nl80211 do the heavy lifting of requesting and retrieving data from the kernel, decoding it, and packaging it up nicely for the user.

Summary

That wraps up my series on Linux, Netlink, and Go! I hope you’ve enjoyed the series, and found it informative. If you’d like to get started working with netlink, I’d encourage you to check out the source code of following projects:

  • github.com/mdlayher/wifi: uses netlink and generic netlink to speak to nl80211 on Linux. client_linux.go in particular should be quite helpful if you’d like to work with other generic netlink families in Go.
  • github.com/jsimonetti/rtnetlink: uses netlink and route netlink on Linux to manipulate network interfaces.

If you’d like to begin work on a new netlink family package, I’d love to hear from you! I am told that someone is working on a netfilter netlink package at the moment, but there is no source code available yet.

I happily welcome contributions to all of the packages discussed in this series. Please file an issue if you’d like to contribute!

What’s next?

For the netlink family of packages, I’d like to build out a nice testing package akin to net/http/httptest, to avoid the need to “mock” netlink connections using an interface. This would greatly simplify testing netlink family packages, and provide similar benefits for consumer packages such as wifi.

In addition to my netlink work, I’m currently working on a few different projects that I’d like to write about in the future:

Feel free to reach out via the comments, Twitter, or Gophers Slack (username: mdlayher), if you have any comments or questions.

Thank you very much for your time. It’s been a pleasure authoring this series, and I’ve received some great feedback from a wide variety of readers. I’ll keep writing if you keep reading! Until next time!