An ARP Server in Rust – Linux Raw Sockets

Linux has a fiddly but useful C interface for sending and receiving raw network packets. It’s documented primarily in the packet(7) manpage and the workhorse of this interface is a struct called sockaddr_ll. It’s ostensibly a type of sockaddr but it has a different length, different fields, and different parts that are relevant in different situations.

This is not especially ergonomic in Rust. The libc bindings for recvfrom(), for example, require you to pass in a *mut sockaddr. If you’re receiving from a raw socket then this actually needs to be a pointer to a sockaddr_ll, which is included in libc but is a completely different type from Rust’s point of view. Clearly Rust’s type checker is not going to be thrilled about this.

I recently figured out how to make this work for sending and receiving raw AX.25 packets. I wanted to blog about how I got it working, but since AX.25 is a little obscure I’ve decided to build a basic ARP server using similar code. Hopefully it makes a good example. This is the complete code for the server I’m going to be describing: https://gist.github.com/thombles/16736c9c656e6dad9a08c81b30a974ac

Opening A Socket

To send and receive packets the first thing you need is a socket. If you’re using the AF_PACKET interface you can choose between SOCK_RAW and SOCK_DGRAM. The first includes the ethernet headers in the packet body; the second does not. I’m using SOCK_RAW so when I receive an ARP packet so this is the information I will end up receiving:

To open the socket I use the socket() function from libc and some of the standard constants. You can choose to filter by a particular protocol by using one of the constants from the Linux header file if_ether.h. In this case I’m using ETH_P_ARP because I only want to know about ARP packets

The ETH_P_ARP parameter must be handled carefully. Ethernet protocol identifiers are 2 bytes, so it’s a u16. However Linux requires that we provide this parameter in network byte order, which is big-endian. The parameter to socket() is then defined as a c_int, which means an i32. It’s important to do these steps in the right order. This is how it works on my Intel processor. If you happened to be using a big-endian platform it would be 0806 all the way through.

Overall flow

This is an unintelligent ARP server and it will work like this:

  • Set up a HashMap of IPv4 addresses that we will respond to, with each entry storing the corresponding MAC address.
  • Listen to all incoming ARP packets on all interfaces. If it is an ARP query for an address in our HashMap, send a reply, acting as that computer.

This won’t be useful for much more than ARP spoofing since anything that knows how to talk IP will generally take care of its own ARP. But it will do for a demo. In this code I’m just using a hard-coded HashMap that’s configured when the app launches.

The following will open the socket then attempt to receive a packet. Each time it does, if it was not an error it will hand it off to another function to read.

For each received packet we get back two blobs of data. One is the actual packet. The other is a chunk of metadata, the sockaddr_ll. In this case I am (slightly arbitrarily) using two different strategies for receiving these chunks of data.

The sockaddr_ll is declared on the stack and I’m passing in a mutable pointer, so the C function can fiddle with the contents of the on-stack memory. Later on, after the unsafe code, hopefully the zeroed fields have been replaced with something valid.

The packet content is defined as a simple buffer of u8. I could probably use my RawArpFrame struct the same as sockaddr_ll, but keeping it as an untyped buffer means I could potentially handle different kinds of protocols. I will instead turn the buffer into a RawArpFrame inside handle_packet().

Receiving an individual frame

The recvfrom() function will block until it it either successfully receives a frame or suffers an error. This function wraps recvfrom() and returns the number of valid bytes that have been written into the buffer.

This is where it gets a little nasty. I can’t create a *mut sockaddr from a sockaddr_ll; they are simply different types. I use the unsafe transmute function to convert the pointer from one type to another. That transmuted pointer can then be used as a parameter to recvfrom(). On the other hand, the data buffer’s mut ptr can simply be cast to an opaque *mut c_void as required for the libc parameter.

Parsing the incoming frame

Once an ARP frame has been received we have two pieces of information: the sockaddr_ll “metadata” provided by Linux, and an opaque blob of binary data in a buffer. The handle_packet() function converts that opaque blob into a struct with fields and decides if we want to reply to it or not.

Our sockaddr_ll was written into directly during the recvfrom() so we can simply use it. The buffer takes more work. Here, ptr::read takes a pointer and uses it to construct a real RawArpFrame based on that chunk of memory. We have to hope that the layout in memory matches the definition of the struct, which is why it’s unsafe.

Once we have that frame it’s straightforward to decide whether we should reply to it or not.

Sending an ARP response

Sending a raw packet in Linux requires filling in particular fields of the sockaddr_ll as documented in the manpage. The most tricky of these is the interface index, which identifies which network interface on the machine should be used to send the data. You access this with the SIOCGIFINDEX ioctl, as described in netdevice(7). To find out which interfaces exist you can either read /proc/net/devices (more complete if you don’t always have IP addresses) or use the SIOCGIFCONF ioctl. (Example code for this ifindex discovery.)

It is possible to sidestep a lot of this work when you only wish to reply to incoming packets on the same interface that you received them. Happily, this is the case for ARP. All the information you need is contained in the sockaddr_ll from the packet you received.

The top of the function which manipulates frame is simply shuffling fields around to turn it into a valid ARP response, including the missing MAC address.

It then creates a new sockaddr_ll which is mostly initialised to zero but takes the key fields from the received sockaddr_ll. It likely wouldn’t hurt to include more of the fields. The documentation is not crystal clear so I’m not taking my chances.

The call to sendto() is very similar to what we saw before with recvfrom(). We have to pass both a sockaddr and the data to be transmitted. The difference this time is that both of these sources of data are strongly typed structs.

Tips

The snippets above cover the main functionality. I want to mention a few other things I found along the way.

When you use the netdevice ioctls you are forced to work with an ifreq union. This is an especially tricky data structure that can easily get messy. In my own code I used a technique by Herman J. Radtke III that makes this reasonably easy to handle without using unsafe.

Normally AF_PACKET/SOCK_RAW will only give you incoming packets. Word on Stack Overflow is that using ETH_P_ALL instead of a specific protocol will include outgoing packets, but then it will be necessary to filter by protocol yourself.

Finally, capturing packets promiscuously and sending raw packets requires either root access or the capabilities CAP_NET_ADMIN and CAP_NET_RAW. If you see permission denied errors, this is probably why.

Posted in programming, rust | Leave a comment

Improving ARP performance on slow AX.25 links

Recently I’ve been setting up TCP/IP connections over VHF radio. They are Linux systems linked by 1200 baud AX.25 modems. Opinions vary as to whether the overheads of TCP/IP are worthwhile at 1200 baud—9600 is certainly better—but it’s handy and it definitely works provided you use the available bandwidth carefully. All IP communications need to be precise and tidy. Otherwise the channel gets gummed up for seconds at a time and it’s not much fun to use.

Using Debian/Raspbian one of the most obvious problems is ARP traffic. When I watch the channel with axlisten I see huge quantities of requests and replies flying past, maintaining associations between callsigns like “VK7NTK-1” and IP addresses like 44.136.224.30*.

At home I have two separate computers and radios set up to talk to each other. With nothing in the ARP cache, if I run ping -c 1 44.136.224.30 I would naively expect to see something like the following. It’s basically like Ethernet except instead of MAC addresses there are callsigns and SSID numbers.

First the pinging station finds out who owns the IP; then it sends the ping. Aah, if only.

Problem 1: Duplicate ARP requests

Using a default ARP configuration the transmissions actually turn out like this:

At 1200 baud it takes most of a second to transmit even the shorter packets. The modems also check that the channel is clear before transmitting, introducing additional delays. As a result, after VK7NTK-2 decided it needed to do an ARP query the other computer didn’t even finish receiving it until 0.8 seconds later. VK7NTK-1 transmitted its reply, taking another 0.7 seconds, so VK7NTK-2 had the address it needed about 1.5 seconds after its initial request. Unfortunately at the 1 second mark it decided that the first request had been lost and queued a second query for transmission.

When that first reply did come back at 1.5 seconds VK7NTK-2 realised it had the information it needed to do the ping so it created the ICMP packet and queued that for delivery too. At that point it had two packets to transmit: the second ARP query, now unnecessary, and the ping request. In this case the modem decided to transmit them back-to-back, which is allowed.

VK7NTK-1 received both of these and queued up a reply to each one. These were also transmitted back-to-back and the ping ultimately succeeded.

The basic problem is that 1 second is far too short a timeout for ARP requests at 1200 baud. Linux isn’t clever enough to cancel the second ARP query when it’s no longer needed so I end up with an entire extra request and reply.

But wait, there’s more! Several seconds after the ping there is suddenly an ARP query in the other direction, originating from the computer that received the ping. I’m guessing this is an optimisation in Linux to warm up the ARP cache in advance. This one was even messier:

In total VK7NTK-1 transmitted three ARP requests and VK7NTK-2 transmitted two ARP replies. Two of those transmissions doubled—that is, both modems thought the channel was clear and started transmitting simultaneously. VK7NTK-1 didn’t receive the answer it wanted because it was too busy shouting the question over the top.

This is also caused by the 1 second timeout but in a different way. If you send out a query that takes 3/4 of a second to transmit, then add a little time for processing and making sure the channel is clear, you would assume that the reply will begin at about the 1 second mark. For the sender of the query, 1 second later is about the worst possible time to make a second transmission, regardless of whether it has anything to do with the previous one. You’re just asking for a double.

Solution 1. Adjust the the ARP retransmission time

The good news is that Linux makes it easy to change the ARP retransmission time. The following command increases it from the default 1 second to 5 seconds:

I placed this in a script that runs after the ax0 interface is configured. It solves both of the above problems fine.

Problem 2: The forgetful cache

After all the nonsense they went through exchanging ARP packets it would be nice if Linux remembered the results for a while. Unfortunately it doesn’t. It’s slightly randomised but roughly 15 seconds later the ARP entry will change from “REACHABLE” to “STALE”. A subsequent ping will cause the whole schmozzle to happen again. It wouldn’t be a big deal on zippy Ethernet but it really is on 1200 baud.

You can check the state of ARP entries using this command:

Solution 2: Increase the reachable time

This is another parameter that’s easy to control. To have 10+ minutes in the REACHABLE state:

The best solution: static ARP entries

Unlike Ethernet where most people are using DHCP and changing addresses semi-regularly, AX.25 stations tend to have pretty fixed addresses. Adding a permanent ARP entry means that Linux never makes any requests at all for that station.

I use this command to add an entry for my upstream gateway VK7HDM-6:

It is still good to have the other optimisations in place for stations that come and go.


* The problem looks slightly worse than it is because axlisten (or arguably the kernel) is a little buggy. The obsolete AF_INET raw interface seems to report duplicates of some frames. I have confirmed with my own software that the newer AF_PACKET/SOCK_RAW interface works fine. See manpage packet(7).

P.S. The very nature of doubled transmissions is that the computers don’t really know what’s going on and Wireshark gives misleading results. An iPad clock and phone video are a low-tech but dependable way to observe sub-second collisions.

Posted in radio | Leave a comment

Making the CW filter work on an FT-817ND

I recently ordered a lovely 500 Hz mechanical filter to use for CW on my FT-897 and FT-817—the YF-122C. I was baffled for much of this week because I could not make it work in the FT-817. If I changed the OP FILTER setting to CW it was as though there was no filter installed at all. If I set it to SSB the filter was engaged but of course the offset was wrong. It’s not helpful at all unless I want to do CW at 1250 Hz.

The difficulty turned out to be this cheeky little menu option: NAR. This is one of the settings that you access by pressing the F button briefly then rotating the SEL knob. When a narrow CW filter is installed it must be engaged by pressing the C button, which places the little triangle to the left. This function key lets you easily toggle the filter in and out, which is very useful when you’re scouting around for QSOs.

FT-817 function menu showing NAR enabled

Yaesu does not bother to point out this setting in the part of the manual that describes installing the filter. I am writing this small post in the hope that it will save someone else some trouble.

Posted in radio | 1 Comment

Sending JSON over websockets in Rust

I like the JSON format but I feel it’s important to contain it to the job of passing a message from one system to another. Ideally the sender generates the text automatically from some strongly-typed object and the receiver does the reverse. If any required parts are missing or a field is the wrong type the entire message is rejected. Skipping this step and operating directly on the fields, as you can do in Javascript or with NSJSONSerialization, relies on the programmer not stuffing it up. I usually stuff it up.

I’m working on software that needs to send regular status updates over a TCP connection to a central server. JSON is a nice human-readable choice for the data format. Websockets are great because I can lean on an Apache proxy to provide LetsEncrypt-signed TLS, and unlike a raw TCP stream the communication is already packetised into discrete messages.

I felt like using Rust but had no idea how hard it would be compared with a dynamically typed language, so I prototyped it. It turned out relatively straightforward thanks to a couple of excellent crates, serde_json and ws-rs:

This is the data model, which is similar to what I have planned for the real app. To exercise the parser I made sure it has a few tricky bits: an enum with associated values, an array, and a struct of one type within another. I didn’t need to tweak them at all. The default implementations of Serialize and Deserialize cope perfectly.

The None type of Option is represented as a JSON null, and the serde parser will quietly provide a None if the field is missing entirely in the JSON. While serde_json supports various enum formats, without any customisation I already get a very nice representation for the overall PlayerState. (serde_json’s output is minified.)

Very little is required on the server. The listen function provided by ws-rs blocks, using an mio event loop under the hood to handle an arbitrary number of connected clients. Right now I’m only providing a handler for each incoming message. If I can parse the JSON I do so and print the debug representation of the PlayerState.

As I hoped, the PlayerState is fully filled in from the JSON:

Over in the client it needs to juggle both the websocket connection and a timer to send the JSON at intervals.

Based on the ws-rs guide, I’ve made a struct that represents the established connection. The Handler trait lets me respond to messages and all the websocket lifecycle events by implementing the relevant functions.

Since I don’t care about messages from the server I only implement on_open and use it to start a new thread for sending updates. ws-rs takes care of thread safety—all I need to do is clone the Sender and give it to my new thread where it can use it to send messages. For this test I’ve created a hard-coded status and simulated playback by increasing the position on every update.

Similarly to listen, connect blocks for the duration of a complete connection, after which it returns and the program terminates. This disposes of the update thread, which would normally need to be cleaned up properly.

All this makes me very happy. I love using enums in data models and enums with associated values are even better. In my experience translating enums to numbers or string placeholders can be tedious and a great source of bugs. Having them transparently travel through the JSON means I can use them heavily and be guaranteed that they will have valid values at the other end. And if I did want to create a browser interface it would be easy to replicate the format in Javascript, albeit with none of the safety. I think I’m going to carry on with Rust for this project.

In the meantime Swift 4 finally has good support for JSON encoding so I am looking forward to using that at work.

 

Posted in programming, rust | Leave a comment