User Guides

The AXA API Reference

This article is intended to introduce and acquaint the reader with Farsight Security® Inc.’s (now a part of DomainTools) AXA C API. It will introduce the libaxa C programming API and showcase a simple working example program.

This information is intended for users of SIE Remote Access (SRA). This article assumes the reader is familiar with AXA and related technologies. To brush up, on AXA, a good starting point is the AXA User Guide.

This article, geared towards intermediate-level C programmers, is by no means an exhaustive API reference. For that, the reader is directed to the accompanying Doxygen-based API manual that is available in the AXA GitHub repository. Please see the Additional Information section below. This article covers libaxa version 1.1.2.

sratesttool

sratesttool is a Unix command-line utility capable of the following:

  • Connecting to an SRA server
  • Enabling an SIE channel
  • Setting an AXA watch
  • Streaming watch hits to the console

Throughout this short series, we’ll examine how all of this done. In this first article, we’ll cover the main driver which contains the initialization code and controls the main program flow. For example:

$ sratesttool -s tls:user@sraserver,1021 -c 255 -w ch=255
Farsight Security SRA Test Tool
connecting to tls:user@sraserver,1021...
connected
parsing watch: ch=255...
parse ch=255 OK
setting watch on server...
watch set OK
parsing channel: 255...
parse 255 OK
enabling channel on server...
channel enabled OK
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:21.111788988
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:21.612488985
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:22.113173007
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:22.613782882
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:23.114434957
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:23.615050077
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:24.115665912
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:24.616344928
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:25.117007970
NMSG watch hit, channel: 255 @ 2015-07-29T19:13:25.617640972
^C10 total watch hits

Please note sratesttool is not a full implementation of the AXA protocol. It is meant as a tutorial on how to stand up a simple connection to the SRA server, issue a few commands, and stream data. sratesttool is only written to work for TLS and many libaxa code paths are not taken nor is robust error checking performed. It is intended as an entry-level program to get the reader familiar with the AXA protocol and the libaxa API. 

Sratesttool.c 

The code for sratesttool is contained in a single source file that can be compiled into a fully functional program. To build it, you’ll need to link against the libaxa library. You can find the source code in AXA GitHub repository. If you’re a Debian-user, there are packages ready for download as well. While we will cover the source code across several articles, the full source code is available for download from Farsight Security’s blog-code GitHub page.

Preamble

The first section contains the source code license and the standard C header file include progression:


/*
* Copyright (c) 2015 by Farsight Security, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include <sysexits.h>
#include <unistd.h>
#include <signal.h>
#include <limits.h>

#include <axa/client.h>
#include <axa/axa_endian.h>

Server-Specific Functions

The following server-specific functions wrap libaxa functions. We’ll explore all of them in detail later, for now it is sufficient to understand what they do:

  • srvr_connect(): Connects to the server. – srvr_cmd(): Send a command to the server and wait for a response.
  • srvr_wait_resp(): Wait for a response from the server.
  • srvr_process(): Process a message from the server.
  • srvr_disconnect(): Disconnects from the server.
/* connect to server
*
* returns true on success, false on failure
*/
static bool srvr_connect(void);

/* send an AXA command to the server and wait for the response
*
* returns true on success, false on failure
*/
static bool srvr_cmd(axa_tag_t, /* AXA tag or AXA_TAG_NONE */
axa_p_op_t, /* AXA opcode (original) */
const void *, /* body of message to send */
size_t, /* length of message */
axa_p_op_t); /* expected response opcode */

/* wait for a response from the server
*
* returns true on success, false on failure
*/
static bool srvr_wait_resp(axa_p_op_t, /* expected response opcode */
axa_p_op_t); /* original opcode*/

/* process a message from the server */
static void srvr_process(void);

/* disconnect from server and print a message */
static void srvr_disconnect(const char *, /* message to print */
...) /* optional varargs */
AXA_PF(1,2);

Miscellaneous Functions

Miscellaneous functions are declared next:

/* stop everything and shutdown */
static void stop(int                        /* exitcode */
                        );
/* signal handler */
static void sigterm(int                     /* signal number of caught sig */
                        );
/* print usage */
static void usage(const char *              /* program name */
                        );

Global Variables

There is a small collection of global data:

  • static axa_client_t client: Opaque blob, contains all client state.
  • static const char *server_str: The transport/username/server string.
  • static const char *watch_str: The watch string.
  • static const char *channel_str: The channel string.
  • static int terminated: When this value is > 0, it’s time to quit
  • static uint64_t hits: Number of watch hits sratesttool has observed.
static axa_client_t client;
static const char *server_str;
static const char *watch_str;
static const char *channel_str;
static int terminated;
static uint64_t hits;

Main Function Argument Processing

The initial stanza of main function is standard and uninteresting command-line argument processing:

int
main(int argc, char **argv)
{
    int opt;
    axa_emsg_t emsg;
    const char *cp;

    while ((opt = getopt(argc, argv, "c:hs:w:")) >= 0)
    {
        switch (opt)
        {
            case 'c':
                channel_str = optarg;
                break;
            case 'h':
                usage(argv[0]);
                return (EXIT_SUCCESS);
            case 's':
                server_str = optarg;
                break;
            case 'w':
                watch_str = optarg;
                break;
            default:
                usage(argv[0]);
                return (EX_USAGE);
        }
    }
    if (server_str == NULL || channel_str == NULL || watch_str == NULL)
    {
        fprintf(stderr, "-s, -c, -w are all required\n");
        return (EX_USAGE);
    }

Axa Initialization

Next, we encounter the first libaxa API function calls. The first stanza sets up the AXA logging substructure. One of the conveniences libaxa offers is an opaque “three stream” logging / syslog interface. With libaxa, you have the following logging streams:

  • trace: Tracing stream for server-side error messages
  • verror: Error stream for client-side error messages
  • accounting: Accounting information (packet sent/loss/transit/limit totals)

sratesttool does not use the syslog interface and as such initializes the loggers to emit messages only to stderr. For more information, see the Doxygen manual for axa_parse_log_opt().

After the logging initialization, we initialize the AXA client context. This is an opaque structure that contains all of the state required to open and maintain an SRA server connection.

Next comes the signal processing. In order to provide an orderly exit from signals that cause the termination of the sratesttool process with garbage collection and a statistics report, we catch and hand them off to our simple signal handler.

/* set the global program name (for logging) */
axa_set_me(argv[0]);
/* set the tracing stream to emit to stderr only */
AXA_ASSERT(axa_parse_log_opt(&emsg, "trace,off,stderr"));
/* set the error stream to emit to stderr only */
AXA_ASSERT(axa_parse_log_opt(&emsg, "error,off,stderr"));
/* set the accounting stream to emit to stderr only */
AXA_ASSERT(axa_parse_log_opt(&emsg, "acct,off,stderr"));

/* initialize the AXA syslog interface */
axa_syslog_init();

/* ensure all stdio FDs are open and ready for business */
axa_clean_stdio();

/* initialize the client context (including AXA IO engine) */
axa_client_init(&client);

/* catch and handle these signals for graceful exit */
signal(SIGPIPE, SIG_IGN);
signal(SIGHUP, sigterm);
signal(SIGTERM, sigterm);
signal(SIGINT, sigterm);

axa_trace_msg("Farsight Security SRA Test Tool\n");

Server Connection

At this point, our AXA client is initialized and sratesttool is ready for business. It then reaches out and tries to make a connection to the server. If something goes wrong, sratesttool will bail. More robust implementations such as sratool, sratunnel and axaclientd will attempt to reconnect using an exponential back off and retry algorithm.

A lot of core libaxa code is contained in the srvr_connect() function including:

  • Opening the client connection to the server.
  • Parsing the watch string and setting the watch on the server.
  • Parsing the channel string and enabling the channel on the server.
  • Starting the data stream.

We will explore this in more detail later.

/* try (once) to connect to the SRA server and bail on any error */
if (!srvr_connect())
{
    exit(EXIT_FAILURE);
}

Process Data

We now descend into the infinite event loop. The first check will always be to see if sratesttool needs to exit (as the result of an asynchronous event such as the user hitting ctrl-c at the console).

The call to axa_io_wait() tells libaxa to simply wait up to 10ms for input from the server. Internally, libaxa polls its list of file descriptors to look for input from the server. If the timer expires and no data is available, axa_io_wait() will return AXA_IO_BUSY which just returns control to the top of the loop. If data is waiting to be read, AXA_IO_OK will be returned and srvr_process() will be called to handle it (covered in detail later). For sratesttool, most of the time, this should be watch hits. If something goes wrong when polling, AXA_IO_ERR is returned and sratesttool will disconnect and quit.

Also of note, axa_io_wait() is called without keepalive functionality (not needed for this example) and without tunnel debugging (only used for SSH-based connections).

/* event loop where we continuously wait/send data/wait */
    for (;;)
    {
        if (terminated != 0)
        {
            stop(terminated);
        }
        /* wait up to 10ms for data from the server */
        switch (axa_io_wait(&emsg,
                    &client.io,         /* address of AXA IO engine */
                    10,                 /* wait up to this many ms */
                    false,              /* wake up to send a keepalive */
                    false))             /* pay attention to tunnel msgs */
        {
            /* AXA IO error */
            case AXA_IO_ERR:
                srvr_disconnect("%s", emsg.c);
                break;
            /* incomplete response, poll() and try again */
            case AXA_IO_BUSY:
                break;
            /* operation has finished, time to process the result */
            case AXA_IO_OK:
                srvr_process();
                break;
            default:
                AXA_FAIL("impossible axa_io_wait() result");
        }
    }
}

Connecting To The Server And Setting State

The first srvr_* function we’ll dissect is a big one, responsible for connecting to the SRA server, setting the watch and enabling the channel. First, we explain important AXA-specific data types/variables:

  • axa_p_watch_t watch: An AXA watch object; contains all of the state for an AXA watch specification.
  • axa_p_channel_t channel: An AXA channel object; contains all of the state for an AXA channel specification.
  • axa_emsg_t emsg: An AXA error message object; if an error occurs in a libaxa function, this will contain details on what caused it.
  • axa_tag_t cur_tag: A tag is an identifier used to uniquely “tag” specific events during the lifetime of an AXA session. To refer to these events, the client or server will use the tag. Some AXA messages do not require tags, in which case we will use the value AXA_TAG_NONE. Required tags must be unique during the lifetime of the corresponding client request. Some client requests such as a watch can last indefinitely and will elicit many server responses all with the same tag.
static bool
srvr_connect(void)
{
    axa_p_watch_t watch;
    size_t watch_len;
    axa_p_channel_t channel;
    axa_emsg_t emsg;
    bool res;
    axa_tag_t cur_tag;

Connect To The Server

The axa_client_open() function attempts to open a server connection and establish state for our client’s connection. The first two arguments, passed by reference, will be filled in by libaxa(emsg) if something goes wrong, client if something doesn’t). The next argument, server_str is a specially crafted text string of the following format: transport:user@server[,port].

  • transport: SRA supports either TLS or SSH as an encrypted transport. sratesttool only supports TLS, so the argument here should be tls.
  • user: The username of the user. This is provisioned by Farsight Security when your account is turned up.
  • server: The SRA server IP address or hostname.
  • port: Used only for TLS, to specify the port the SRA server listens for TLS connections.

The input socket buffer size is set to be 256  1024, a larger than normal value due to the potentially high-volume of SIE data transited by SRA. Internally, this will be requested to be set by setsockopt(..SO_RCVBUF..).

The return value from axa_client_open() is evaluated in a small switch table which checks for errors or non sequitur as per the following:

  • AXA_CONNECT_ERR: Something went terribly wrong, print emsg and quit.
  • AXA_CONNECT_TEMP: A temporary error occurred, print emsg and quit. In more robust implementations, we would probably back off and try again.
  • AXA_CONNECT_DONE: The connection is complete.
  • AXA_CONNECT_NOP: The connection is complete and an AXA_P_OP_NOP is sent to the server. This is done so the client can tell the server the version of the AXA protocol that the client is using. If need be, the server can down shifting to an old version of the protocol.
  • AXA_CONNECT_USER: For TCP-based socket connections, does not apply to TLS (or SSH) connections and should be an error in this case.
axa_trace_msg("connecting to %s...", server_str);
switch (axa_client_open(&emsg,      /* if func fails, will contain error */
                        &client,    /* client context */
                        server_str, /* server address string */
                        false,      /* true to enable RAD mode */
                        false,      /* true when SSH tunnel debugging */
                        256 * 1024, /* socket buffer size */
                        false))     /* true for non-blocking */
{
    /* permanent failure, connection has been closed, check emsg */
    case AXA_CONNECT_ERR:
        axa_error_msg("%s", emsg.c);
        return (false);
    /* A temporary failure, connection has been closed, check emsg.
    * In more robust implementations, we would try to reconnect to the
    * server.
    */
    case AXA_CONNECT_TEMP:
        axa_error_msg("%s", emsg.c);
        srvr_disconnect("%s", emsg.c);
        return (false);
    /* connect is complete */
    case AXA_CONNECT_DONE:
        break;
    /* non-blocking connection waiting for TCP SYN/ACK or TLS handshake */
    case AXA_CONNECT_INCOM:
        AXA_FAIL("impossible result from axa_client_open");
    /* connect is complete (incl xmit of AXA_P_OP_NOP) */
    case AXA_CONNECT_NOP:
        break;
    /* connect is complete (incl xmit of AXA_P_OP_USER) */
    case AXA_CONNECT_USER:
        srvr_disconnect("%s", emsg.c);
        return (false);
}

Pause The Output

After successfully connecting to the server but before any watches are set or channels are enabled, an AXA_P_OP_PAUSE command is sent. This “pause” command tells the server to stop sending data and to actually discard it, server side. This is a global switch the pauses data output for all channels for the current user.

This is the first time we encounter the srvr_cmd() function which is used to send commands and data to the SRA server. We’ll cover it in detail later but for now it’s important to note that it accepts an AXA tag argument:

  • AXA_TAG_MIN: The “minimum” tag value, this is a non-specific tag value used when we need a tag but don’t care about which one.

And two AXA opcode arguments:

  • AXA_P_OP_PAUSE: The opcode representing the action requested of the server. In this case to pause the data flow.
  • AXA_P_OP_OK: The opcode expected from the server the for the operation to be considered a success.
/* block any watch hits until we are ready */
if (!srvr_cmd(AXA_TAG_MIN, AXA_P_OP_PAUSE, NULL, 0, AXA_P_OP_OK))
{
    return (false);
}

Parse and Set the Watch

Next, the watch string is parsed with a call to axa_parse_watch(). If there is a problem parsing it, libaxa will do its best to emit a helpful error message in emsg. However, if the watch string makes no sense, the function will fail with an empty emsg.

For a verbose treatment of what SRA watches can look like, see sratool(1).

After successfully parsing the watch, it is set on the server with a call to srvr_cmd() with an AXA opcode of AXA_P_OP_WATCH and an expected server response of AXA_P_OP_OK.

cur_tag = 1;
/* parse the watch string */
axa_trace_msg("parsing watch: %s...\n", watch_str);
res = axa_parse_watch(&emsg, &watch, &watch_len, watch_str);
if (!res)
{
    if (emsg.c[0] == '0')
    {
        axa_error_msg("unrecognized "-w %s"", watch_str);
    }
    else
    {
        axa_error_msg(""-w %s": %s", watch_str, emsg.c);
    }
    return (false);
}
else
{
    axa_trace_msg("parse %s OK\n", watch_str);
}

/* set the watch on the server */
axa_trace_msg("setting watch on server...\n");
if (!srvr_cmd(cur_tag, AXA_P_OP_WATCH, &watch, watch_len, AXA_P_OP_OK))
{
    return (false);
}
axa_trace_msg("watch set OK\n");

Parse And Enable The Channel

Next, in the same manner as above, the channel string is parsed and enabled on the server.

memset(&channel, 0, sizeof(channel));
/* parse the channel string */
axa_trace_msg("parsing channel: %s...\n", channel_str);
if (!axa_parse_ch(&emsg, &channel.ch, channel_str,
            strlen(channel_str), true, true))
{
    axa_error_msg(""-c %s": %s", channel_str, emsg.c);
    return (false);
}
else
{
    axa_trace_msg("parse %s OK\n", channel_str);
}

channel.on = 1;
axa_trace_msg("enabling channel on server...\n");
if (!srvr_cmd(AXA_TAG_MIN, AXA_P_OP_CHANNEL, &channel, sizeof(channel),
    AXA_P_OP_OK))
{
    return (false);
}

Un-Pause The Output

After the channel is enabled, sratesttool is ready to start streaming watch hits. It lets the server know by sending the opcode AXA_P_OP_GO which tells the server it’s time to un-pause the output.

Now the server connection process is complete and the function returns successfully.

    if (!srvr_cmd(AXA_TAG_MIN, AXA_P_OP_GO, NULL, 0, AXA_P_OP_OK))
    {
        return (false);
    }
    axa_trace_msg("channel enabled OK\n");
    return (true);
}

Run a Command on The Server

The next function, as we’ve already seen, srvr_cmd(), is used to send opcodes and data to the SRA server. It is a wrapper to axa_client_send() with additional code to disconnect via srvr_disconnect() if a problem occurs.

We’ve already covered the tag and opcode arguments, but the srvr_cmd() function also accepts data in the form of an opaque pointer and its length. The call to axa_client_send() fires off the request to the server and, if it doesn’t fail, a call to srvr_wait_resp() is made to process the response.

static bool
srvr_cmd(axa_tag_t tag, axa_p_op_t op, const void *b, size_t b_len,
    axa_p_op_t resp_op)
{
    axa_p_hdr_t hdr;
    axa_emsg_t emsg;
    char pbuf[AXA_P_STRLEN];

    if (!axa_client_send(&emsg, &client, tag, op, &hdr, b, b_len))
    {
        srvr_disconnect("sending %s failed: %s",
                axa_p_to_str(pbuf, sizeof(pbuf), true, &hdr, b),
                emsg.c);
        return (false);
    }

    return (srvr_wait_resp(resp_op, op));
}

Wait for a Response From the Server

The main event loop where sratesttool receives input from the SRA server and decides what to do with it, called srvr_wait_resp() is covered next. The function can be broken down into three parts:

  1. Check to see if it’s time to quit
  2. Receive input from the server
  3. Determine what to do with the input

Check To See If It’s Time To Quit

The first thing srvr_wait_resp() does is check to see if the global sentry value terminated is set to true. For this to be the case, one of the signals sratesttool was initialized to catch, has been caught. Most of the time, this will be SIGINT from the user pressing “ctrl-c” at the console. In this case, stop() (covered below) is called which begins the shutdown process.

static bool
srvr_wait_resp(axa_p_op_t resp_op, axa_p_op_t orig_op)
{
    bool result, done;
    axa_emsg_t emsg;

    result = false;
    done = false;
    do
    {
        if (terminated != 0)
        {
            stop(terminated);
        }

Receive Input From The Server

If it’s not time to quit, control is passed to axa_input(). This function accepts the standard emsg argument we’ve already seen, a pointer to a client.io context, and the number of milliseconds to block and wait for input which in this case is INT_MAX (this can be considered an indefinite block while waiting for data).

The result is switched and the following cases are evaluated:

  • AXA_IO_ERR: A fatal error occurred, dump the error and return false which will result in the upper layer terminating the program.
  • AXA_IO_BUSY: There was no input before the poll() timer expired or some other non-fatal condition occurred. Control continues back up to the top.
  • AXA_IO_OK: Valid data was received, time to figure out what to do with it.
switch (axa_input(&emsg, &client.io, INT_MAX))
        {
            case AXA_IO_ERR:
                axa_error_msg("%s", emsg.c);
                goto out;
            case AXA_IO_BUSY:
                continue;
            case AXA_IO_OK:
                break;
            default:
                AXA_FAIL("impossible axa_input() result");
        }

Determine What To Do With The Input

Next, sratesttool evaluates the received AXA protocol message header’s opcode inside of a large switch table. For posterity, all of the possible opcodes are listed and in a more robust implementation, each code path would likely be populated. In the case of sratesttool only two server responses are interesting:

  • AXA_P_OP_HELLO: Process the “hello” message from the server. The AXA protocol version used by the server is saved and the client attempts to adjust to a version it can understand.
  • AXA_P_OP_OK: Process a “result” message from the server. Here sratesttool ensures that the opcode received is the expected one.

After a processing the response, axa_recv_flush() is called to purge (free()) the AXA protocol message from the IO context.

If something went wrong, axa_client_backoff() is called which will result in the client closing the connection to the server and the shutting down of the IO context.

switch ((axa_p_op_t)client.io.recv_hdr.op)
        {
            case AXA_P_OP_NOP:
                break;
            case AXA_P_OP_HELLO:
                if (!axa_client_hello(&emsg, &client, NULL))
                {
                    axa_error_msg("%s", emsg.c);
                }
                else
                {
                    axa_trace_msg("connected OK");
                }
                break;
            case AXA_P_OP_OPT:
                done = true;
                break;
            case AXA_P_OP_OK:
                if (resp_op == client.io.recv_hdr.op &&
                        orig_op == client.io.recv_body->result.orig_op)
                {
                    result = true;
                    done = true;
                }
                break;
            case AXA_P_OP_ERROR:
                axa_error_msg("server returned error");
                done = true;
                break;
            case AXA_P_OP_MISSED:
            case AXA_P_OP_MISSED_RAD:
            case AXA_P_OP_WHIT:
            case AXA_P_OP_AHIT:
            case AXA_P_OP_WLIST:
            case AXA_P_OP_ALIST:
            case AXA_P_OP_CLIST:
                /* in this function, these are all unexpected op codes */
                break;
            case AXA_P_OP_USER:
            case AXA_P_OP_JOIN:
            case AXA_P_OP_PAUSE:
            case AXA_P_OP_GO:
            case AXA_P_OP_WATCH:
            case AXA_P_OP_WGET:
            case AXA_P_OP_ANOM:
            case AXA_P_OP_AGET:
            case AXA_P_OP_STOP:
            case AXA_P_OP_ALL_STOP:
            case AXA_P_OP_CHANNEL:
            case AXA_P_OP_CGET:
            case AXA_P_OP_ACCT:
            case AXA_P_OP_RADU:
            default:
                AXA_FAIL("impossible AXA op of %d from %s",
                    client.io.recv_hdr.op, client.io.label);
        }
        axa_recv_flush(&client.io);
    }
    while (!done);

out:
    if (!result)
    {
        /* disconnect for now if we failed to get the right response */
        axa_client_backoff(&client);
    }
    return (result);
}

Disconnect From the Server

If something egregious went wrong, sratesttool will emit an error message via axa_verror_msg() and shutdown the server connection via axa_client_backoff(). As has been mentioned before, a more robust implementation may make use of the internal backoff timers and attempt a reconnect.

Stop and Shutdown

The stop() function is called any time the interrupted sentinel evaluates to not 0. It performs an orderly shutdown, reports the number of watch hits, and then exits the program.

static void AXA_NORETURN
stop(int s)
{
    axa_client_close(&client);
    axa_io_cleanup();

    axa_trace_msg("%"PRIu64" total watch hits\n", hits);

    exit(s);
}

Read SIE Data From The Server

The function that is called from the main event loop every time SIE data is ready is called srvr_process(). First, axa_recv_buf() is called and the result is evaluated. Important to note, the axa_recv_buf() can block so if this is undesirable, use of the other server read functions covered earlier such as axa_io_wait() or axa_input(). Once an AXA_IO_OK is returned, control passes to another switch table where the response opcode is evaluated.

static void
srvr_process(void)
{
    int n;
    char buf[BUFSIZ];
    axa_emsg_t emsg;
    struct timespec ts;
    struct tm *tm_info;

    switch (axa_recv_buf(&emsg, &client.io))
    {
        case AXA_IO_OK:
            break;
        case AXA_IO_ERR:
            srvr_disconnect("%s", emsg.c);
            return;
        case AXA_IO_BUSY:
            return;             /* wait for the rest */
        case AXA_IO_KEEPALIVE:  /* NA for this example */
        case AXA_IO_TUNERR:     /* NA for this example */
            break;
    }

Process the Watch Hit

The only case sratesttool is interested in is AXA_P_OP_WHIT. When the received header opcode is a “watch hit”, sratesttool next figures out the type of watch hit being reported to the client, NMSG or IP. While sratesttool is not interested in the contents of the watch hit, it does need to extract the timestamp, which is stored in different places depending on the type. Next, a human readable string is constructed using the type of watch hit, the SIE channel it occurred on, and the timestamp of the watch hit (including nanoseconds). The hits counter is incremented and control proceeds to the end of the switch table where, as seen above, axa_recv_flush() is called to purge the AXA protocol message from the IO context.

n = 0;
    switch ((axa_p_op_t)client.io.recv_hdr.op)
    {
        case AXA_P_OP_NOP:
            break;
        case AXA_P_OP_ERROR:
            srvr_disconnect(" ");
            return;
        case AXA_P_OP_MISSED:
        case AXA_P_OP_MISSED_RAD:
            break;
        case AXA_P_OP_WHIT:
            switch (client.io.recv_body->whit.hdr.type)
            {
                case AXA_P_WHIT_NMSG:
                    ts.tv_sec = client.io.recv_body->whit.nmsg.hdr.ts.tv_sec;
                    ts.tv_nsec = client.io.recv_body->whit.nmsg.hdr.ts.tv_nsec;
                    break;
                case AXA_P_WHIT_IP:
                    ts.tv_sec =
                        AXA_P2H32(client.io.recv_body->whit.ip.hdr.tv.tv_sec);
                    ts.tv_nsec = 1000 *
                        AXA_P2H32(client.io.recv_body->whit.ip.hdr.tv.tv_usec);
                    break;
            }
            tm_info = localtime((time_t *)&ts.tv_sec);
            n = strftime(buf + n, 26, "%Y-%m-%dT%H:%M:%S", tm_info);
            snprintf(buf + n, BUFSIZ - n, ".%ld", ts.tv_nsec);
            axa_trace_msg("%s watch hit, channel: %3d @ %s\n",
                    client.io.recv_body->whit.hdr.type == AXA_P_WHIT_NMSG ?
                    "NMSG" : "IP",
                    client.io.recv_body->whit.hdr.ch,
                    buf);
            hits++;
            break;
        case AXA_P_OP_AHIT:
        case AXA_P_OP_OK:
        case AXA_P_OP_HELLO:
        case AXA_P_OP_WLIST:
        case AXA_P_OP_ALIST:
        case AXA_P_OP_OPT:
        case AXA_P_OP_CLIST:
            break;
        case AXA_P_OP_USER:
        case AXA_P_OP_JOIN:
        case AXA_P_OP_PAUSE:
        case AXA_P_OP_GO:
        case AXA_P_OP_WATCH:
        case AXA_P_OP_WGET:
        case AXA_P_OP_ANOM:
        case AXA_P_OP_AGET:
        case AXA_P_OP_STOP:
        case AXA_P_OP_ALL_STOP:
        case AXA_P_OP_CHANNEL:
        case AXA_P_OP_CGET:
        case AXA_P_OP_ACCT:
        case AXA_P_OP_RADU:
        default:
            AXA_FAIL("impossible AXA op of %d from %s",
                client.io.recv_hdr.op, client.io.label);
    }

    axa_recv_flush(&client.io);
}

Signal Handler

The signal handler simply sets the global sentinel terminated to the integral value of the signal. This allows the uppers layers to:

  1. Know when something asynchronous has happened and it’s time to quit
  2. Know which signal was sent to sratesttool

If a signal is sent repeatedly, it is considered urgent and sratesttool will reset the default signal handler via signal(sig, SIG_DFL) which will immediately terminate the program.

void
sigterm(int sig)
{
    terminated = sig;

    signal(sig, SIG_DFL);       /* quit early on repeated signals */
}

Usage

Finally, we conclude with a simple usage function instructing the user how to invoke sratesttool.

static void
usage(const char *name)
{
    printf("SRA Test Tool (c) 2015 Farsight Security, Inc.\n");
    printf("nUsage: %s [options]\n", name);
    printf("[-s tls:user@server,port]\n");
    printf("ttuser:      SRA username\n");
    printf("ttserver:    SRA server\n");
    printf("ttport:      TCP port (typically 1021)\n");
    printf("[-h]");
    printf("ttthis prose\n");
    printf("[-c channel]");
    printf("tenable channel (default: "255", SIE Heartbeat)\n");
    printf("[-w watch]");
    printf("tset watch (default: "ch=255", SIE Heartbeat channel)\n");
}

Conclusion

We’ve covered the internals of sratesttool, a very simple “hello world” SRA client. If you are interested in learning more, please check out the AXA distribution repository which contains sratool which are more robust implementations of SRA clients built on top of libaxa.

Additional Information