6.2. Filter

6.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.

6.2.2. Basic TCP/IP filtering

Basic IP filtering

The directory <haka_install_path>/share/haka/sample/filter/ contains all the example used in this tutorial.

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

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

-- drop all packets originating from
-- blacklisted address 192.168.10.10
haka.rule{
    -- This rule is applied on each ip incoming packet
    hook = ipv4.events.receive_packet,
    eval = function (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
            -- Raise 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 ran with a pcap file provided in the sample directory.

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

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 ipfilter.lua trace.pcap -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 Haka script file. A script containing a lua error is provided as tcpfilter.lua.

This script will authorize only packets from/to port 80.

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

require('protocol/ipv4')
local tcp = require('protocol/tcp')

-- Allow only packets to/from port 80
haka.rule{
    hook = tcp.events.receive_packet,
    eval = function (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("Authorizing trafic on port 80")
        else
            haka.log("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 tcfilter.lua tcpfilter.pcap

When running this script, 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 --debug-lua at the end of the command line:

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

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

   entering debugger: unknown field 'destport'
    thread: 0
    Backtrace
    =>0     [C]: in function '(null)'
      #1    [C]: in function '(null)'
      #2    [C]: in function '__index'
      #3    tcpfilter.lua:18: in function 'signal'
      #4    /usr/share/haka/core/events.bc:0: in the main chunk
      #5    /usr/share/haka/core/events.bc:0: in the main chunk
      #6    /usr/share/haka/core/context.bc:0: in the main chunk
      #7    [string "tcp"]:14: in function 'receive'
      #8    /usr/share/haka/core/dissector.bc:0: in the main chunk
      #9    [C]: in function 'xpcall'
     ...

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

Here we are interested in the third frame 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
      14:      hook = haka.event('tcp', 'receive_packet'),
      15:      eval = function (pkt)
      16:          -- The next line will generate a lua error:
      17:          -- there is no 'destport' field. replace 'destport' by 'dstport'
      18=>         if pkt.destport == 80 or pkt.srcport == 80 then
      19:              haka.log("Filter", "Authorizing trafic on port 80")
      20:          else
      21:              haka.log("Filter", "Trafic not authorized on port %d", pkt.dstport)
      22:              pkt:drop()
      23:          end

We now see that Lua is complaining about an unknown 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 {
              ack_seq : 0
              checksum : 417
              dstport : 80
              flags : userdata tcp_flags {
                ack : false
                all : 2
                cwr : false
                ecn : false
                fin : false
                psh : false
                rst : false
                syn : true
                urg : false
              }
              hdr_len : 40
              ip : userdata ipv4 {
                  ...
              }
              ...
              srcport : 37542
              urgent_pointer : 0
              window_size : 14600
            }

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.

A rule group has 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 final function is called after all rules have been ran. 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 and port 22.

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

------------------------------------
-- Security rule group
------------------------------------

-- 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",
    hook = tcp_connection.events.new_connection,
    init = function (flow, pkt)
        haka.log.debug("Entering packet filtering rules : %d --> %d",
            pkt.srcport, pkt.dstport)
    end,

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

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

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

my_group:rule{
    eval = function (flow, pkt)
        if pkt.dstport == 80 then
            haka.log("Authorizing traffic on port 80")
            return true
        end
    end
}

my_group:rule{
    eval = function (flow, pkt)
        if pkt.dstport == 22 then
            haka.log("Authorizing traffic on port 22")
            return true
        end
    end
}

6.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"

[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 Haka script file used here is the one from the first tutorial. This filter will discard 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 tcp_connection = require('protocol/tcp_connection')
local http = require('protocol/http')

-- Allow only connections on port 80, close all other connections
-- Forward all accepted connections to the HTTP dissector
haka.rule{
    hook = tcp_connection.events.new_connection,
    eval = function (flow, pkt)
        if pkt.dstport == 80 then
            http.dissect(flow)
        else
            haka.log("Dropping TCP connection: tcp dstpport=%d",
                pkt.dstport)
            pkt:reset() -- Send a TCP RST packet to both sides: client and server
        end
    end
}

-- Allow only connections from the 'Mozilla' user agent.
haka.rule{
    hook = http.events.request,
    eval = function (http, request)
        if string.match(request.headers['User-Agent'], 'Mozilla') then
            haka.log("User-Agent Mozilla detected")
        else
            haka.log("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 alter packet content 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
http.install_tcp_rule(80)

-------------------------------------
-- Rule group definition
-------------------------------------
safe_update = haka.rule_group{
    hook = http.events.response,

    -- Initialization
    init = function (http, response)
        local host = http.request.headers['Host']
        if host then
            haka.log("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 (ret)
        return not ret
    end
}

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

-- If the User-Agent contains firefox and the version is outdated,
-- then redirect the traffic to firefox_web_site
safe_update:rule{
    eval = function (http, response)
        -- Uncomment the following line to see the the content of the request
        -- debug.pprint(request, nil, nil, { debug.hide_underscore, debug.hide_function })

        local UA = http.request.headers["User-Agent"] or "No User-Agent header"
        haka.log("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
                response.status = "307"
                response.reason = "Moved Temporarily"
                response.headers["Content-Length"] = "0"
                response.headers["Location"] = firefox_web_site
                response.headers["Server"] = "A patchy server"
                response.headers["Connection"] = "Close"
                response.headers["Proxy-Connection"] = "Close"
                debug.pprint(response, nil, nil, { debug.hide_underscore, debug.hide_function })
            end
        else
            haka.log("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