AX.25 Client and Server Programming

Recently I’ve been experimenting with getting computers to talk to each other purely using AX.25 over VHF radio. That is, establishing connections and passing messages back and forth without any assistance whatsoever from TCP/IP.

The BSD networking API in Linux is very convenient for this. I’ve been able to write C programs that listen for connections, create connections and exchange data using code that doesn’t look all that different from what you would write for TCP/IP.

This post is a small showcase of working programs that perform these basic AX.25 tasks in Linux. Although the AX.25 HOWTO is extremely helpful (and required reading if you want to do any of this), it doesn’t go to this level of detail. The AX.25 apps source code is also a useful source of information, albeit more difficult to read.

My Setup

I have two stations that I refer to in the code. One is called VK7NTK-1 and the other is VK7NTK-2. Each is a computer running Debian Linux with its own VHF radio attached. I am using the Dire Wolf soundmodem along with kissattach to create a network interface ax0. On both stations this is configured as an AX.25 port simply called radio.

To keep things simple I have hard-coded station names inside the code. You can obtain these from the system—have a look at the header file axconfig.h from libax25-dev.

A server using ax25d

Using ax25d is kind of cheating but it’s a legitimate way to do things. Like inetd does for TCP/IP, it’s a daemon you run that will take care of listening for connections. When a connection request arrives it will look at its configuration and decide which program to run to handle that request.

When ax25d runs a program it will hook up the AX.25 input and output streams to the STDIN and STDOUT file descriptors. Sending data over the radio becomes as simple as a printf statement. Additional metadata such as the callsign of the calling station can be provided with command line parameters.

This means that you can write an AX.25 server without having to write any AX.25 code. Here is an example that will ask the user their name, reply, then hang up on them.

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
 
int main(int argc, char *argv[]) {
  /* Adjust standard in and out to minimise buffering when using '\r' line endings
   * Line buffering works best for stdin
   * No buffering works best for stdout */
  setvbuf(stdin, NULL, _IOLBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
 
  if (argc != 2) {
    fprintf(stderr, "Usage: %s calling_station\n", argv[0]);
    exit(1);
  }
 
  /* Display greeting using callsign from command line argument */
  printf("Hello, %s. What is your name?\r", argv[1]);
 
  /* Read in a line */
  size_t size = 32 * sizeof(char);
  char *buffer = malloc(size);
  if (getdelim(&buffer, &size, '\r', stdin) < 0) {
    fprintf(stderr, "Failed to read line\n");
    exit(1);
  }
  /* Strip carriage return from response */
  buffer[strcspn(buffer, "\r")] = 0;
 
  /* Reply */
  printf("Pleased to meet you, %s!\r", buffer);
 
  /* Wait 5 seconds then close the connection */
  sleep(5);
  free(buffer);
  return 0;
}

You’ve probably noticed that there’s a lot of \r going on. Way back in the days of packet they decided that a single carriage return was a good way to indicate the end of a line. If you use the axcall command in Debian to call someone, that’s what it will transmit when you press enter. I’m not sure if all the TNCs do this but it seems like a good idea to support it. If you’re writing all the clients and servers for your application there’s no reason why you couldn’t just use \n and save yourself some headaches.

This is super-tedious with the C standard library since it’s hard-coded to use \n as a line terminator for the purposes of line buffering and flushing. After some trial and error I landed on the settings used above with setvbuf. This worked reliably for me over the radio. Of course, ending lines with \r doesn’t work very well if you want to test your program from a normal terminal so you may want to code in an option to use either \r or \n.

Once I got past that sticking point it was pretty simple. I can use printf to output text and getdelim to read text. They’re nice, normal functions.

If you put this in a file called ax25d_example.c you can compile it on Debian like this:

cc ax25d_example.c -o ax25d_example

Next you need to install the binary somewhere and add a rule to your ax25d configuration so that it will be run. I put the following in /etc/ax25d.conf on the system VK7NTK-2. The part in square brackets refers to the callsign of the receiving station. If your AX.25 port is not called radio or if you put the program in a different location you will need to change it. The %S refers to the callsign of the connecting station, which is used in the message that is sent when they connect.

[VK7NTK-2 via radio]
default   * * * * * *  *    root    /usr/local/bin/ax25d_example ax25d_example %S

Finally, root needs to run the “ax25d” command. (Or you need to configure your system so it runs as a service.) Then you can connect to it! In this case I’m running the command on VK7NTK-1.

sudo axcall radio VK7NTK-2

ax25d_chat

A client using socket API

This example uses the socket API to establish a connection to another AX.25 node using no digipeaters. It simply connects, sends a message to the other node, then disconnects after a short delay.

It is assumed that this is running on VK7NTK-1, which wants to connect to VK7NTK-2.

#include <sys/socket.h>
#include <netax25/ax25.h>
#include <netax25/axlib.h>
#include <netax25/axconfig.h>
 
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
 
int main(int argc, char **argv) {
  int sockfd;
  struct sockaddr_ax25 my_addr, their_addr;
  int addr_size = sizeof(their_addr);
  
  sockfd = socket(AF_AX25, SOCK_SEQPACKET, 0);
  if (sockfd < 0)
  {
    perror("Error opening socket");
    exit(1);
  }
  
  bzero((char *) &my_addr, sizeof(my_addr));
  my_addr.sax25_family = AF_AX25;
  ax25_aton_entry("VK7NTK-1", (char*)&my_addr.sax25_call);
  my_addr.sax25_ndigis = 0;
  
  if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(my_addr)) < 0)
  {
    perror("Error binding");
    exit(1);
  }
  
  bzero((char *) &their_addr, sizeof(their_addr));
  their_addr.sax25_family = AF_AX25;
  ax25_aton_entry("VK7NTK-2", (char*)&their_addr.sax25_call);
  their_addr.sax25_ndigis = 0;
  
  if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(their_addr)) < 0)
  {
    perror("Error making connection");
    exit(1);
  }
  
  char *message = "Hello from the connecting program.\n";
  int written = write(sockfd, message, strlen(message));
  
  printf("Sent message via connection to VK7NTK-2\n");  
  sleep(1);
  close(sockfd);
  
  return 0;
  
}

There are three main preparatory steps before you can begin sending and receiving data.

First I must create the socket using the address family AF_AX25. Note the use of SOCK_SEQPACKET. Although an AX.25 connection has much in common with TCP, it is a sequence of messages, not an octet stream, so I use a different constant here.

Second I must prepare a sockaddr to specify which interface I want to use to connect. This is important if you have more than one radio attached—Linux has no magical knowledge that a particular radio will reach a particular callsign. So I set my own callsign including SSID and call bind.

Third I must connect to the remote node. I have to prepare another sockaddr and pass it to connect.

If all of this is successful then the connection has been established. I can then use write and read to send and receive data over the socket, like anything else. I won’t write any more about this—at this point it’s exactly the same as any other UNIX network programming.

Note that you must link in the AX.25 library to compile this program:

cc -lax25 client.c -o client

A server using socket API

This example is a standalone C program that will listen for a connection request on the radio interface, exchange data, then close the connection. It then terminates.

Again this is hard-coded to run on VK7NTK-2, because that is the callsign used when choosing the interface to which to bind.

#include <sys/socket.h>
#include <netax25/ax25.h>
#include <netax25/axlib.h>
#include <netax25/axconfig.h>
 
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
 
int main(int argc, char **argv) {
  int sockfd;
  struct sockaddr_ax25 my_addr, their_addr;
  
  sockfd = socket(AF_AX25, SOCK_SEQPACKET, 0);
  if (sockfd < 0)
  {
    perror("Error opening socket");
    exit(1);
  }
  
  bzero((char *) &my_addr, sizeof(my_addr));
  my_addr.sax25_family = AF_AX25;
  ax25_aton_entry("VK7NTK-2", (char*)&my_addr.sax25_call);
  my_addr.sax25_ndigis = 0;
  
  if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(my_addr)) < 0)
  {
    perror("Error binding");
    exit(1);
  }
  
  listen(sockfd, 5);
  
  int len = sizeof(their_addr);
  int clientfd = accept(sockfd, (struct sockaddr *)&their_addr, &len);
  if (clientfd < 0)
  {
    perror("Error on accept");
    exit(1);
  }
  
  char greeting[128];
  const ax25_address* their_call = (ax25_address *)(&their_addr.sax25_call);
  char *static_call = ax25_ntoa(their_call);
  sprintf(greeting, "Hello %s, please leave a message:\n", static_call);
  int n = write(clientfd, greeting, strlen(greeting));
  printf("Got a call from %s\n", static_call);
  
  char buffer[256];
  n = read(clientfd, buffer, 256);
  if (n < 0)
  {
    perror("Error reading from socket");
    exit(1);
  }
  
  printf("They left a message: %s\n", buffer);
  
  char message2[128];
  sprintf(message2, "Thanks for the message. Shutting connection in 10 seconds.\n");
  n = write(clientfd, message2, strlen(message2));
  if (n < 0)
  {
    perror("Error writing to socket");
    exit(1);
  }
  
  return 0;
  
}

Like the client there are three steps that must be performed to accept a connection. As before the first is to create an AX.25 SOCK_SEQPACKET socket fd, and the second is to bind to the desired local interface. This is the radio that will be listening for connections.

The third step is to call accept. This will wait for an incoming connection. When it does, the address of the calling station will be filled out in the struct their_addr and the connection is established.

The utility function ax25_ntoa converts the “network” form of the caller’s callsign-SSID to a string that I can print, like VK7NTK-1.

Then I can simply use read and write to exchange data. It looks like this on the server:

on_server

It looks like this on the client (using axcall):

from_connector

Note that I have a used a shortcut here—I should really read in a loop until I have received a \r, then process the data up to that point. Since AX.25 tends to send data in largeish packets by design, this doesn’t present a problem with small strings. The user’s input with terminating \r all arrives in a single read.

Next Steps

I have been exploring the Linux API for “unproto” operation, i.e., sending and receiving packets without establishing connections. Sending with SOCK_DGRAM is working fine but receive is not. This is annoying, because SOCK_RAW requires you to parse packets yourself. I would like to get to the bottom of this.