by Ivan Skytte Jørgensen, April 2013

Implementing fair-use using Linux iptables and tc traffic-control

Introduction

You may have read about my non-trivial network setup. If not, then do so now.

Some months ago I noticed that some of my neighbours were using my access point (which is fine), but they used it quite heavily - around 150GB/month. That was a bit too close to the fair-use limit I have on my cable connection.

I considered re-activating the shaping rules, but then I would have to identify the MAC address of them etc. Instead I chose to implement fair-use on the access point.

Counting bytes

First I had to get the byte counters for the users. Because I don't know the users I could use the MAC address to identify them, but since most of them are not technically adept to get a new IP-address from my DHCP server the IP-address is enough to identify the user (and much easier).

The easiest way to count bytes is to simply use iptables to do it. I modified my firewall script to include this:

#per-user counters
iptables -N vlan61_output
ip=0
while [ $ip -le 255 ]; do
        iptables -A vlan61_output -o vlan61 -d 10.0.61.$ip
        ip=$[ $ip + 1 ]
done
iptables -A FORWARD -j vlan61_output
iptables -N vlan61_input
ip=0
while [ $ip -le 255 ]; do
        iptables -A vlan61_input -i vlan61 -s 10.0.61.$ip
        ip=$[ $ip + 1 ]
done
iptables -A FORWARD -j vlan61_input

I then made a script, record_vlan61_usage.sh, which parsed they output of iptables -vn -L vlan61_output:

Chain vlan61_output (1 references)
 pkts bytes target     prot opt in     out     source               destination         
...
    0     0            all  --  *      vlan61  0.0.0.0/0            10.0.61.239         
 9958 6240K            all  --  *      vlan61  0.0.0.0/0            10.0.61.240         
 899K 1143M            all  --  *      vlan61  0.0.0.0/0            10.0.61.241         
 363K  248M            all  --  *      vlan61  0.0.0.0/0            10.0.61.242         
    0     0            all  --  *      vlan61  0.0.0.0/0            10.0.61.243         
...

and puts the input/output byte counters into a file in /var/lib/fair_use:

...
2013-04-10 0
2013-04-11 0
2013-04-12 90177536
2013-04-13 1288192
2013-04-14 233472
2013-04-15 32505856
2013-04-16 0
2013-04-17 4184064
2013-04-18 13631488

The script is run just after midnight every day.

Checking fair-use limits and shaping traffic

I then made a script, check_fair_use.sh, which iterates over the IP-addresses counting how much has been transferred in the last month and week. Then it determines the tier/class of the user:

#determine bandwidth class from usage
#input: ip (10.0.61.xxx)
#output: tier1/tier2/tier3/tier4
determine_ip_tier() {
        IP="$1"
        month_bytes=`count_total_octets $IP 30`
        if [ $month_bytes -ne 0 ]; then
                week_bytes=`count_total_octets $IP 7`
        else
                week_bytes=0
        fi

        if [ "$month_bytes" -eq 0 ]; then
                echo "tier1"
                return
        fi

        if [ "$month_bytes" -lt 20000000000 ]; then
                #tier 1 unless >=7GB this week
                if [ "$week_bytes" -le 7000000000 ]; then
                        echo "tier1"
                else
                        echo "tier2"
                fi
        elif [ "$month_bytes" -lt 40000000000 ]; then
                echo "tier2"
        elif [ "$month_bytes" -lt 50000000000 ]; then
                echo "tier3"
        elif [ "$month_bytes" -lt 60000000000 ]; then
                echo "tier4"
        elif [ "$month_bytes" -lt 80000000000 ]; then
                echo "tier5"
        else
                echo "tier6"
        fi
}

The tier is then mapped to bandwidth

process_ip() {
        IP="$1"
        tier=`determine_ip_tier $IP`
        case "$tier" in
                tier1) #full speed
                        output_speed=100mbit
                        input_speed=100mbit
                        ;;
                tier2) #10Mbps/1Mbps
                        output_speed=10mbit
                        input_speed=1mbit
                        ;;
                tier3) #5Mbps/512Kbps
                        output_speed=5mbit
                        input_speed=512kbit
                        ;;
                tier4) #5Mbps/512Kbps
                        output_speed=3mbit
                        input_speed=384kbit
                        ;;
                tier5) #2MBps/128Kbps
                        output_speed=2mbit
                        input_speed=256kbit
                        ;;
                tier6) #1MBps/128Kbps
                        output_speed=1mbit
                        input_speed=128kbit
                        ;;
                *)
                        return
        esac
        echo "$output_speed"
}

The output (speed) is then used to generate a tc script:

generate_tc_script() {
        echo "tc qdisc del dev vlan61 root handle 1:0"
        echo "tc qdisc add dev vlan61 root handle 1:0 htb default 500"
        echo "tc class add dev vlan61 parent 1:0 classid 1:1 htb rate 100mbit burst 500k cburst 500k"
        echo " tc class add dev vlan61 parent 1:1 classid 1:500 htb rate  384kbit ceil 100mbit burst 500k cburst 500k"
        ip4=$IP_START
        while [ $ip4 -le 254 ]; do
                output_speed=`process_ip "10.0.61.$ip4"`
                echo " tc class add dev vlan61 parent 1:1 classid 1:$ip4 htb rate  384kbit ceil $output_speed burst 500k cburst 500k"
                ip4=$[ $ip4 + 1 ]
        done
        ip4=$IP_START
        while [ $ip4 -le 254 ]; do
                echo "tc filter add dev vlan61 protocol ip parent 1:0 prio 1 u32 match ip dst 10.0.61.$ip4 classid 1:$ip4"
                ip4=$[ $ip4 + 1 ]
        done
}

generate_tc_script > /tmp/tc_script || exit
chmod +x /tmp/tc_script
/tmp/tc_script

The resulting script looks something like this:

tc qdisc del dev vlan61 root handle 1:0
tc qdisc add dev vlan61 root handle 1:0 htb default 500
tc class add dev vlan61 parent 1:0 classid 1:1 htb rate 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:500 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:2 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:3 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:4 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
...
 tc class add dev vlan61 parent 1:1 classid 1:190 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:191 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:192 htb rate  384kbit ceil 10mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:193 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:194 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:195 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:196 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:197 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:198 htb rate  384kbit ceil 1mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:199 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
 tc class add dev vlan61 parent 1:1 classid 1:200 htb rate  384kbit ceil 100mbit burst 500k cburst 500k
...

Purging usage records

Never forget to clean up potentially ever-growing files. I purge records older than 2 months in a script:

#!/bin/bash

#purges records older than 2 months

DATA_DIR=/var/lib/fair_use
DAYS_TO_KEEP=62

for file in $DATA_DIR/*.{input,output}; do
        lines=`wc -l <$file`
        if [ "$lines" -gt $DAYS_TO_KEEP ]; then
                tmpfile=/tmp/$$.tmp
                tail -$DAYS_TO_KEEP <$file >$tmpfile && cp $tmpfile $file
                rm $tmpfile
        fi
done

This script is run from crontab once a month.

Conclusion

It is possible to implement (simple) fair-use using iptables and traffic-control. It is vulnerable to IP-address change, it doesn't really identify the user, only download speed is throttled, it doesn't handle when one of my two upstream links dies for extended period. But it suits my needs so far.

Note: My former employer develops, among other things, fair-use solutions for mobile operators and ISPs. The above scripts have nothing to do with what they sell. First of all, they don't use iptables/tc to do this. They integrate to probes from Procera, Ipoque, Allot, Cisco, Sandvine... using 3GPP Gx/Gy signalling or proprietary protocols. Secondly, they have much more advanced and flexible configuration of users/class/tiers/shaping/protools/"packages"/... than the scripts above provide.

The scripts