L3/L4 Firewall (simple_firewall)

Introduction

The simple_firewall program is a sample L3/L4 firewall which looks at network addresses and ports of a TCP packet and determines if that packet should be allowed or blocked.

The following code block shows the example rules for simple_firewall. Each rule consists of action, source address, destination address, and (optional) port fields. The action field can be either ACCEPT or DROP. The source and destination address field can be any network address or netmask space on which the action will be performed. The port field specifies the TCP port number relevant to the action.

#(act)  (src)        (dst)        (port)
DROP    10.0.0.0/24  10.0.0.0/24  dport:80
ACCEPT  10.0.1.7     10.0.1.9     sport:1024
ACCEPT  10.0.2.7     10.0.2.9
ACCEPT  10.0.3.0/24  10.0.3.0/24

Code Walkthrough

The following sections provide an explanation of the main components of the simple_firewall code. All mOS net library functions used in the sample code are prefixed with mtcp_ and are explained in detail in the Programmer’s Guide - mOS Programming API. Note that we omit the error handling logics in the example codes for brevity.

(1) The main() Function

The main() function performs the initialization and calls the execution threads for each CPU core.

The first task is to initialize mOS thread based on the mOS configuration file. mos_conf_file is the file path to the mos.conf file which will be provided to mtcp_init(). ParseConfigFile() function is a custom parser for simple_firewall rules whose format is specified in the previous subsection.

/* parse mos configuration file */
ret = mtcp_init(mos_conf_file);

/* parse simple firewall-specfic startup file */
ParseConfigFile(simple_firewall_file);

Within the main() function, you can define your own events (user-defined event) which can be used by the application. Following example defines an event triggered when the incoming packet is an initial SYN packet (not a SYN+ACK packet) for a certain flow.

/* event for the initial SYN packet */
initSYNEvent = mtcp_define_event(MOS_ON_PKT_IN, CatchInitSYN);

static bool
CatchInitSYN(mctx_t mctx, int sockid,
             int side, event_t events, filter_arg_t *arg)
{
        struct pkt_info p;
        mtcp_getlastpkt(mctx, sockid, side, &p);

        return (p.tcph->syn && !p.tcph->ack);
}

We use mtcp_getconf() to retrieve the mOS configuration parameters (e.g. number of cores).

/* populate local mos-specific mcfg struct for later usage */
mtcp_getconf(&mcfg);

...

/* initialize monitor threads */
for (i = 0; i < mcfg.num_cores; i++)
     CreateAndInitThreadContext(&ctx[i], i, initSYNEvent);

(2) Per-thread Initialization Function

The CreateAndInitThreadContext() function is the core functional part of the mOS thread initialization. core variable is the CPU id that is used to affinitize new thread to the core. udeForSYN is the user-defined event which catches the initial SYN packet for each flow.

static void
CreateAndInitThreadContext(struct thread_context* ctx, int core, event_t udeForSYN)
{
        ...

The first step is the creation of mtcp context and socket. This is followed by the creation of a MOS_SOCK_MONITOR_STREAM socket to monitor TCP-related events.

/* create mtcp context */
ctx->mctx = mtcp_create_context(core);

/* create socket */
ctx->mon_listener = mtcp_socket(ctx->mctx, AF_INET, MOS_SOCK_MONITOR_STREAM, 0);

The next step is to register the event callback function, which handles the incoming packets with specific conditions. This simple_firewall application registers an event callback function for handling the initial SYN packet.

/* register callback */
mtcp_register_callback(ctx->mctx, ctx->mon_listener, udeForSYN,
                        MOS_PRE_RCV, ApplyActionPerFlow);

The last step of the per-thread initialization is to register timer callback for printing the firewall rule table every second. Note that this function should be registered only for CPU core id 0, to prevent duplicate printing.

/* CPU 0 is in charge of printing stats */
if (ctx->mctx->cpu == 0 &&
        mtcp_settimer(ctx->mctx, ctx->mon_listener, &tv_1sec, DumpFWRuleTable))
...

(3) The ApplyActionPerFlow() Event Callback

The ApplyActionPerFlow() event callback function is used for applying actions for each initial SYN packet of the flows. The following code snippet shows the procedure of retrieving the packet information and looking up the firewall rules. mtcp_getlastpkt() function retrieves the packet information in the variable p, and the application looks up for the action for that flow (defined by the network address and port number).

mtcp_getlastpkt(mctx, msock, side, &p);
...
action = FWRLookup(p.iph->saddr, p.iph->daddr, p.tcph->source, p.tcph->dest);

If the decided action is to drop the packet, the application calls mtcp_setlastpkt() with MOS_DROP keyword. If the decided action is to accept the flow, the application does not need to monitor that flow anymore, and it can stop monitoring the flow by calling mtcp_setsockopt() with MOS_STOP_MON keyword.

if (action == FRA_DROP) {
             mtcp_setlastpkt(mctx, msock, side, 0, NULL, 0, MOS_DROP);
} else {
             assert(action == FRA_ACCEPT);
             /* no need to monitor this flow any more */
             opt = MOS_SIDE_BOTH;
             mtcp_setsockopt(mctx, msock, SOL_MONSOCKET,
             MOS_STOP_MON, &opt, sizeof(opt));
}