linux

Yocto Hardening: Kernel and GCC Configuration

Find all of the Yocto hardening texts from here!

Would you like to make your Yocto image a tiny bit harder to hack ‘n’ crack? Of course you would. This time we’re going to be doing two things to improve its security: hardening the Linux kernel, and setting the hardening flags for GCC. The motivation for these is quite obvious. Kernel is the privileged core of the system, so it better be as hardened as possible. GCC compilation flags on the other hand affect almost every C and C++ binary and library that gets compiled into the system. As you may know, over the years we’ve gotten quite a few of them, so it’s a good idea to use any help the compiler can provide with hardening them.

On the other hand, who wouldn’t like to live a bit dangerously?

Kernel Configuration Hardening

Linux kernel is the heart of the operating system and environment. As one can guess, configuring the kernel incorrectly or enabling everything in the kernel “just in case” will in the best situation lead to suboptimal performance and/or size, and in the worst case, it’ll provide unnecessary attack surfaces. However, optimizing the configuration manually for size, speed, or safety is a massive undertaking. According to Linux from Scratch, there are almost 12,000 configuration switches in the kernel, so going through all of them isn’t really an option.

Fortunately, there are automatic kernel configuration checkers that can help guide the way. Alexander Popov’s Kernel Hardening Checker is one such tool, focusing on the safety of the kernel. It combines a few different security recommendations into one checker. The project’s README contains the list of recommendations it uses as the guideline for a safe configuration. The README also contains plenty of other useful information, like how to run the checker. Who would have guessed! For the sake of example, let’s go through the usage here as well.

Obtaining and Analyzing Kernel Hardening Information

The kernel-hardening-checker doesn’t actually only check the kernel configuration that defines the build time hardening, but it also checks the command line and sysctl parameters for boot-time and runtime hardening as well. Here’s how you can obtain the info for each of the checks:

  • Kernel configuration: in Yocto, you can usually find this from ${STAGING_KERNEL_BUILDDIR}/.config, e.g. <build>/tmp/work-shared/<machine>/kernel-build-artifacts/.config
  • Command line parameters: run cat /proc/cmdline on the system to print the command line parameters
  • Sysctl parameters: run sysctl -a on the system to print the sysctl information

Once you’ve collected all the information you want to check, you can install and run the tool in a Python virtual environment like this:

python3 -m venv venv
source venv/bin/activate
pip install git+https://github.com/a13xp0p0v/kernel-hardening-checker
kernel-hardening-checker -c <path-to-config> -l <path-to-cmdline> -s <path-to-sysctl>

Note that you don’t have to perform all the checks if you don’t want to. The command will print out the report, most likely recommending plenty of fixes. Green text is better than red text. Note that not all of the recommendations necessarily apply to your system. However, at least disabling the unused features is usually a good idea because it reduces the attack surface and (possibly) optimizes the kernel.

To generate the config fragment that contains the recommended configuration, you can use the -g flag without the input files. As the README states, the configuration flags may have performance and/or size impacts on the kernel. This is listed as recommended reading about the performance impact.

GCC Hardening

Whether you like it or not, GCC is the default compiler in Yocto builds. Well, there exists meta-clang for building with clang, and as far as I know, the support is already in quite good shape, but that’s beside the point. Yocto has had hardening flags for GCC compilation for quite some time. To check these flags, you can run the following command:

bitbake <image-name> -e | grep ^SECURITY_CFLAGS=

How the security flags get applied to the actual build flags may vary between Yocto versions. In Kirkstone, SECURITY_CFLAGS gets added to TARGET_CC_ARCH variable, which gets set to HOST_CC_ARCH, which finally gets added to CC command. HOST_CC_ARCH gets also added to CXX and CPP commands, so SECURITY_CFLAGS apply also to C++ programs. bitbake -e is your friend when trying to figure out what gets set and where.

I don’t think any other meme can capture this feeling of madness

So, in addition to checking the SECURITY_CFLAGS, you most likely want to check the CC variable as well to see that the flags actually get added to the command that gets run:

# Note that the CC variable gets exported so grep is slightly different
bitbake <image-name> -e |grep "^export CC="

The flags are defined in security_flags.inc file in Poky (link goes to Kirkstone version of the file). It also shows how to make package-specific exceptions with pn-<package-name> override. The PIE (position-independent executables) flags are perhaps worth mentioning as they’re a bit special. The compiler is built to create position-independent executables by default (seen in GCCPIE variable), so PIE flags are empty and not part of the SECURITY_CFLAGS. Only if PIE flags are not wanted, they are explicitly disabled.

Extra Flags for GCC

Are the flags defined in security_flags.inc any good? Yes, they are, but they can also be expanded a bit. GCC will most likely get in early 2024 new -fhardened flag that sets some options not present in Yocto’s security flags:

-D_FORTIFY_SOURCE=3 (or =2 for older glibcs) 
-D_GLIBCXX_ASSERTIONS 
-ftrivial-auto-var-init=pattern 
-fPIE -pie -Wl,-z,relro,-z,now
-fstack-protector-strong
-fstack-clash-protection
-fcf-protection=full (x86 GNU/Linux only)

Lines 2, 3, and 6 are not present in the Yocto flags. Those could be added using a SECURITY_CFLAGS:append in a suitable place if so desired. I had some trouble with the trivial-auto-var-init flag though, seems like it is introduced in GCC version 12.1 while Yocto Kirkstone is still using 11 series. Most of the aforementioned flags are explained quite well in this Red Hat article. Considering there’s plenty of overlap with the SECURITY_CFLAGS and -fhardened, it may be that in future versions of Poky the security flags will contain just -fhardened (assuming that the flag actually gets implemented).

All in all, assuming you have a fairly modern version of Yocto, this GCC hardening chapter consists mostly of just checking that you have the SECURITY_CFLAGS present in CC variable and adding a few flags. Note once again that using the hardening flags has its own performance hit, so if you are writing something really time- or resource-critical you need to find a suitable balance with the hardening and optimization levels.

In Closing

While this was quite a short text, and the GCC hardening chapter mostly consisted of two grep commands and silly memes, the Linux kernel hardening work is something that actually takes a long time to complete and verify. At least my simple check for core-image-minimal with systemd enabled resulted in 136 recommendation fails and 110 passes. Fixing it would most likely take quite a bit longer than writing this text. Perhaps not all of the issues need to be fixed but deciding what’s an actual issue and what isn’t takes its own time as well. So good luck with that, and until next time!

As a reminder, if you liked this text and/or found it useful, you can find the whole Yocto hardening series from here.


The Movember blog series continues with this text! As usual, after reading this text I ask you to do something good. Good is a bit subjective, so you most likely know what’s something good that you can do. I’m going to eat a hamburger, change ventilation filters, and donate a bit to charity.

Yocto Hardening: Firewalls, Part 2: firewalld

Find all of the Yocto hardening texts from here!

People often ask me two things. The first question is “Why did you choose to write this firewall text in two parts?”. The answer to that is I actually started writing this over a year ago, the scope of the text swelled like crazy, and in the end, to get something published I chose to write the thing in two parts to have a better focus. The second question that I get asked is “Do you often have imaginary discussions with imaginary people in your head?”. To that, the answer is yes.

Comic about imaginary conversations
I prepare for daily stand-up by mentally going through conversations where I get deservedly fired because I didn’t close three tickets the previous day.

But without further ado, let’s continue where we left off in part 1. As promised, let’s take a look at another way of setting up and configuring a firewall: firewalld.

One Step Forward, Two Steps Back: Configuring Kernel (Again)

To get the firewalld running we need a few more kernel configuration items enabled. How many? Well, I spent a few days trying to figure out the minimal possible configuration to run some simple firewalld commands, all without success. firewalld is not too helpful with its error messages, as it basically says “something is missing” without really specifying what that special something is.

Meme about Linux kernel modules
I’ve always liked Lionel’s ‘stash, but I feel like he’s looking extra fine in this pic.

After banging my head on a wall for a few days I attempted to enable every single Netfilter and nftables configuration item (because firewalld uses nftables), but no success. After some further searching, I found this blog post that contained a kernel configuration that had worked for the author of the blog. I combined that with my desperate efforts, and the resulting config actually worked! In the end, my full configuration fragment looked like this:

As you can see, my shotgun approach to configuration wasn’t 100% accurate because there were few NF and NFT configuration items I missed. Is everything here absolutely necessary? Most likely not. There actually are some warnings about unused config options for the Linux kernel version 5.15, but I’m afraid to touch the configuration at this point.

I spent some more time optimizing this and trying to make the “minimal config”, just to realize that I’m actually minimizing something that can’t be really minimized. The “minimal config” really depends on what your actual firewall configuration contains. After this realization, I decided that it’s better to have too much than too little, and found an inner peace. You can think that the fragment provides a starting point from which you can start to remove useless-sounding options if you feel like it.

While we’re still talking about the build side of things, remember to add the kernel-modules to your IMAGE_INSTALL as instructed in part 1. Or if you chose to go through the RRECOMMENDS route, remember to add all 36 modules to the dependencies.

Meme about saving disk space by removing kernel modules
To be fair, I’ve been in a situation where a few KBs of root file system content have made the difference between a success and a catastrophic failure.

The Hard Firewall – firewalld

Now that I’ve spent three chapters basically apologizing for my Linux config, it’s time to get to the actual firewall stuff. The “hard” way of setting up a firewall consists of setting up firewalld. This is in my opinion suitable if you have multiple interfaces with changing configurations, possibly edited by a human user, meaning that things change (and usually in a more complex direction).

firewalld is a firewall management tool designed for Linux distributions, providing a dynamic interface for network security (another sentence from ChatGPT, thank you AI overlords). Or to put it into more understandable words, it’s a front-end for nftables providing a D-Bus interface for configuring the firewall. The nice thing about firewalld is that it operates in zones & services, allowing different network interfaces to operate in different network zones with different available services, all of which can be edited run-time using D-Bus, creating mind-boggling, evolving, and Lovecraftian configurations. This is the nice thing, so you can imagine what the not-so-nice things are.

Now that we are somewhat aware of what we’re getting into, we can add firewalld to IMAGE_INSTALL:

IMAGE_INSTALL:append = " firewalld"

One thing worth noting is that firewalld is not part of the Poky repository, but it comes as a part of meta-openembedded. Most likely this is not a problem, because every project I’ve worked on has meta-openembedded, but it’s worth knowing nevertheless.

Now that everything is in place we can start hammering the firewalld to the desired shape. To edit the configuration we can use the firewall-cmd shipped with firewalld package. This firewall-cmd (among some other firewall-* tools) uses the D-Bus interface to command the firewalld, which in turn commands nftables, which finally sets the Netfilter in the kernel. This picture illustrates it all nicely:

Graph showing firewalld internal structure
The picture originates from here:
https://firewalld.org/2018/07/nftables-backend

So, about the actual configuration process: each network interface can be assigned a zone, and the zones can enable a certain set of services. Here’s a short command reference for how to add a custom zone, add a custom service, add the new service to the new zone, and set the new zone to a network interface:

# List available zones
firewall-cmd --get-zones
# These zones are also listed in /usr/lib/firewalld/zones/
# and /etc/firewalld/zones/

# Get the default zone with the following command
# (This is usually public zone)
firewall-cmd --get-default-zone

# This'll return "no zone" if nothing is set,
# meaning that the default zone will be used
firewall-cmd --get-zone-of-interface=eth0

# Create a custom zone blocking everything
firewall-cmd --permanent --new-zone=test_zone
firewall-cmd --permanent --zone=test_zone --set-target=DROP
# If permanent option is not used, changes will be lost on reload.
# This applies to pretty much all of the commands

# Reload the firewalld to make the new zone visible
firewall-cmd --reload

#List services, and add a custom service
firewall-cmd --list-services
firewall-cmd --permanent --new-service=my-custom-service
firewall-cmd --permanent --service=my-custom-service --add-port=22/tcp
firewall-cmd --reload

# Add the service to the zone
firewall-cmd --permanent --zone=test_zone --add-service=my-custom-service

# Set zone
firewall-cmd --zone=home --change-interface=eth0
# Add --permanent flag to make the change, well, permanent

You can (and should) check between the commands how the ports appear to the outside world using nmap to get a bit better grasp on how the different commands actually work. The changes are not always as immediate and intuitive as one would hope. Good rule of thumb is that if the settings survive --reload, they’re set up properly.

How does this all work with the file-based configuration and Yocto? You could create the zone and service files for your system using the firewall-cmd, install them during build time, and change the default zone in the configuration file /etc/firewalld/firewalld.conf to have the rules active by default. This is similar to nftables approach, but is a bit easier to customize for example from a graphical user interface or scripts, and doesn’t require custom start-up scripts. An example of adding some zones and services, and a patch setting the default zone, can be found in the meta-firewall-examples repo created for this blog text.

Comparison and Closing Words

So, now we have two options for firewalling. Which one should be used? Well, I’d pick nftables, use it as long as possible, and move to firewalld if it would be required later on. This is suitable when the target is quite simple and lightweight, at least at the beginning of the lifespan of a device. On the other hand, if it’s already known from the get-go that the device is going to be a full-fledged configurable router, it’d be better to pick the firewalld right off the bat.

Meme about differences of nftables and firewalld

What about iptables and ufw then? Both have bitbake recipes and can definitely be installed, and I suppose they even work. iptables is a bit of a legacy software these days, so maybe avoid that one though. It should be possible to use iptables syntax with nftables backend if that’s what you want. ufw in Yocto uses iptables by default, but it should be possible to use nftables as the backend for ufw. So as usual with the open-source projects, everything is possible with a bit of effort. And as you can guess from the amount of weasel words in this chapter, I don’t really know these two that well, I just took a quick glance at their recipes.

Note that the strength of the firewall depends also on how protected the rulesets are. If the rules are in a plaintext file that everyone can write to, it’s safe to assume that everyone will do so at some point. So give a thought to who has the access and ability to edit the rules. Also, it may be worthwhile to consider some tampering detection for the rule files, but something like that is worth a text of its own.

In closing, I hope this helped you to set up your firewall. There are plenty of ways to set it up, this two-parter provides two options. It’s important to check with external tooling that the firewall actually works as it should and there are no unwanted surprises. Depending on the type of device you want to configure you can choose the simple way or the configurable way. I’d recommend sticking to the easy solutions if they’re possible. And finally, these are just my suggestions. I don’t claim to know anything, except that I know nothing, so you should check your system’s overall security with someone who’s an actual pro.

Yocto Hardening: Firewalls, Part 1: nftables

Find all of the Yocto hardening texts from here!

The eternal task of making the Yocto Linux build an impenetrable fortress continues. Next, we’ll look into setting up a firewall two different ways: the easy way with nftables, and in the part two the easily configurable way with firewalld. I must warn you beforehand that this text contains a bit of resentment towards kernel configuration. I also used ChatGPT as a tool for writing a part of this text, but I’ll inform you of the section I asked it to write.

So yeah, firewall. The system keeping the barbarians out of our Linux empire. Also, one of the most bad-ass names for a program, at least if taken literally. Reality is a bit less exciting, turns out “firewall” is some technical construction term. In the magical world of computers, a firewall is a piece of software used to allow or deny network traffic coming in and out of a machine. It’s mighty useful for improving the overall security of the system to be able to prevent unwanted remote connections and such.

Step 0 – Configuring Kernel

The first step of installing and configuring a firewall on a Yocto image is ensuring that the kernel is configured correctly. What, do you think you can just start configuring the firewall and blocking your nefarious enemies just like that? Pfft, get out with that casual attitude (please don’t).

To get some basic traffic filtering with nftables done, we need to ensure that the Netfilter is enabled in the kernel, as well as the Netfilter support for nftables, and that some nftables modules are installed as well. It’s all a bit confusing, but to summarize: Netfilter is the kernel framework for packet filtering and nftables is the front-end for Netfilter that can be used from user space. nftables can then be built with or without modules to extend its functionality. The following kernel configuration fragment should do the trick:

Getting this config fragment working was where things got problematic for me. I thought that I’d just slap =y to everything and be done with it, no need to install and load modules. Well, the configuration process of the kernel consists of merging the defconfig, kernel features, and configuration fragments, and as it turns out, the kernel feature enabling the Netfilter overrode some parts of my config fragment, changing =y to =m. For some reason bitbake didn’t give a warning about this. I’m quite sure it used to do.

But where is this mythical Netfilter kernel feature located? The fragment above enables only nftables related things, how to get the Netfilter working? Well, Kirkstone release (3.1.27) of Poky has this line that enables the feature by default in the kernel, and the related configuration fragment looks like this. The multiple configs lead to funny situations where you try to remember the difference between CONFIG_NF_CONNTRACK and CONFIG_NFT_CT and where they are defined and to what value.

Ensure that you’re also installing the kernel modules to the image by adding the following line somewhere in the image configuration:

IMAGE_INSTALL:append = " kernel-modules"

No use enabling the modules if they’re not installed, and by default nothing installs them, so they won’t get added to the final image. At least to the qemu64 core-image-minimal image they won’t be installed by default, guess if I found that out the hard way as well. If you need more fine-grained control over the modules that get installed, you can add them as a runtime recommendation to some package:

RRECOMMENDS:${PN} += "kernel-module-nf-tables kernel-module-nf-conntrack kernel-module-nft-ct ...

Most likely nftables package is the best place for this. However, I’d advise against this approach if possible, because there will be a lot of modules later on. If you choose to go down this route, the naming format of the modules is fortunately quite self-explanatory. CONFIG_NF_TABLES being built as a module generates kernel-module-nf-tables package, CONFIG_NFT_CT results in kernel-module-nft-ct being built etc, so this should be easily scriptable (although painful to maintain).

The Easy Firewall – nftables

The easy way of getting some firewalling done involves installing nftables, adding some rules to it, and loading them as a part of the start-up. This is suitable if you have a few interfaces with a simple configuration that doesn’t change often, preferably ever.

The actual first step of getting the firewall up and running is adding the nftables package to the image if you don’t have it already installed. This can be easily checked by running nft command. If it fails, add the package to the image configuration:

IMAGE_INSTALL:append = " nftables"

If you see the following types of errors when running the nft command it means that your kernel configuration is missing either nftables support (the first error) or some nftables module (the second error):

root@qemux86-64:~# nft
../../nftables-1.0.2/src/mnl.c:60: Unable to initialize Netlink socket: Protocol not supported
root@qemux86-64:~# 
root@qemux86-64:~# nft -f /tmp/test.conf
/tmp/test2:32:9-16: Error: Could not process rule: No such file or directory
        ct state vmap { established : accept, related : accept, invalid : drop } 
        ^^^^^^^^
root@qemux86-64:~# 

Quite often the firewall in Yocto is empty by default. The active rules of the firewall can be checked by running nft list ruleset command to print the current ruleset. If nothing is output, no rules are present. If something gets output, there are some rules present. Here’s a short reference of commands on how to set some basic rules and list them on the command line.

# List ruleset
nft list ruleset

# Add a table named filter for IP traffic 
nft add table inet filter

# Add an input chain that drops traffic by default
nft add chain inet filter input \{ type filter hook input priority 0 \; policy drop \}

# Add rule to allow traffic to port 22
nft add rule inet filter input tcp dport \{ 22 \} accept

# List tables and rules in a table
nft list tables
nft list table inet firewall

In addition to listing the tables and rulesets from inside the device, you should also try a black-box approach. Running nmap command against your device is a good way to check how the outside world sees your device. In this example, I have added the Dropbear SSH server to the image. Because the server is listening on port 22 and there are no rules defined in the firewall, the port is seen as open:

esa@ubuntu:~$ nmap 192.168.7.2
Starting Nmap 7.80 ( https://nmap.org ) at 2023-07-22 16:14 UTC
Nmap scan report for 192.168.7.2
Host is up (0.0024s latency).
Not shown: 999 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
Nmap done: 1 IP address (1 host up) scanned in 0.06 seconds

The command line commands are useful for editing the configuration run-time, but for the rest of the text we’re going to focus on a file-based configuration that is set during the Yocto build. The configurations presented here are mostly based on “a simple ruleset for a server” in nftables wiki. It contains some useful extra things not presented here, like a rule for the loopback interface, differentiating between ipv4 and ipv6 traffic and logging, and I recommend checking it out.

Usually, a good way to start creating a firewall ruleset is to block everything by default, and then start poking holes as needed. To block everything, you can write a configuration file with the following content:

flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy drop;
    }
}

This config creates one rule table named firewall for inet traffic. The table contains three chains: inbound, forward and output. You can load the ruleset with nft -f <config_file> command. Once you’ve blocked all traffic, you can give the nmap command another go. As you can see, blocking is really effective. nmap actually considers the device to be non-existent at this point:

esa@ubuntu:~$ nmap 192.168.7.2
Starting Nmap 7.80 ( https://nmap.org ) at 2023-07-22 16:18 UTC
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 3.03 seconds

Trying to ping from the device itself to the outside world also fails because the output is blocked. In theory, allowing outgoing traffic is not the worst idea, usually many sample configurations actually allow it. But if you know the traffic that’s going to go out and the ports that will be used there’s in my opinion no sense allowing all of the unnecessary traffic. That’s just an attitude that will lead to botnets. But if you want/need to allow all outgoing traffic, you can just drop the output chain from the config. By default everything will be accepted.

After nothing goes in or out, it’s time to start allowing some traffic. For example, if you want to allow SSH traffic from the barbarians to port 22 (generally a Bad Idea, but for the sake of an example), you can use the following ruleset:

flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;

        # Allow SSH on port TCP/22 for IPv4 and IPv6.
        tcp dport { 22 } accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy drop;

        # Allow established and related traffic
        ct state vmap { established : accept, related : accept, invalid : drop } 
    }
}

If you want to define multiple allowed destination ports, separate them using commas, like { 22, 80, 443 }. If you choose not to block outgoing connections, you obviously don’t need the output chain presented here.

On the other hand, if there’s a process in the Yocto image that wants to contact the outside world, you could consider adding a ruleset like below:

flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;

        # Allow established and related traffic
        ct state vmap { established : accept, related : accept, invalid : drop } 
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy drop;

        # You can check the ephemeral port range of kernel 
        # from /proc/sys/net/ipv4/ip_local_port_range
        tcp sport 32768-60999 accept
    }
}

Note how we need to use a port range for the output. Outgoing connections usually use a port from an ephemeral range for their connection, meaning that you may not know beforehand the exact port that needs to be allowed. Having fixed ports for outgoing traffic makes the device more susceptible to port scanning and prevents multiple connections from the same service because the port is already used, but if you choose to create a service that uses a static source port, you can use a single port number in the rule.

You can combine the rules in the chains as required by your system and services. If you want to enable the ping command (for ipv4), you can add the following piece of configuration to the input chain (note that this is just a single line of configuration, not a full config):

icmp type echo-request limit rate 5/second accept

In the end, creating the firewall ruleset is fairly simple: block everything and allow only necessary things. The difficulty really comes from keeping the rules up-to-date when something changes. However, when developing an embedded device with Yocto you generally should have a well-defined list of allowed ports & traffic (as opposed to a general-purpose computer used by a living human where all sorts of traffic and configs may come and go at the user’s whim). If that’s not the case, the part 2 where we work with firewalld may be useful to you.

So, now we know how to write the perfect configuration file. But how to add this to a Yocto build? You can create a bbappend file for nftables into your own meta-layer. This append then installs the configuration file, because nftables does not install any configuration by default. An example of how to add a configuration to nftables can be found in meta-firewall-examples repository I made for this blog post.

You should also note that nftables package doesn’t contain any mechanism to activate the rules during the start-up. Therefore you should append the nftables recipe so that it adds an init.d script or a service file that activates the rules from the configuration file before the networking is started. You can find an example of this as well from the same meta-firewall-examples repository.

Note that if you do run-time edits to the configuration with nft command, the changes will be lost when the device is reset. To ensure that the changes are kept, you can run the following command to overwrite the default config with the new, enhanced configuration:

nft list ruleset > /path/to/your/configuration-file

And one more thing: if you face some No such file or directory issues when trying more exotic configurations you are most likely missing some kernel configurations. In part 2, we’ll be enabling pretty much every feature of the Netfilter, so that’ll (most likely) be helpful. On that cliffhanger, we eagerly await the next chapter in this ever-evolving journey of discovery and innovation (I asked Chat-GPT to write that sentence).

Open-source contribution: RTL8821AU driver recipe

This is a story of how I became a useful member of society by doing my first open-source contribution.

It all began one fateful afternoon, when I purchased a TP-Link Wifi dongle, thinking that it would allow me to connect my old Raspberry Pi 2 wirelessly to the internet. It was running my own Poky-based distro, but what could really go wrong with random USB devices and Linux?

Well quite a plenty really. I plugged the device in, but I couldn’t connect to the highway of data. No delicious internet cookies for me. Not even a blinking led.

To begin troubleshooting the issue, I tried checking if the network interface was seen by kernel by running both ifconfig -a and ip link show. No wlan devices were found. Some googling suggested running lsusb. That showed the device, which at least proved that it wasn’t broken and was recognized by kernel. Some sort of network driver was clearly needed.

Bus 001 Device 004: ID 2357:011f TP-Link 802.11ac WLAN Adapter 
Bus 001 Device 003: ID 0424:ec00 Microchip Technology, Inc. (formerly SMSC) SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Microchip Technology, Inc. (formerly SMSC) SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Finding the correct driver turned out to be a bit tougher than expected. First I tried googling the name of the wifi dongle suffixed with “driver”. Bad Idea. This led to a lot of ancient forum posts that suggested all kinds of Realtek drivers for (almost) similarly named devices that were installed by enabling a variety of kernel configuration options. None of the drivers worked.

After some more of the furious googling I found out that the wifi dongle I bought required an out-of-tree kernel module instead. That meant I couldn’t just enable a kernel configuration to build the driver in my distro. Finding the correct driver was another trial and error type of affair. Someone suggested a driver for the 8812au chip. It did not work but helped me to find a correct trail.

Fortunately there’s a lot less diseases on this trail.

RTL8812AU driver repo contained a file supported-device-IDs that expectedly did not contain the device id output by lsusb. However, that gave me an idea (that I really should have gotten from the beginning): googling “driver 2357:011f”. Who would have guessed that searching for a driver with an exact device id instead of vague product names would yield the correct driver(s)? This search also helped me to find the name of the Realtek chip, 8821au, which I confused plenty of times with 8812au. I’m not sure if this info would have been available on the manual of the dongle because I did not read it.

After finding the driver & chip I connected some dots and realized that there actually is a kernel configuration driver named CONFIG_RTL8XXXU that I tried. Despite what the name suggests, it does not work with rtl8821au.

Once the correct driver was figured out it was time to add it to the Yocto build. Some more googling revealed that there is a meta layer called meta-rtlwifi for these Realtek out-of-tree modules. Unfortunately, it didn’t contain the RTL8821AU driver. Fortunately, I’ve been using git at work so I could fix that myself. You can see where this is heading.

So I took the RTL8812AU driver recipe as I suspected that it should mostly work, and updated the relevant parts, i.e. the repo to fetch the driver from. I was pleasantly surprised that the build worked just like that. Even more shocking was that the module worked as well. After that, it was just a matter of a pull request to get the driver added to the meta-layer alongside the other friendly drivers.

There were actually multiple drivers available for 8821au. At least morrownr, ulli-kroll and ivanborislav provide RTL8821AU drivers. In the end, I chose morrownr driver because their driver worked satisfactorily out of the box and their driver is also used for 8812au. I first gave a shot at ivanborislav driver but it filled my TTY with logs about power save mode. Most likely a configuration mistake from my side, but usually a thing that works without extra tinkering is the better choice.

It’s almost weird that there’s a meme for literally everything.

That’s how I got quite familiar with my wifi dongle, and made my first open-source contribution in the process. I also learned something. I’m not yet 100% sure what that is. Perhaps it’s that the device id is quite important when trying to find a suitable driver. And googling can give all kinds of interesting useful information. Until next time!