Analysing a Network Protocol

gpredict is piece of software for tracking things in orbits, sometimes you want to automatically point things at stuff in orbit. To get things pointed at stuff in orbit we can use a rotator controller, gpredict as a piece of radio software has an antenna rotator controller built it. The gpredict rotator controller expects to speak to something over TCP.

I have not been able to find documentation for the protocol (I didn't look very hard), I thought it would be fun to reverse engineer the protocol and write a simple daemon. Earlier I took some first steps to see what gpredict was doing on the network.

If you want to play a long at home this is what I am going to do:

  • set up a dummy daemon using netcat (nc -l localhost 4533)
  • use tcpdump with -XX to watch all traffic (e.g. tcpdump -XX -ilo0 tcp and port 4533)
  • send data from gpredict to the daemon (hit the 'engage' button on the antenna control screen)
  • play with responses (type into the console running nc)
  • look at the gpredict code starting here: https://github.com/csete/gpredict/blob/master/src/gtk-rot-ctrl.c

The Network traffic

$ nc -l 0.0.0.0 4533  
p

P  180.00   45.00

When I press the 'engage' button, gpredict sends a single lower case 'p', if I press enter, sending a blank line, gredict responds with a capital 'P' and two numbers. To me these numbers look like an Az El pair, they correspond to the values on the antenna control screen in gpredict. No need for tcpdump this time.

We have source avaialble

With only one half of the network protocol to look at, we can't get very far. gpredict is open source and there is a github mirror where we can browse the source tree. The file names in the 'src' directory show some promising results:

gtk-rot-ctrl.c
gtk-rot-ctrl.h
gtk-rot-knob.c
gtk-rot-knob.h
rotor-conf.c
rotor-conf.h
sat-pref-rot.c
sat-pref-rot.h

The pref and conf files, are probably configuration stuff, I have no idea what is in the knob file, but the gtk-rot-ctrl set of files is what we want. I confirmed this by picking a string in the UI of the relevant screen and grepping through the code for it. This can be troublesome if the software is heavily localised, but it this case I could track down the 'Engage' button to a comment in the code.

There are two functions used for network traffic, send is used to send data into a tcp connection, recv is used to receive data from a TCP connection. If we can find these in the code, we find where the software is generating network traffic. Normally only a starting point, it is very common to wrap these two functions into other convenience functions.

A grep through the code brings up a send call in send_rotctld_command. More grepping and we find that send_rotctld_command is called from two places, the get_pos function (which I have to guess asks for the rotators positions) and the set_pos function (which must try to set the rotators position).

The get_pos function fills a format string with "p\x0a" and uses send_rotcld_command to send it. Looking up 0x0A in an ascii table shows it is Line Feed(LF) also known as a newline on a unix system. It splits buffback on newlines using g_strsplit, looking to find two floating point numbers to use as azimuth and elevation, one on each line.

get_pos:

/* send command */
buff = g_strdup_printf("p\x0a");
retcode = send_rotctld_command(ctrl, buff, buffback, 128);
...
vbuff = g_strsplit(buffback, "\n", 3);
if ((vbuff[0] != NULL) && (vbuff[1] != NULL))
{
    *az = g_strtod(vbuff[0], NULL);
    *el = g_strtod(vbuff[1], NULL);
}

This piece of code shows up something really important, gpredict is using a single function to both send a command and gather the response from the remote end. If we look at send_rotctld_command the recv call is called right after a send. Here we can see that gpredict only does a single recv to gather responses, it is expecting a reply that fits into a single read. This is a bug, but probably not one that really matters.

/* try to read answer */
size = recv(ctrl->sock, buffout, sizeout, 0);

The set_pos function fills up a format string with a capital 'P', and two floating point numbers. It doesn't do any parsing of the response, only looking at the error code from the socket call.

set_pos:

/* send command */
g_ascii_formatd(azstr, 8, "%7.2f", az);
g_ascii_formatd(elstr, 8, "%7.2f", el);
buff = g_strdup_printf("P %s %s\x0a", azstr, elstr);

retcode = send_rotctld_command(ctrl, buff, buffback, 128);

Write a Daemon

With this little bit of analysis we have enough to write an antenna control daemon that gpredict can speak to. The rotator control protocol has two simple commands, a position query which expects the currect az/el across separate lines and a position setter, which expects no response.

#!/usr/bin/env python

import socket

TCP_IP = '127.0.0.1'
TCP_PORT = 4533
BUFFER_SIZE = 100

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(1)

conn, addr = s.accept()
print 'Connection address:', addr

az = 0.0
el = 0.0

while 1:
    data = conn.recv(BUFFER_SIZE)
    if not data:
        break

    print("received data:", data)

    if data == "p\n":
        print("pos query at az:{} el: {}", az, el);

        response = "{}\n{}\n".format(az, el)
        print("responing with: \n {}".format(response))
        conn.send(response)
    elif data.startswith("P "):
        values = data.split("  ")
        print(values)
        az = float(values[1])
        el = float(values[2])

        print("moving to az:{} el: {}".format( az, el));

        conn.send(" ")
    elif data == "q\n":
        print("close command, shutting down")
        conn.close()
        exit()
    else:
        print("unknown command, closing socket")
        conn.close()
        exit()

Using the python TCP server example as a starting point it is easy to put together a daemon that will listen to the rotator controller. The code should be pretty straight forward to read, we process the commands documented earlier. There is one addition that I didn't see in the code at first. There is a quit command that does not use the normal wrapper and instead uses send directly. This command was easy to handle.

This is how I approach network problems, whether in code I have written or code that is completely new to me. Hopefully if you have been following along at home the example above is straightforward to read.