среда, 17 сентября 2008 г.

Userspace routing policy

Задача: построить маршрутизатор, который пропускал бы хосты в некоторую сеть (например, в интернет) на основании т.н. "билетов". В билете указывается максимальный объем входящего трафика, по исчерпании которого билет изымается и доступ запрещается, однако на тот же хост через некоторое время можно выписать новый билет.

Я не сомневаюсь в том, что эту задачу можно решить средствами готовых биллинговых систем - из свободных первым в голову приходит NeTAMS. Но слишком уж все они монсторобразны для такой простой задачи. Попробуем решить ее на коленке с помощью connexion по мотивам следующего примера.

Устанавливаем все необходимое:
# apt-get install python-module-cxnet python-module-MySQLdb MySQL-server
# service mysqld start
Создаем базу данных и таблицу, в которой будем хранить билеты, выписываем первый билет:
$ mysql -u root
...
mysql> create database cx;
Query OK, 1 row affected (0.00 sec)
mysql> use cx;
Database changed
mysql> create table tickets (
->         id        serial,
->         host      varchar(50),
->         bytes     bigint,
->         max_bytes bigint,
->         enabled   boolean
-> );
Query OK, 0 rows affected (0.01 sec)
mysql> insert into tickets(host, bytes, max_bytes, enabled) values ('192.168.100.103', 0, 100, true);
Query OK, 1 row affected (0.00 sec)
mysql> select * from tickets;
+----+-----------------+-------+-----------+---------+
| id | host            | bytes | max_bytes | enabled |
+----+-----------------+-------+-----------+---------+
|  1 | 192.168.100.103 |      0|       100 |       1 |
+----+-----------------+-------+-----------+---------+
1 row in set (0.00 sec)
Входящие пакеты для хоста 192.168.100.103 завернем в userspace следующим образом:
# modprobe ip_queue
# iptables -A FORWARD -p icmp -d 192.168.100.103 -j QUEUE
А обрабатывать эти пакеты и решать, пропустить или нет, мы будем следующей программой, использующей один из компонентов connexion - библиотеку cxnet:
#!/usr/bin/python

from cxnet.netlink.ipq import *
from cxnet.ip4 import *
from cxnet.utils import *
import MySQLdb

db = MySQLdb.connect(host="localhost", user="root", passwd="", db="cx")
cursor = db.cursor()

ipqs = ipq_socket()

while True:
  (l,msg) = ipqs.recv()
  header = iphdr.from_address(addressof(msg.data.payload))
  if header.daddr>0:
    host = int_to_dqn(header.daddr)
    size = header.tot_len
    print "packet: %s:%s" % (host,size)
    cursor.execute("update tickets set bytes = bytes+%s where enabled = true and host = %s", (size, host))
    cursor.execute("update tickets set enabled = false where enabled = true and bytes >= max_bytes")
    cursor.execute("select count(*) from tickets where enabled = true and host = %s", host)
    for record in cursor.fetchall():
      print "count: %s" % record[0]
      if record[0] > 0:
        ipqs.verdict(msg.data.packet_id, NF_ACCEPT)
        continue
    ipqs.verdict(msg.data.packet_id, NF_DROP)
Теперь отправляем первый пинг с хоста 192.168.100.103 наружу:
$ ping -c 1 192.168.1.1
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=1.12 ms

--- 192.168.1.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.121/1.121/1.121/0.000 ms
При этом билет изменится следующим образом:
mysql> select * from tickets;
+----+-----------------+-------+-----------+---------+
| id | host            | bytes | max_bytes | enabled |
+----+-----------------+-------+-----------+---------+
|  1 | 192.168.100.103 |    84 |       100 |       1 |
+----+-----------------+-------+-----------+---------+
1 row in set (0.00 sec)
Отправим следующий пинг:
$ ping -c 1 192.168.1.1
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.

--- 192.168.1.1 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
При этом билет будет заблокирован, поскольку число принятых байт превысило лимит:
mysql> select * from tickets;
+----+-----------------+-------+-----------+---------+
| id | host            | bytes | max_bytes | enabled |
+----+-----------------+-------+-----------+---------+
|  1 | 192.168.100.103 |   168 |       100 |       0 |
+----+-----------------+-------+-----------+---------+

1 row in set (0.00 sec)
Работает! Правда, тормозить должно жутко, ибо задавать кучу вопросов СУБД (пусть даже и MySQL с дефолтным хранилищем MyISAM, которое и транзакций-то не умеет) на каждый входящий пакет - удовольствие не дешевое. Но о бенчмарках в следующий раз ...

Комментариев нет: