Test driving App Firewall with IPTables

With more and more application moving to the cloud, web based applications have become ubiquitous. They are ideal for providing access to applications sitting on the cloud (over HTTP through a standard web browser). This has removed the need to install specialized application on the client system, the client just needs to install is a fairly modern browser.

While this is good for reducing load on the client, the job of the firewall has become much tougher.

Traditionally firewall rules look at the Layer 3 and Layer 4 attributes of a packet to identify a flow and associate it with applications generating the traffic. To a traditional firewall looking at L3/L4 headers all the traffic between the client and different web apps looks like http communication. Without proper classification of traffic flows the firewall is not be able to apply a security policy.

It has now become important to look at the application layer to identify the traffic associated with a web service or web application and enforce effective security and bandwidth allocation policy.

In this blog, I will look at features provided by IPTables that can be used to classify packets by application Layer header and how this can be used to implement security and other network policy.

Looking in to the application layer

IPTables are the de-facto choice for implementing firewall on Linux. It provides extensive packet matching, classification, filtering and many more facilities. Like any traditional firewall the core features of IPTables allow packet matching with Layer3 and Layer4 header attributes. These features as we discussed in the introduction may not be sufficient to differentiate between traffic from various web apps.

While researching for a solution to provide APP Firewall on Linux I came across an IPTables extensions called NFQUEUE.

The NFQUEUE extension provides a mechanism to pass a packet to a user-space program which can run some kind of test on the packet and tell IPTables what action(accept/drop/mark) to perform for the packet.

appfw

This gives a lot of flexibility for the IPTables user to hook up custom tests for the packets before it is allowed to pass through the firewall.

To understand how NFQUEUE can help classify and filter traffic based on application layer headers, let’s try to implement a web app filter providing URL based access control. In this test we will extract the request URL from the HTTP header and the filter will allow access based on this URL

A simple web app

For this experiment, we will use python bottle to deploy two application. Access will be allowed for the first app(APP1) while access to the second app(APP2) will be denied.

We will use the following code to deploy the sample Apps

from bottle import Bottle

app1 = Bottle()
app2 = Bottle()

@app1.route('/APP1/')
def app1_route():
    return 'Access to APP1!\n'


@app2.route('/APP2/')
def app2_route():
    return 'Access to APP2!\n'


if __name__ == '__main__':
    app1.merge(app2)
    app1.run(server='eventlet', host="192.168.121.22", port=8081)

The web application will bind to port 8081 and local IP of 192.168.121.22.

NOTE: we need a eventlet based bottle server else the application hangs after a deny from the app filter(connections are not closed and the next request is not processed)

To access the web apps use the curl commands

curl http://192.168.121.22:8081/APP1/
curl http://192.168.121.22:8081/APP2/

Configuring the IPTables NFQUEUE

The next step is to configure IPTables to forward the client traffic accessing the web apps to our user space web-app filter.

The NFQUEUE IPTables extension works by adding a new target to IPTables called NFQUEUE. This target allows IPTables to put the matching packet on a queue. These packets can then be read from this queue by a filter application in user space. The filter application can then perform custom tests and provide a verdict to allow or deny the packet.

The NFQUEUE extension provides 65535 different queues. It also provides fail-safe options like what action IPTables should take if a queue is created but no filter is attached to it, load balancing of packets across multiple queues. Also, there are knobs in the /proc filesystem to control how much of the packet data will be copied to user space. A complete list of options can be found in the iptables extensions man page

To enable NFQUEUE for the web-app traffic we will add the following rule to IPTables.

iptables -I INPUT -d 192.168.121.22 -p tcp --dport 8081 -j NFQUEUE --queue-num 10 --queue-bypass

The –queue-num option selects the NFQUEUE number to which the packet will be queued. The –queue-bypass option allows the packet to be accepted if no custom filter is attached to queue number 10, without this option if no filter is attached to the queue, packets will be dropped.

Implementing a simple APP filter

With the above IPTables rule the packets destined for our sample web app will be pushed into NFQUEUE number 10.  I am going to use the python bindings for NFQUEUE called nfqueue-bindings to develop the filter. Let’s run a simple print and drop filter.

#!/usr/bin/python

# need root privileges

import struct
import sys
import time

from socket import AF_INET, AF_INET6, inet_ntoa

import nfqueue
from dpkt import ip


def cb(i, payload):
    print "python callback called !"
    payload.set_verdict(nfqueue.NF_DROP)
    return 1

def main():
    q = nfqueue.queue()
    
    print "setting callback"
    q.set_callback(cb)
    
    print "open"
    q.fast_open(10, AF_INET)
    q.set_queue_maxlen(50000)
    
    print "trying to run"
    try:
    	q.try_run()
    except KeyboardInterrupt, e:
    	print "interrupted"
    
    print "unbind"
    q.unbind(AF_INET)
    print "close"
    q.close()

if __name__ == '__main__':
    main()

Now we have tested that the packets trying to access our web app are passing through a app filter implemented in user space. Next we need to unpack the packet and look at the HTTP header to extract the URL that the user is trying to access. For unpacking the headers we will use python dpkt library. The following code will let us access to APP1 and deny access to APP2

#!/usr/bin/python

# need root privileges

import struct
import sys
import time

from socket import AF_INET, AF_INET6, inet_ntoa

import nfqueue
import dpkt
from dpkt import ip

count = 0

def cb(i, payload):
    global count
    
    count += 1
    
    data = payload.get_data()

    pkt = ip.IP(data)
    if pkt.p == ip.IP_PROTO_TCP:
        # print "  len %d proto %s src: %s:%s    dst %s:%s " % (
        #        payload.get_length(),
        #        pkt.p, inet_ntoa(pkt.src), pkt.tcp.sport,
        #        inet_ntoa(pkt.dst), pkt.tcp.dport)
        tcp_pkt = pkt.data
        app_pkt = tcp_pkt.data
        try:
            request = dpkt.http.Request(app_pkt)
            if "APP1" in request.uri:
                print "Allowing APP1"
                payload.set_verdict(nfqueue.NF_ACCEPT)
            elif "APP2":
                print "Denying APP2"
                payload.set_verdict(nfqueue.NF_DROP)
            else:
                print "Denying by default"
                payload.set_verdict(nfqueue.NF_DROP)
        except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
            pass
    else:
        print "  len %d proto %s src: %s    dst %s " % (
               payload.get_length(), pkt.p, inet_ntoa(pkt.src), 
               inet_ntoa(pkt.dst))


    sys.stdout.flush()
    return 1

def main():
    q = nfqueue.queue()

    print "setting callback"
    q.set_callback(cb)

    print "open"
    q.fast_open(10, AF_INET)

    q.set_queue_maxlen(50000)

    print "trying to run"
    try:
        q.try_run()
    except KeyboardInterrupt, e:
        print "interrupted"

    print "%d packets handled" % count

    print "unbind"
    q.unbind(AF_INET)
    print "close"
    q.close()
    
if __name__ == '__main__':
    main()

Here are the result of the test on the client

result1

The output from the filter on the firewall

result2

What else can be done with App based traffic classification

Firewall is just one use-case of the advance packet classification. With the flows identified and associated to different applications we can apply different routing and forwarding policy. NFQUEUE based filter can be used to set different firewall marks on the classified packets. The firewall marks can then be used to implement policy based routing in Linux.

Advertisements

Rate Limiting ACT broadband on Ubuntu

ISPs have started to provide high bandwidth connections while the FUP (Fair Usage Policy) limit is still not enough (I am using ACT Broadband). Once you decide to be on youtube most of the time the download limit gets exhausted rather quickly.

As I use Ubuntu for my desktop, I decided to use TC to throttle my Internet bandwidth to bring in some control over my Internet bandwidth usage. Have a look at my previous posts about rate limiting and  traffic shaping on Linux to learn about usage of TC.

Here is my modest network setup at home.

Slide1

The problem is that TC can throttle traffic going out on an interface but traffic shaping will not impact the download bandwidth.

The Solution

To get around this problem I introduced a Linux network namespace into the topology. Here is how the topology looks now.

Slide2

I use this script to setup the upload/download bandwidth limit.

Results

Here are readings before and after applying the throttle

Before

media-20160302

After rate-limiting to 1024Kbps upload and download

media-20160302-1.png

Visualizing KVM (libvirt) network connections on a Hypervisor

At times I have found the need to visualize the network connectivity of KVM instances on a Hypervisor Host. I normally use libvirt and KVM for launching my VM workloads. In this post we will look at a simple script that can parse the information available with libvirt and the host kernel to plot the network connectivity between the KVM instances. The script can parse Linux Bridge and OVS based switches.

It can generate a GraphViz based topology rendering (requires pygraphviz), can use networkx and d3js to produce a webpage which is exposed using a simple webserver or just a json output describing the network graph.

The source of the script is available here .

The following is a sample output of my hypervisor host.

SVG using d3js
d3_svg

PNG using GraphViz
gviz

Json text
json_net

Fullscreen display for Core Linux on VirtualBox

The default screen resolution for Core Linux on VirtualBox is 1024×768 and does not provide a good screen area usage.

defaultI came across this post on the Core Linux forum that provides steps to set the correct screen resolution. I am capturing the steps in this post for my reference, all credits are due to the original poster.

I found 1440×762 to be a better resolution if you are going to use VirtualBox in window mode, while 1440×900 is better suited for full-screen mode. In the following command “linux” is the name of my VM.

VBoxManage setextradata "linux" "VBoxInternal2/UgaHorizontalResolution" 1440
VBoxManage setextradata "linux" "VBoxInternal2/UgaVerticalResolution" 762
VBoxManage setextradata "linux" "CustomVideoMode1" 1440x762x24
VBoxManage setextradata "linux" "GUI/CustomVideoMode1" 1440x762x24

Once done you need to update the .xsession file in your home directory with the proper resolution

/usr/local/bin/Xvesa -br -screen 1440x762x24 ...

Now reboot the VM to activate the new resolution or you can exit to the prompt and restart the Window Manager with “startx” command

Here is the result after the configuration

custom

Bitmap resource pool


Bitmap resource pool

class BitMapPool:

    def __init__(self, start, end, bitmap=[]):
        self.start = start
        self.end = end
        if bitmap == []:
            bitmap = ['0' for i in range(0, end)]
        self.bitmap = bitmap

    def allocate(self):

        for i in range(self.start, self.end):
            if self.bitmap[i] == '0':
                self.bitmap[i] = '1'
                return i
        return None

    def free(self, i):
        if self.bitmap[i] == '1' and i in range(self.start, self.end):
            self.bitmap[i] = '0'

    def free_pool_size(self):
        count = 0
        for i in range(self.start, self.end):
            if self.bitmap[i] == '0':
                count += 1
        return count

def main():

    pool = BitMapPool(200, 400)
    for i in range(1, 100):
        res = pool.allocate()
        print "Res: %s \nBitmap: %s" % (res, ''.join(pool.bitmap))

    print "Free pool size: %s" % pool.free_pool_size()

    for i in range(250, 300):
        pool.free(i)
        print "Freeing %s" % i

    print "Free pool size: %s" % pool.free_pool_size()

    for i in range(1, 40):
        res = pool.allocate()
        print "Res: %s \nBitmap: %s" % (res, ''.join(pool.bitmap))

    print "Free pool size: %s" % pool.free_pool_size()
    _bitmap = pool.bitmap
    pool = BitMapPool(200, 400, _bitmap)
    for i in range(1, 30):
        res = pool.allocate()
        print "Res: %s \nBitmap: %s" % (res, ''.join(pool.bitmap))

    print "Free pool size: %s" % pool.free_pool_size()

if __name__ == '__main__':
    main()

Run Length Encoding

from itertools import groupby
import json

def encode(input_string):
    return [[len(list(g)), k] for k, g in groupby(input_string)]

def decode(lst):
    return ''.join(c * n for n, c in lst)

_str = ('000000000000000000000000000000000000000000000000000000000000000000000'
        '000000000000000000000000000000000000000000000000000000000000000000000'
        '000000000000000000000000000000000000000000000000000000000000001111111'
        '111111111111111111111111111111111111111111111111111111111111111111111'
        '111111111111111111111111111111111111111110000000000000000000000000000'
        '0000000000000000000000000000000000000000000000000000000')

#worst case
#_str = '10'*200

_enc = encode(_str)
_dec = decode(_enc)

print "Encoded:", json.dumps(_enc), 'Length:', len(json.dumps(_enc))
print "Decoded:", _dec, 'Length:', len(_dec)

#worst case
_str = '10' * 200

_enc = encode(_str)
_dec = decode(_enc)

print "Worst case Encoded:", _enc, 'Length:', len(json.dumps(_enc))
print "Worst case Decoded:", _dec, 'Length:', len(_dec)

OUTPUT

Encoded: [[200, "0"], [117, "1"], [83, "0"]] Length: 35
Decoded: 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000 Length: 400
Worst case Encoded: [[1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0'], [1, '1'], [1, '0']] Length: 4000
Worst case Decoded: 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 Length: 400