5.2. Filter

5.2.1. Introduction

This chapter will document how to use haka to filter packets and streams according to their different fields.

This tutorial is divided into two parts. The first part will use hakapcap and a pcap file to do some basic, offline, filtering. The second part will use nfqueue to alter tcp streams as they pass through haka.

5.2.2. Basic TCP/IP filtering

Basic IP filtering

The directory <haka_install_path>/share/haka/sample/filter/ contains all the example files for this section.

A basic filter script on IP fields is provided as ipfilter.lua:

------------------------------------
-- Loading dissectors
------------------------------------

-- Load the ipv4 dissector to filter on IPV4 fields
local ipv4 = require('protocol/ipv4')

------------------------------------
-- Detects packets originating from 192.168.10.10
--     * Log an alert
--     * Drop the packet
------------------------------------
haka.rule{
    -- This rule is applied on each IP incoming packet
    hooks = { 'ipv4-up' },
    eval = function (self, pkt)
        -- Parse the IP address and assign it to a variable
        local bad_ip = ipv4.addr('192.168.10.10')
        if pkt.src == bad_ip then
            -- Log an alert
            haka.alert{
                description = string.format("Filtering IP %s", bad_ip),
                severity = 'low'
            }
            -- and drop the packet
            pkt:drop()
        end
        -- All other packets will be accepted
    end
}

This script can be run with a pcap file provided in the sample directory.

$ cd <haka_install_path>/share/haka/sample/filter/
$ hakapcap ipfilter.pcap ipfilter.lua

To create a pcap file containing all packets that were not dropped, run the following command:

$ cd <haka_install_path>/share/haka/sample/filter/
$ hakapcap trace.pcap ipfilter.lua -o output.pcap

The resulting pcap file can be opened with wireshark to check that haka correctly filtered the packets according to their IP source address.

Interactive rule debugging

Haka allows to interactively debug lua rules. A script containing a lua error is provided as tcpfilter.lua.

This script will filter TCP packets, only allowing packets to/from port 80 to pass through.

------------------------------------
-- Loading dissectors
------------------------------------
require('protocol/ipv4')
require('protocol/tcp')

------------------------------------
-- Only allow packets to/from port 80
------------------------------------

haka.rule{
    -- The hooks tells where this rule is applied. Only TCP packets will be
    -- intecepted by this rule. Other protocol will flow.
    hooks = { 'tcp-up' },
    eval = function (self, pkt)
        -- The next line will generate a lua error:
        -- there is no 'destport' field. replace 'destport' by 'dstport'
        if pkt.destport == 80 or pkt.srcport == 80 then
            haka.log("Filter", "Authorizing trafic on port 80")
        else
            haka.log("Filter", "Trafic not authorized on port %d", pkt.dstport)
            pkt:drop()
        end
    end
}

To run this example, use the following commands:

$ cd <haka_install_path>/share/haka/sample/filter/
$ hakapcap tcpfilter.pcap tcfilter.lua

When this script is run, haka will output a high number of errors, complaining that the field destport doesn’t exist. We will use haka’s debug facilities to find out precisely where the error occurs.

See also

Debugging contains documentation on haka’s debugging facilities.

To start haka in debugging mode, add --luadebug at the end of the command line:

$ cd <haka_install_path>/share/haka/sample/filter/
$ hakapcap tcpfilter.pcap tcpfilter.lua --luadebug

When a lua error code occurs, the debugger breaks and outputs the error and a backtrace.

entering debugger: unknown field 'destport'
Backtrace
=>0    [C]: in function '(null)'
 #1    [C]: in function '(null)'
 #2    [C]: in function '__index'
 #3    tcpfilter.lua:21: in function 'eval'
 #4    /opt/haka/share/haka/core/rule.bc:0: in the main chunk
 #5    /opt/haka/share/haka/core/rule.bc:0: in the main chunk
 #6    /opt/haka/share/haka/core/rule.bc:0: in the main chunk
 #7    [C]: in function 'xpcall'
 #8    /opt/haka/share/haka/core/rule.bc:0: in the main chunk
...

The general syntax of the debugger is close to the syntax of gdb.

Here we are interested in the third frames which is the one in the lua script itself.

To set the debugger to focus on that particular frame, type frame 3. We can now use the list command to display the faulty source code:

debug>  list
  16:  hooks = { 'tcp-up' },
  17:      eval = function (self, pkt)
  18:          -- The next line will generate a lua error:
  19:          -- there is no 'destport' field. replace 'destport' by 'dstport'
  20=>         if pkt.destport == 80 or pkt.srcport == 80 then
  21:              haka.log("Filter", "Authorizing trafic on port 80")
  22:          else
  23:              haka.log("Filter", "Trafic not authorized on port %d", pkt.dstport)
  24:              pkt:drop()
  25:          end

We now see that lua is complaining about an unknonw field destport on the line testing the destination port of the packet.

Packets, like all structures provided by haka, can be printed easily using the debugger.

To see the content of the packet, type print pkt:

debug>  print pkt
  #1  userdata tcp {
          ...
        checksum : 417
        res : 0
        next_dissector : "tcp-connection"
        srcport : 37542
        payload : userdata tcp_payload
        ip : userdata ipv4 {
          ...
        }
        flags : userdata tcp_flags {
          ...
        }
        ack_seq : 0
        seq : 3827050607
        dstport : 80
        hdr_len : 40
    }

You can notice that there is no field called destport. The correct name for the field is dstport. Once this typo is corrected, the script will run properly

Press CTRL-C to quit or type help to get the list of available commands.

Using rule groups

Haka can handle multiple rules as a group.

Rule groups have three functions:

  • The init function is called before any rule from the group is applied
  • The continue function is called between each rule of the group and can decide to stop processing the group at any point.
  • The fini function is called after all rules have been run. It is not called if continue has forced a cancelation mid-group.

The following example uses the concept of group to implement a simple filter that only accepts connections on port 80 or port 22.

------------------------------------
-- Loading dissectors
------------------------------------
require('protocol/ipv4')
require('protocol/tcp')

------------------------------------
-- Security group
------------------------------------
-- This group implements an logical 'or' between rules
-- Each rule returns a boolean,
--
-- if any rule return 'true' we leave the rule group
-- if all rules return 'false' the packet is droped


-- create a new group with no rules in it
-- The 'my_group' lua variable will be used to add rules to the group
local my_group = haka.rule_group{
    name = "my_group",
    init = function (self, pkt)
        haka.log.debug("filter", "Entering packet filtering rules : %d --> %d",
            pkt.tcp.srcport, pkt.tcp.dstport)
    end,

    -- rules will return a boolean
    -- if the boolean is false, we continue
    -- if the boolean is true, we finish the group immediately
    continue = function (self, pkt, ret)
        return not ret
    end,

    -- if we reach the fini function, all rules have returned false
    -- Nobody has accepted the packet => we drop it.
    fini = function (self, pkt)
        haka.alert{
            description = "Packet dropped : drop by default",
            targets = { haka.alert.service("tcp", pkt.tcp.dstport) }
        }
        pkt:drop()
    end
}

------------------------------------
-- Security rules for my_group
------------------------------------

-- return true if connection is on port 80
my_group:rule{
    hooks = { 'tcp-connection-new' },
    eval = function (self, pkt)
        if pkt.tcp.dstport == 80 then
            haka.log("Filter", "Authorizing traffic on port 80")
            return true
        end
    end
}

-- return true if connection is on port 22
my_group:rule{
    hooks = { 'tcp-connection-new' },
    eval = function (self, pkt)
        if pkt.tcp.dstport == 22 then
            haka.log("Filter", "Authorizing traffic on port 22")
            return true
        end
    end
}

5.2.3. Advanced TCP/IP Filtering

Filtering with NFQueue

All the examples so far have used hakapcap to test some recorded packets.

Haka can also use nfqueue to capture packets from a live interface. The following examples will illustrate how to do that.

When configured to use nfqueue, haka will hook itself up to the raw nfqueue table in order to inspect, modify, create and delete packets in real time.

The rest of this tutorial assumes that the haka package is installed on a host which has a network interface named eth0.

The configuration file for the daemon is given below:

[general]
# Select the haka configuration file to use
configuration = "ipfilter.lua"

# Optionally select the number of thread to use. 
#thread = 4

# Pass-through mode
# If yes, haka will only inspect packet
# If no, it means that haka can also modify and create packet
pass-through = no

[packet]
# Select the capture model, nfqueue or pcap
module = "packet/nfqueue"

# Select the interfaces to listen to
interfaces = "eth0"

# Select packet dumping for nfqueue
#dump = yes
#dump_input = "/tmp/input.pcap"
#dump_output = "/tmp/output.pcap"
#dump_drop = "/tmp/drop.pcap"

[log]
# Select the log module
module = "log/syslog"

[alert]
# Select the alert module
module = "alert/syslog"

In order to be able to capture packets, the haka daemon needs to be run as root. The –no-daemon` option will prevent haka from detaching from the command line and will force haka to send its outputs to stdout instead of syslog.

# cd <haka_install_path>/share/haka/sample/filter/
# haka -c daemon.conf --no-daemon

The lua file used here is the one from the first tutorial. This filter will refuse all packets coming from 192.168.10.10

HTTP filtering

Haka comes with an HTTP parser. Using that module it is easy to filter packets using specific fields from HTTP headers.

------------------------------------
-- Loading dissectors
------------------------------------
require('protocol/ipv4')
require('protocol/tcp')
local http = require('protocol/http')

------------------------------------
-- Only allow connections on port 80, close all other connections
-- Forward all accepted connections to the HTTP dissector
------------------------------------
haka.rule{
    hooks = { 'tcp-connection-new' },
    eval = function (self, pkt)
        if pkt.tcp.dstport == 80 then
            pkt.next_dissector = 'http'
        else
            haka.log("Filter", "Dropping TCP connection: tcp dstpport=%d",
                pkt.tcp.dstport)
            pkt:reset() -- Send a TCP RST packet to both sides: client and server
        end
    end
}

-- Only allow connections from from the 'Mozilla' user agent.
haka.rule{
    hooks = { 'http-request' },
    eval = function (self, http)
        if string.match(http.request.headers['User-Agent'], 'Mozilla') then
            haka.log("Filter", "User-Agent Mozilla detected")
        else
            haka.log("Filter", "Only Mozilla User-Agent authorized")
            http:drop()
        end
    end
}

To test this filter you will need to modify dameon.conf to tell haka to use httpfilter.lua:

[general]
# Select the haka configuration file to use.
configuration = "httpfilter.lua"

It is now possible to start the daemon using this new configuration.

# cd <haka_install_path>/share/haka/sample/filter/
# haka -c dameon.conf --no-daemon

Modifying HTTP responses

Haka can also be used to modify packets as they pass through the system. The following example will redirect traffic based on HTTP headers.

More specifically, it will detect outdated firefox versions and will redirect all traffic from these browsers to the firefox update site.

------------------------------------
-- Loading dissectors
------------------------------------
require('protocol/ipv4')
require('protocol/tcp')
local http = require('protocol/http')

local last_firefox_version = 24.0
local firefox_web_site = 'http://www.mozilla.org'

-------------------------------------
-- Domain whitelist,
-- all traffic to these domains will be unmodified
-------------------------------------
local update_domains = {
    'mozilla.org',
    'mozilla.net',
    -- You can extend this list with other domains
}

-------------------------------------
-- Forward all traffic on port 80 to the HTTP dissector
-------------------------------------
haka.rule{
    hooks = { 'tcp-connection-new' },
    eval = function (self, pkt)
        if pkt.tcp.dstport == 80 then
            pkt.next_dissector = 'http'
        end
    end
}

-------------------------------------
-- Rule group implementing a logical 'or'
-------------------------------------
safe_update = haka.rule_group{
    name = 'safe_update',

    -- Initialization
    init = function (self, http)
        local host = http.request.headers['Host']
        if host then
            haka.log("Filter", "Domain requested: %s", host)
        end
    end,

    -- Continue is called after evaluation of each security rule the ret
    -- parameter decide whether to read next rule or skip the evaluation of
    -- the other rules in the group
    continue = function (self, http, ret)
        return not ret
    end
}


-- Traffic to all websites in the whitelist is unconditionnally allowed
safe_update:rule{
    hooks = { 'http-response' },
    eval = function (self, http)
        local host = http.request.headers['Host'] or ''
        for _, dom in ipairs(update_domains) do
            if string.find(host, dom) then
                haka.log("Filter", "Update domain: go for it")
                return true
            end
        end
    end

}

-- If the User-Agent contains firefox and the version is outdated
-- the redirect the traffic to firefox_web_site
safe_update:rule{
    hooks = { 'http-response' },
    eval = function (self, http)
        -- Uncomment the following line to see the the content of the request
        -- http.request:dump()

        local UA = http.request.headers["User-Agent"] or "No User-Agent header"
        haka.log("Filter", "UA detected: %s", UA)
        local FF_UA = (string.find(UA, "Firefox/"))

        if FF_UA then -- Firefox was detected
            local version = tonumber(string.sub(UA, FF_UA+8))
            if not version or version < last_firefox_version then
                haka.alert{
                    description= "Firefox is outdated, please upgrade",
                    severity= 'medium'
                }
                -- redirect browser to a safe place where updates will be made
                http.response.status = "307"
                http.response.reason = "Moved Temporarily"
                http.response.headers["Content-Length"] = "0"
                http.response.headers["Location"] = firefox_web_site
                http.response.headers["Server"] = "A patchy server"
                http.response.headers["Connection"] = "Close"
                http.response.headers["Proxy-Connection"] = "Close"
                -- dump the response for illustrative purpose
                http.response:dump()
            end
        else
            haka.log("Filter", "Unknown or missing User-Agent")
        end
    end
}

Modify the dameon.conf in order to load the httpmodif.lua configuration file:

[general]
# Select the haka configuration file to use
configuration = "httpmodif.lua"

And start it.

# cd <haka_install_path>/share/haka/sample/filter/
# haka -c dameon.conf --no-daemon