|
RF24Gateway - TCP/IP over RF24Network v2.2.0
TMRh20 - Pushing the practical limits of RF24 modules
|
RF24Gateway bridges a wireless RF24Mesh network onto a Linux TUN/TAP interface (tun_nrf24 by default). Anything that can transmit on the same channel with compatible radio settings can try to join the mesh and send traffic into the Linux host, and from there onto your LAN or the internet. By default nothing on the Linux side limits what that traffic can do.
This document describes a layered approach to locking the gateway down:
These steps are independent; apply as many as suit your deployment. The commands below assume the default interface name tun_nrf24 and a Debian-based OS such as Raspberry Pi OS. Run them on the device running RF24Gateway.
Most RF24Ethernet sensor nodes only need to publish and subscribe to an MQTT broker, and answer pings for diagnostics. The rules below drop all other traffic entering or leaving the mesh through tun_nrf24. Traffic on other interfaces (eth0, wlan0, loopback) is not affected.
The rules are kept in a dedicated chain, RF24-FW, so they are easy to inspect and remove:
What these rules do:
The default port 1883 is plain-text MQTT. If your broker uses MQTT over TLS, allow port 8883 instead of (or as well as) 1883. If your nodes need other services (DNS on UDP port 53, NTP on UDP port 123, HTTP to a node's web server, etc.), add an ACCEPT rule for each one above the final DROP rule, for example:
The packet counters on each rule show what is being accepted and dropped. From a mesh node, pinging the gateway and connecting to the MQTT broker should work, while connections to other ports (for example SSH on port 22 at the gateway's mesh IP) should time out.
iptables rules are lost on reboot. To save them:
Rules are saved to /etc/iptables/rules.v4 (and rules.v6 for the IPv6 rules below) and restored at boot. Because the rules match the interface by name, they take effect as soon as RF24Gateway creates tun_nrf24, even though the interface does not exist when the rules are loaded.
To remove the firewall:
RF24Gateway and RF24Ethernet only use IPv4, but Linux may still enable IPv6 on the tun_nrf24 interface, and the IPv4 firewall above does not apply to IPv6 traffic at all. Block it in two ways.
First, drop all IPv6 traffic on the interface with ip6tables:
Second, disable IPv6 on the interface itself so it never gets an IPv6 address:
Since RF24Gateway creates tun_nrf24 after boot, a per-interface setting in a config file is not applied at boot. To have it applied automatically, disable IPv6 by default for newly created interfaces (see Making the sysctl Settings Permanent):
Check the result with:
which should print nothing.
Reverse path filtering makes the kernel check the source address of every incoming packet: if the reply to that packet would not be sent back out the same interface it arrived on, the packet is dropped. This stops a mesh node from sending packets that pretend to come from your LAN (or the internet), and stops LAN hosts from pretending to be mesh nodes.
Enable strict mode (1) for all interfaces:
The kernel uses the higher of the all value and the per-interface value, so setting all to 1 applies strict mode to tun_nrf24 even though it is created later.
On the master gateway, RF24Gateway also checks traffic arriving from the radio side: an IPv4 packet is only accepted if the sender currently holds an address assigned by the master, and its source IP ends in the sender's own nodeID (or, for addresses outside the mesh subnet, the sender is listed as the gateway for that subnet in routing.txt). rp_filter adds the same kind of protection at the Linux level, for every interface.
Strict mode requires the kernel's routing table to know about every network that should arrive on tun_nrf24. In the simple configuration (everything in one subnet such as 10.1.3.0/24) this is automatic.
In the Advanced Configuration described on the main page, where child RF24Gateway nodes route extra subnets (for example 10.1.5.0/24 behind 10.1.3.33):
On the master, the kernel needs a route to each of those subnets via tun_nrf24. If the interface netmask already covers them (for example 10.1.3.1/16 covers 10.1.5.x), nothing else is needed. Otherwise add a route, for example:
If a correct route is not practical, use loose mode (2) instead. Loose mode only drops packets whose source address is not reachable through any interface, which still blocks obviously bogus source addresses:
To see packets that are being dropped by rp_filter, temporarily enable logging and watch the kernel log:
sysctl -w changes are lost on reboot. To keep them, create /etc/sysctl.d/90-rf24gateway.conf containing:
then load it with:
Confirm the values with sysctl net.ipv4.conf.all.rp_filter net.ipv6.conf.tun_nrf24.disable_ipv6.
The firewall controls what traffic can cross the gateway. The gateway examples also include a commented-out block that controls which nodes are allowed to hold an address on the mesh. It can be found in:
The master node (nodeID 0) acts as the DHCP server for the mesh. Every time a node joins, it requests an address and the master records the pair in mesh.addrList, an array of {nodeID, address} entries. mesh.addrListTop is the number of entries in the list.
The block above runs through the whole list on every pass of the main loop. Any entry whose nodeID is not 3, 4 or 5 has its address set to 0, which is the same thing mesh.releaseAddress() does: it marks the entry as no longer holding an address. Once released:
The result is that only nodes 3, 4 and 5 can exchange TCP/IP traffic through the gateway.
For more than a few nodes, a list is easier to maintain than a chain of != comparisons:
The comment says the check "can be run on a timer" to save CPU time. Keep the interval short: an unauthorized node holds a valid address from the moment the master assigns it until the next time the check runs. Running it on every loop, right after the gateway processes DHCP requests, gives the smallest window.
The nodeID is chosen by the node itself and sent in its address request; nothing proves a node is who it says it is. Anyone who knows (or guesses) an authorized nodeID can configure a device with that ID and be treated as authorized. The allowlist is useful for keeping stray or misconfigured nodes off the network, and limits which IP addresses can be used, but it is not authentication. Use it together with the firewall and rp_filter settings above, and enable encryption so that only nodes holding the network key can join at all.
By default, everything sent over the radio is plain text: anyone with a compatible radio on the same channel can read the traffic, and can inject frames of their own. RF24Gateway can encrypt and authenticate every radio frame with AES-128 in CCM mode, using the same over-the-air format as the encryption built into the nRF52/nRF54 radio peripheral (and the nrf_to_nrf library).
Each frame carries a random IV, a packet counter, and a 4-byte message integrity code (MIC). A receiver drops any frame that was not encrypted with the shared key or was modified in transit. A node that does not have the key cannot read traffic, join the mesh, or get an address.
| Device | Radio driver | Encryption support |
|---|---|---|
| Linux (Raspberry Pi, etc.) with an nRF24L01(+) module | RF24, via the RF24CCM wrapper | Yes: software AES. This is how RF24Gateway itself is encrypted |
| nRF52840, nRF52833, nRF52832 | nrf_to_nrf (v1.5.1 or newer) | Yes: uses the chip's hardware CCM peripheral |
| nRF54L15 | nrf_to_nrf, with the nrf54l15clean core | Yes: uses the core's Ccm and CracenRng classes |
| Arduino (AVR, ESP8266, ESP32, etc.) with an nRF24L01(+) module | RF24 | No |
The key points:
Encryption support is a compile-time option, RF24NETWORK_CCM, which must be enabled for RF24Network, RF24Mesh and RF24Gateway.
Building manually with CMake: add -DRF24NETWORK_CCM=ON when configuring each of the three libraries, then build and install them in order:
This installs RF24Network/RF24CCM.h and builds the library templates for the RF24CCM radio type. Libraries built without the option still work as before; they just cannot be used with RF24CCM.
The gateway examples (for example examples/RF24GatewayNode.cpp) include the required code, commented out. Replace the usual radio, network, mesh and gateway declarations with their RF24CCM versions, and set a key before calling gw.begin():
RF24CCM is a drop-in replacement for RF24, so the rest of the program does not change. Encryption must be enabled before gw.begin() because begin() sets the radio's retry settings, and because mesh address requests are encrypted too.
Generate a key with:
and never use the example key. Anyone who has the key can read and inject traffic, so keep it out of public source repositories.
On nrf_to_nrf nodes, set the same key and enable encryption before mesh.begin():
If a node cannot connect (mesh.begin() keeps failing), check that the key, channel and data rate match the gateway exactly. A key mismatch looks the same as being out of range.
Every encrypted frame carries 12 bytes of overhead (5-byte IV, 3-byte packet counter, 4-byte MIC). With 32-byte frames and the 8-byte RF24Network header, each frame holds 12 bytes of data instead of 24, so:
Frames that are too short or fail authentication are dropped and counted in radio.decryptFailures. A steadily rising count means there is a node with the wrong key (or no key) on the channel, or someone is sending invalid frames.