Pull to refresh

Рулим трафиком в Linux. Часть третья.

Reading time7 min
Views4.9K
Часть 1, Часть 2

Добавляем лимитирование трафика и ограничение пропускной способности канала.


Лимитируем трафик


Модернизируем нашу базу:
ALTER TABLE `users` ADD COLUMN `status` char(1) NOT NULL DEFAULT '1';
ALTER TABLE `users` ADD COLUMN `speed` int(11) NOT NULL DEFAULT '0';
ALTER TABLE `users` ADD COLUMN `traf_limit` bigint(20) NOT NULL DEFAULT '0';
ALTER TABLE `users` ADD COLUMN `traf_remain` bigint(20) NOT NULL DEFAULT '0';

Поле status определяет текущий статус аккаунта (1 — включен, 0 — заблокирован), speed — ограничение скорости в Kbit'ах (0 — без ограничения). Поля traf_limit и traf_remain лимит трафика в байтах и оставшееся его количество соответственно, если traf_limit равен 0, то считаем, что лимита нет.

Так как теперь при подключении нам нужно отсеивать пользователей, лимит которых закончился или их аккаунты в статусе «блокирован», подправим конфиг freeradius'a. В файле /etc/freeradius/sql.conf замените строчки
authorize_check_query = "SELECT id, login, 'User-Password' AS \"Attribute\", `password` AS \"Value\", '==' AS \"op\" FROM users WHERE login = '%{SQL-User-Name}'"
authorize_reply_query = "SELECT id, login, 'Framed-IP-Address' as \"Attribute\", ip as \"Value\", ':=' as \"op\" FROM users WHERE login = '%{SQL-User-Name}'"
authorize_group_check_query = "SELECT '1' as \"id\",'default' AS \"GroupName\", 'Auth-Type' as \"Attribute\", CASE WHEN status='1' THEN 'MS-CHAP' ELSE 'REJECT' END as \"Value\", ':=' as \"op\" FROM users WHERE login='%{SQL-User-Name}'"

на такие:
authorize_check_query = "SELECT id, login, 'User-Password' AS \"Attribute\", `password` AS \"Value\", '==' AS \"op\" FROM users WHERE login = '%{SQL-User-Name}' and status='1' and (traf_remain>0 or traf_limit=0)"
authorize_reply_query = "SELECT id, login, 'Framed-IP-Address' as \"Attribute\", ip as \"Value\", ':=' as \"op\" FROM users WHERE login = '%{SQL-User-Name}' and status='1' and (traf_remain>0 or traf_limit=0)"
authorize_group_check_query = "SELECT '1' as \"id\",'default' AS \"GroupName\", 'Auth-Type' as \"Attribute\", CASE WHEN status='1' THEN 'MS-CHAP' ELSE 'REJECT' END as \"Value\", ':=' as \"op\" FROM users WHERE login='%{SQL-User-Name}' and status='1' and (traf_remain>0 or traf_limit=0)"

и перезапустите freeradius.

Теперь модернизируем скрипт парсера, код:
#!/usr/bin/perl

use DBI;

# функция для преобразования айпи из формы ххх.ххх.ххх.ххх в десятичную
sub inet_aton {
    my @addr = split(/\./,$_[0]);
    my $dec = 0;
    for($n = 3; $n >= 0; $n--) {
       $dec += ($addr[-$n-1] << 8 * $n);
    }
    return $dec;
}

# определяем имя БД, пользователя и пароль
my $db_name = "ulogdb";
my $db_user = "ulog";
my $db_pass = "1234";

# путь к лог-файлу
$account_log = "/var/log/ulog-acctd/account.log";

# подключаемся к нашей базе
my $DBH = DBI->connect("DBI:mysql:$db_name:localhost",$db_user,$db_pass) or die "Error connecting to database";

# если скрипт запущен с параметром --set-limits, сбрасывает счетчики трафика пользователей
if ($ARGV[0] eq "--set-limits") {
    print "$ARGV[0]\n";
    # если 1, то неизрасходованный трафик переносится на следующий месяц
    my $move_unused = 1;
    if ($move_unused) {
       $STH = $DBH->prepare("update users set traf_remain=traf_remain+traf_limit where traf_limit");
    } else {
       $STH = $DBH->prepare("update users set traf_remain=traf_limit where traf_limit");
    }
    $STH->execute; $STH->finish;
    exit;
}

# получаем список пользователей в связке ip+id_user
my $STH = $DBH->prepare("select ip,id from users");
$STH->execute;
while (@tmp = $STH->fetchrow_array()) {
    $users{$tmp[0]} = $tmp[1];
}
$STH->finish;

# получаем список сетей
my $STH = $DBH->prepare("select prio,firstip,lastip,id from zones order by prio");
$STH->execute;
while (@tmp = $STH->fetchrow_array()) {
    $zones[$tmp[0]] = [$tmp[1], $tmp[2], $tmp[3]];
}
$STH->finish;

# делаем временную копию лога и очищаем оригинальный файл
system "cp $account_log /tmp/ulog-parser.tmp && cat /dev/null > $account_log";
open LOGFILE,"< /tmp/ulog-parser.tmp";
while (<LOGFILE>) {
    chomp;
   
    # переменную $saddr пока не используем,
    # она пригодится позже

    ($ts,$saddr,$daddr,$bytes) = split /\t/;

    # создаем новую временную метку, необходимо для агрегирования
    # статистки пользователя за определенный интервал времени
    # в одну запись. интервалом будем считать 1 минуту

    $ts = $ts - $ts % 60;

    # сопоставляем айпи из лога со списком пользователей
    # если айпи имеется в базе - наш клиент
    # массив со статистикой имеет древовидную структуру:
    # метка времени -> id пользователя -> полученный трафик

    if (exists($users{$daddr})) {
       # получаем идентификатор зоны трафика
       $zone_id = 0;
       for($i=0;$i>=$zones;$i++) {
          $nip = inet_aton($saddr);
          if ($zones[$i][0] <= $nip and $zones[$i][1] >= $nip) {
             $zone_id = $zones[$i][2];
             last;
          }
       }
       $data{$ts}{$users{$daddr}}{$zone_id} += $bytes;
    }
}
close LOGFILE;
unlink("/tmp/ulog-parser.tmp");

# немного оптимизировал запрос, спасибо хабраюзеру mgyk за подсказку :)
my $STH = $DBH->prepare("insert into data (id_user,id_zone,ts,bytes) values(?,?,?,?) on duplicate key update bytes=bytes+?");
my $STH_LIMIT = $DBH->prepare("update users set traf_remain=traf_remain-? where id=? and traf_limit");
# проходим по всему массиву статистики вложенным циклом
#
for $ts (keys %data) {
    for $id_user (keys %{$data{$ts}}) {
       for $id_zone(keys %{$data{$ts}{$id_user}}) {
          $STH->execute($id_user,$id_zone,$ts,$data{$ts}{$id_user}{$id_zone},$data{$ts}{$id_user}{$id_zone});
          $STH->finish;
          # вычитаем из кол-ва оставшегося трафика пользователя текущий трафик
          $STH_LIMIT->execute($data{$ts}{$id_user}{$id_zone},$id_user);
          $STH_LIMIT->finish;
       }
   }
}

# выберем из базы пользователей, у которых закончился лимит
$STH = $DBH->prepare("select ip from users where traf_limit>0 and traf_remain<=0");
$STH->execute;
while (($ip) = $STH->fetchrow_array) {
    my $lnk = `/sbin/ip addr show|/bin/grep $ip`;
    $lnk =~ m/^.+(ppp[0-9]+)$/;
    # разрываем сессию
    system("/bin/kill `cat /var/run/$1.pid`");
}
# отключаемся от БД
$DBH->disconnect;

Если у Вас есть пользователи с лимитированным трафиком, добавьте в crontab скрипт парсера с парметром --set-limits на первое число месяца, тем самым каждый месяц пользователям будет начисляться трафик. Чтобы своевременно отключать тех, у кого лимит уже закончился, сделайте интервал запуска парсера раз в 1-2 минуты. Например, так:
* *   * * *   root   /usr/bin/ulog-parser.pl
1 0   1 * *   root   /usr/bin/ulog-parser.pl --set-limits

каждую минуту парсим лог и в 00:01 1-го числа каждого месяца обновляем лимиты трафика. Проще некуда (:

Режем скорость


Добавляем в iptales правило:
iptables -t mangle -A FORWARD -d 10.1.0.0/24 -j MARK --set-mark 0x1

Маркируем все пакеты, идущие на адреса наших пользователей.

При инициализации любой ppp-сессии запускаются скрипты, находящиеся в /etc/ppp/ip-up.d. Нам это приходится как нельзя к стати. Создадим в этой директории скрипт, который при подключении пользователя будет ограничивать пропускную способность его интерфейса, я назвал его set_speed:
#!/usr/bin/perl

use DBI;

my $db_name = "ulogdb";
my $db_user = "ulog";
my $db_pass = "1234";

my ($ip,$iface) = @ARGV[4,0];

my $DBH = DBI->connect("DBI:mysql:$db_name:localhost",$db_user,$db_pass) or die "Error connecting to database";

my $STH = $DBH->prepare("select speed from users where ip='$ip'");
$STH->execute;
my ($speed) = $STH->fetchrow_array;
$STH->finish;

if ($speed) {
    system("/sbin/tc qdisc add dev $iface root handle 1: htb");
    system("/sbin/tc class add dev $iface classid 1:1 htb rate ${speed}kbit");
    system("/sbin/tc filter add dev $iface protocol ip handle 1 fw classid 1:1");
}
$DBH->disconnect;

В нем тоже нужно указать имя пользователя базы и пароль. Вообще-то, правильней было бы хранить их в отдельном конфиге, но я не стал заморачиваться с реализацией, Вы можете сами сделать это (:

Не забудьте добавить атрибут +x к скриптам.

Готовые скрипты, конфиги и схема БД тут.
Tags:
Hubs:
+37
Comments24

Articles

Change theme settings