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

    Часть 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 к скриптам.

    Готовые скрипты, конфиги и схема БД тут.

    Комментарии 24

      +4
      Поклон Вам за Вашу плодовитость (в плане статей). Продолжайте.
        +2
        Спасибо, старался(:
        0
        Очень быстро написали третью часть, после второй — большущее спасибо!
          0
          Интересует, на каком железе не будет тормозить при 20 клиентах?
            0
            У друга подобная система, тоже на ulog-acctd, около 15 человек + всякие samba, ftp и еще куча всяких сервисов. Железо Cel 400, 128Mb памяти. Больше 2х лет живет и есть-пить не просит.
              0
              спасибо, попробуем…
            0
            как тут дела с приоритетом обстоят? например следующая ситуация:
            на серваке круглые сутки висит торрент клиент и жрет трафик. пользователь решил посерфить web или скачать небольшой файл. как повысить приоритет скорости для этого пользователя? по аналогии с процессорным временем, необходимо для торрент клиента задать приоритет idle.
              0
              В этом примере просто ограничивается скорость на всем интерфейса пользователя, естественно, если запустить торрент-клиент на сервере, он сожрет весь трафик. У самого торренты качаются на домашнем роутере, но пока вопрос приоритетов не решал(: Если руки дотянутся — обязательно поделюсь.
                0
                iproute вам поможет
                  0
                  а можно поподробнее???
              0
              Интересно какое надо железо, чтобы, например при 1к одновременных сессий, система не сбоила.
                0
                улог при таком количестве трафика загнется, лучше использовать что-то понадежней ;) описанный вариант подойдет для небольшого офиса.
                  0
                  А что не загнется? Из программного.
                  Например UTM с авторизацией через VPN+Radius+MPD на Атлоне каком-то в 1.5 Ггц умирает при ~150 сессиях.
                    0
                    IMHO, тут уже нужно смотреть сторону аппаратного решения (Cisco, etc..).
                    0
                    а если один-два из офисных работников будут качать с торрентов? сессий тоже может быть под 1000? Не знаете как улог при этом себя ведет?
                      0
                      нормально ведет
                  0
                  Мониторинг израсходованного и отключение абонентов можно делать средствами pppd с radius-плагином, без внешних костылей.
                    0
                    при таком скрипте не будет резаться исходящий траффик, только входящий, советовал бы исправить этот кусок кода

                    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");

                    на

                    system("$tc qdisc del dev $iface root");
                    system("$tc qdisc add dev $iface root handle 1: htb");
                    system("$tc class add dev $iface classid 1:1 htb rate ${speed}kbit ceil ${speed}kbit");
                    system("$tc filter add dev $iface protocol ip handle 1 fw classid 1:1");

                    тогда входящи кнала будет равен исходящему… тоесть просто надо уже играться с переменными для контроля скорости

                      0
                      С удовольствием прочел, спасибо.
                        0
                        народ никто не встречал доступного описания как сделать шейпер на 4-5 машин для небольшой сети без авторизации и т. п.
                        но что бы делил шейпер поровну канал между подключенными ip?
                          0
                          помоему нетрудно из вышенаписанных статей выжать...?

                          хотя, если кто-нибудь напишет конкретно под ваши цели — тоже с удовольствием почитаю! (сам пока тоже начинающий)
                          0
                          > Готовые скрипты, конфиги и схема БД тут.
                          Перезалейте, пожалуйста.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое