Farsight's Advanced Exchange Access: The C Programming API, Part Three
By Mike Schiffman
Introduction
This article is the third and final in a multi-part blog series intended to
introduce and acquaint the reader with Farsight Security's AXA C API. This
article concludes the introduction of the libaxa
C programming API.
This article assumes the reader is familiar with AXA and related technologies. To brush up, the following Farsight Security Blog articles are recommended reading:
- Farsight's Advanced Exchange Access, Volume 1: Introduction to AXA
- Farsight's Advanced Exchange Access, Volume 2: Introduction to Sratool
- Farsight's Advanced Exchange Access, Volume 3: Introduction to Sratunnel
Additionally, if you haven't yet, you'll want to read the first and second articles in the series:
- Farsight's Advanced Exchange Access, The C Programming API, Part One
- Farsight's Advanced Exchange Access, The C Programming API, Part Two
We will pick up right where we left in the second article.
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:
- Check to see if it's time to quit
- Receive input from the server
- 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 returnfalse
which will result in the upper layer terminating the program.AXA_IO_BUSY
: There was no input before thepoll()
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. Heresratesttool
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.
static void AXA_PF(1,2)
srvr_disconnect(const char *p, ...)
{
va_list args;
va_start(args, p);
axa_verror_msg(p, args);
va_end(args);
axa_client_backoff(&client);
}
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 everytime 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:
- Know when something asynchronous has happened and it's time to quit
- 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("\t\tuser: SRA username\n");
printf("\t\tserver: SRA server\n");
printf("\t\tport: TCP port (typically 1021)\n");
printf("[-h]");
printf("\t\tthis 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
In the last three articles, 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
which contains sratool
and radtool
which are more robust implementations of
SRA and RAD clients built on top of libaxa
.
Mike Schiffman is a Packet Esotericist for Farsight Security, Inc.