Рубрики
Kamailio

Kamailio часть 5. HTABLE в Kamailio, введение в хэш-таблицы и реализация кэширования SIP REGISTER + fail2ban с примерами

В этой статье мы рассмотрим модуль для работы с хэш таблицами в Kamailio и реализуем кэширование SIP авторизаций + fail2ban с помощью модуля htable.

Для чего нужны хэш-таблицы в Kamailio

Как вы знаете из предыдущих (Kamailio с нуля) статей, мы уже реализовали обработку запросов REGISTER (регистрацию и авторизацию в SIP). Так же мы научились проходить NAT и обрабатывать запросы пользователей которые находятся за NAT (использую сервер для прохождения NAT, «Server-Side NAT»). Так же мы из 4 части узнали, как обрабатывать(проксировать) голос (RTP потоки) в Kamailio с помощью RTPEngine модуля. Сейчас у нас реализована схема простых звонков между двумя пользователями. Но давайте представим, что у нас не два пользователя, а несколько сотен пользователей.

Давайте разберемся еще раз, что происходит на сервере, когда приходит:

а) SIP REGISTER сообщения т.е запросы на регистрацию от пользователя

б) SIP INVITE от пользователя А к пользователю B, т.е запрос на установку сеанса связи через проткол SIP

Обработка SIP REGISTER запросов в Kamailio

Примечание. Эта краткая выдержка из отдельной статьи. Полная статья доступна по (SIP регистрация в Kamailio с примерами)

Первым делом, мы обрабатываем запрос REGISTER в главном маршруте request_route:

request_route {
        route(REQINIT);

        if (is_method("CANCEL")) {
                if (t_check_trans()) {
                        route(RELAY);
                }
                exit;
        }

        if (is_method("INVITE") || is_method("REGISTER")) {
                route(NAT);
        }

        if (is_method("REGISTER")) {
                route(AUTH);
        }

        route(DIALOG);
}

Изначально каждый SIP запрос попадает в главный маршрут request_route, после чего уже происходит обработка SIP сообщения согласно написанному конфигурационному файлу. Сначала SIP запрос REGISTER попадает в маршрут REQINIT, в котором единственное, что мы на данный момент реализовано — вызов функции force_rport, это функция используется для того, чтобы Kamailio отвечал пользовательскому агенту (User-Agent, UA) на тот же порт, с которого поступил запрос.

route[REQINIT] {
        force_rport;
}

Далее, т.к отправлен запрос на регистрацию (REGISTER), мы пропускает условие на проверку сообщения CANCEL из главного маршрута request_route, идем дальше по конфигурационному файлу сверху вниз.

После чего видно что стоит условный оператор if, условие равно истине при запросах REGISTER и INVITE (выделено красным цветом), после чего идет вызов пользовательского маршрута NAT. Этот маршрут, как мы знаем из предыдущих статей используется для прохождения NAT со стороны сервера (прохождением NAT занимается сервер, пользовательский агент может даже не знать о его существовании):

route[NAT] {
        if (nat_uac_test("19")) {
                if (is_method("REGISTER")) {
                        set_contact_alias();
                } else {
                        if(is_first_hop()) {
                                set_contact_alias();
                        }
                }
        }
        return;
}

Здесь также ничего сложного, этот маршрут подробно разбирался во второй статье цикла (Прохождение NAT в Kamailio с примерами). Очень кратко напомню, что здесь мы с помощью nat_uac_test функции проверяем, находится ли пользователь за NAT или нет и если обнаруживается, что пользователь за NAT, мы узнаем его реальный («белый») IP адрес и добавляем этот IP адрес в поле Contact как Alias.

После чего запрос REGISTER возвращается в главный маршрут request_route, где опять проверяется является ли полученный SIP запрос регистрацией (привожу только интересующее нас условие):

if (is_method("REGISTER")) {
         route(AUTH);
}

После чего запрос на регистрацию отправляется в пользовательский маршрут AUTH для авторизации. Сам AUTH маршрут выглядит следующим образом:

route[AUTH] {
                if (!auth_check("$fd", "subscriber", "1")) {
                        auth_challenge("$fd", "1");
                        exit;
                }
                if (is_method("REGISTER")) {
                        save("location");
                        exit;
                } else {
                        return;
                }
}

Что мы здесь видим? А здесь мы видим вызов функции auth_check с некоторыми параметрами. Сама функция auth_check очень подробно разбиралась в первой статье (SIP регистрация в Kamailio) цикла. Мы передаем функции некоторые аргументы, в частности псевдопеременную $fd (домен который указан в запросе, т.е 1111@ipcalls24.com в таком виде доменом является все, что указано после @), также во втором аргументе вызова функции мы указываем из какой таблицы в базе данных необходимо брать данные (subscriber — таблица по умолчанию) и третьим аргументов функции мы указываем, что необходимо проверять (в данном случае флаг 1 значит, что мы проверяем, совпадает ли поля From и To в SIP сообщении). Далее рассматривать не будем, т.к нас интересует только эта функция.

Для нас интересна функция auth_check по единственной причине, эта функция делает запрос непосредственно в базу данных Kamailio.

Давайте посмотрим наглядно, что при запросе REGISTER, Kamailio отправляет SQL запрос в базу данных. Для этого мы захватим трафик на порту 5433 (порт который слушает база данных Postgres) с помощью утилиты tcpdump и отправим запрос REGISTER на Kamailio. Первым делом посмотрим трассировку полученную с помощью утилиты sngrep:

sip register in kamailio trace

И в этот же момент, я снимал трафик с порта 5433 с помощью tcpdump:

tcpdump sip register

Из этих скриншотов видно, что при запросе на регистрацию или запросе INVITE каждый раз будет отравляться SQL запрос в базу данных. Но проблема возникает в том, что запросы на жесткий диск (неважно HDD это или SSD, хотя в SSD с этим намного лучше) выполняются намного медленней нежели запросы в оперативную память сервера. Именно поэтому, пока у нас нет большого количества пользователей все будет хорошо. Когда же появится значительное количество пользователей, сервер будет выполнять свою задачу с задержками, а также появятся блокировки (процесс kamailio, запущенный в операционной системе, будет ожидать ответа от жесткого диска на SQL запрос). Для повышения производительности и уменьшения времени отклика мы будем использовать хэш-таблицы модуля HTable

Обработка SIP INVITE запросов в Kamailio

Примечание. Эта краткая выдержка из отдельной статьи. Полная статья доступна по ссылке (SIP INVITE в Kamailio, обработка вызова и транзакций с примерами)

Обработка INVITE запросов так же начинается с главного маршрута request_route. Выполняются аналогичные шаги как и с запросом REGISTER. Единственное отличие запросов INVITE от запросов REGISTER это вызов дополнительного маршрута DIALOG в котором обрабатываются транзакции. Давайте посмотрим этот пользовательский маршрут:

route[DIALOG] {
        if (is_method("INVITE")) {
                route(AUTH);
                if (!lookup("location")) {
                        sl_send_reply("403", "Forbidden");
                        exit;
                }
                handle_ruri_alias();
                record_route();
                route(RELAY);
        }
        if (has_totag()) {
                if (loose_route()) {
                        handle_ruri_alias();
                        route(RELAY);
                }
        }
        if (is_method("ACK")) {
                if ( t_check_trans() ) {
                        route(RELAY);
                        exit;
                } else {
                        exit;
                }
        }
}

Очень подробно мы разбирали этот маршрут в третье статье цикла (Обработка вызова и транзакций в Kamailio с примерами). Также очень кратко напомню, что здесь вызывается маршрут AUTH для авторизации пользователя. И вот этот маршрут особенно интересен в рамках этой статьи так же как и в случае с запросом REGISTER. Дело в том, что получая запрос INVITE мы так же авторизуем пользователя для того, чтобы убедится, что пользователь имеет право на совершение звонков через наш SIP сервер (у пользователя введен корректный логин/домен/пароль).

Так же как и с запросами на регистрацию, Kamailio выполняет SQL запросы в базу данных, чтобы получить данные из таблицы subscriber (таблица по умолчанию в которой хранятся все локальные абоненты). И в этом случае также возникают задержки и блокировки при серьезной нагрузке. Чтобы этого избежать мы добавим обработку запросов REGISTER и INVITE через хэш-таблицы которые хранятся в оперативной памяти сервера.

HTable — хэш таблицы в Kamailio

Хэш-таблицы имеют архитектуру «ключ-значение (key=>value)» которые хранятся в оперативной памяти сервера. Это значительно сокращает время обработки пользовательских SIP запросов и значительно повышает скорость работы Kamailio. Сам модуль, который позволяет создавать хэш-таблицы называется HTable.

Для использования htable в kamailio необходимо подключить модуль ко всем остальным модулям:

loadmodule "htable.so"

После чего необходимо создать саму таблицу. Ицициализация хэш-таблицы происходит следующим образом:

modparam("htable", "htable", "название_хэш_таблицы=>размер; время_жизни_записей; таблица_в_бд;колонки_в_таблице_бд;флаг_использования_бд;return_code_при_null;автообновление_срока_жизни;кластеризация_dmq;")

Где:

  1. Название хэш таблицы — задается пользователем, в будущем используется для обращения в хэш таблицу по названию
  2. Размер таблицы — определяется как 2заданный_размер. Т.е при размере таблицы 5 будет создана таблицы размером 25=32 записи.
  3. Время жизни записей — указывается время, сколько будут хранится значения в таблице. Например 1800 секунд означает, что значения попав в хэш таблцу пробудут там пол часа (1800сек)
  4. Таблица в бд — используемая таблица в базе данных из которой будут загружаться данные при загрузке Kamailio.
  5. Колонка в бд — используемая колонка из которой будут браться данные при загрузке Kamailio
  6. Флаг использования бд — записывать ли значения hash таблицы в базу данных. 0 — не записывать, 1 — записывать
  7. return_code при null — возвращаемое значение при не нахождении записи в хэш таблицу. Т.е будет возращена условная единица вместо null по умолчанию
  8. Автообновление срока жизни — обновлять ли время хранения значений в таблицу при изменении (обновлении) таблицы. 1 — обновлять (значение по умолчанию), 0 — не обновлять
  9. Реплицировать ли хэш-таблицу на другие kamailio сервера в кластере через dmq (будет отдельная статья), по умолчанию нет т.е 0.

Не все параметры являются обязательными.

HTable кэширование регистраций в Kamailio

Давайте создадим следующую таблицу для кэширования регистраций:

modparam("htable", "htable", "auth=>size=10;autoexpire=1800;")

В данном случае, название таблицы по которому будет происходить обращение — auth, размер таблицы 1024 значения, время жизни значений в таблице — 1800 секунд. По умолчанию, если не задано иное, при каждом обновлении таблицы время жизни записей будет обнуляться и начинаться заново с 1800 секунд.

А также добавим параметр для модуля auth_db в котором укажем название avp переменной с помощью которой будем извлекать данные из базы данных.

AVP перменные

В kamailio есть два вида переменных:
var — обычные переменные, могут быть случайно доступны в одном child процессе kamailio
avp — переменные которые доступны только в рамках одной транзакции. Имеют архитектуру ключ-значение, могут выполнять динамически запросы в базу данных.
modparam("auth_db", "load_credentials", "$avp(pass)=password")

Т.е из вышеприведенного кода мы сохраняем полученные значения из таблицы password (из базы данных), в переменную $avp(pass)

Теперь необходимо изменить пользовательский маршрут AUTH, созданный ранее в первой статье (Регистрация в SIP протоколе в Kamailio) цикла:

route[AUTH] {
                if (sht_match_name("auth", "eq", "$Au")) {
                        if (!pv_auth_check("$fd", "$sht(auth=>$Au)", "0", "1")) {
                                auth_challenge("$fd", "1");
                                xlog("L_ERR", "First stage, found AoR in htable\n");
                                exit;
                        }
                        consume_credentials();
                        if (is_method("REGISTER")) {
                                save("location");
                                exit;
                        }
                } else {
                        if (!auth_check("$fd", "subscriber", "1")) {
                               auth_challenge("$fd", "1");
                                exit;
                        }
                        $sht(auth=>$Au) = $avp(pass);
                        consume_credentials();
                        if (is_method("REGISTER")) {
                                save("location");
                                exit;
                        }
                }
}

Какие изменения мы внесли:

  1. Изначально с помощью функции sht_match_name модуля htable проверяется существует ли значение в хэш-таблице auth (ранее созданной) имени со значением полученным из псевдопеременной $Au. Псевдопеременная $Au позволяет получить значение имя_абонента@домен_абонента из SIP запроса, например [email protected]
  2. Если значение в хэш-таблице найдено — сопоставить значения хранимые в хэш-таблице с полученными значениями из SIP запроса (проверить логин/пароль/домен).
  3. В случае, если данные неверны или отсутствуют поля авторизации — отправить запрос 401 (при отсутствии поля Authorization или неверных данных в логине/пароле/домене в запросе REGISTER), и 407 (при отсутствии поля Proxy-Authorization или неверных данных в логине/пароле/домене в запросе INVITE)
  4. Если данные корректны и совпадают со значениями из хэш-таблицы auth, удалить авторизационные заголовки (Authorization/Proxy-Authorization) из пересылаемых SIP сообщений для безопасности.
  5. Если это запрос регистрации — сохранить AoR (Address of Record, берется из заголовка Contact) в location, т.е оперативную память kamailio и зарегистрировать абонента

Теперь давайте посмотрим как обрабатывается запрос на регистрацию/авторизацию INVITE, если значения соответствующие псевдопеременной $Au (username@domain) не найдены:

  1. С помощью функции auth_check выполняем запрос в базу данных чтобы сравнить логин/пароль/домен с полученными из SIP запроса значениями.
  2. Если данные неверны или отсутствуют заголовки для авторизации, то тогда отправить запрос 401 (при запросе REGISTER)/407 (при запросе INVITE) в ответ пользовательскому агенту.
  3. Если данные верны — вызывается функция:
$sht(auth=>$Au) = $avp(pass);

$sht — переменная для работы с хэш таблицами, auth — название хэш таблицы, $Au — псевдопеременная из которой мы получаем логин@домен из SIP запроса абонента. В данном случае перменная $Au используется как ключ (key) в хэш таблице, $avp(pass) — используется как значение в хэш таблице (value). В этой переменной содержится пароль из базы данных для абонента.

Давайте посмотрим, как выглядят закэшированные значения в таблице HTable:

htable in kamailio
Наглядно видно строение записей key=>value. В данном случае, ключ указан в name, значение в value

4. После чего мы удаляем заголовки для авторизации с помощью функции consume_credentials() и регистрируем пользователя через save(«location»);

После внесения изменений давайте посмотрим, делаются ли запросы в базу данных при наличии кэша в хэш-таблице:

sip trace in kamailio
Сама трассировка вызова между абонентом А и абонентом B

И в этот же момент, через tcpdump захватим весь трафик который поступает на порт 5433 (порт, который слушает база данных):

Снимаем весь трафик на порт 5433 во время совершения вызова между абонентами.

Как видно из tcpdump’а, в момент совершения звонка запросов в базу данных не поступало. Как еще в этом можно убедится? Давайте посмотрим саму хэш-таблицу auth и убедимся, что там есть кэшированные значения:

htable in kamailio
Значения хранимые в хэш-таблице auth

Как видно, у нас есть два закэшированных абонента (1111 и 1112) и поэтому не происходит обращений в базу данных.

Fail2ban через HTable в Kamailio

Что мы обычно видим включая sngrep? Обычно картина выглядит примерно так:

Снятие трассировок в течении 5 минут. Всего на сервере заведено два абонента 1111 и 1112, остальные — это обычный спам с попытками подбора пароля

Также можно увидеть запросы INVITE на международные номера (начинаются с 00). И каждый раз Kamailio обрабатывает эти запросы согласно своему конфигурационному файлу. Как вы понимаете — это крайне не выгодно. Представим, что у нас высоконагруженный VoIP сервис который доступен из интернета. Помимо наших абонентов мы получаем огромное количество SIP запросов от мошейников и каждый раз сервер пытается авторизовать такие запросы. Как с этим бороться? Самый примитивный способ — сделать fail2ban.

Т.е при получении запросов с одного IP адреса и в случае, если авторизация была неудачной — мы добавляем +1 в счетчик попыток авторизации с этого IP. При определенном пороге, мы блокируем (не отвечаем) на любые запросы с этого IP адреса.

Для реализации такой примитивной защиты мы будем использовать хэш-табицу. Для этого мы создадим еще одну хэш-таблицу с именем ipban, размером в 210=1024 значений, с временем жизни записей в 5 минут (300 секунд) и в случае не нахождения ключа будем возращать 0:

modparam("htable", "htable", "ipban=>size=10;autoexpire=300;initval=0;")

После чего, добавим в маршрут AUTH счетчик, который будет прибавлять единичку при неуспешной авторизации:

route[AUTH] {
                if (sht_match_name("auth", "eq", "$Au")) {
                        if (!pv_auth_check("$fd", "$sht(auth=>$Au)", "0", "1")) {
                                auth_challenge("$fd", "1");
                                $sht(ipban=>$si) = $sht(ipban=>$si) + 1;
                                exit;
                        }
                        consume_credentials();
                        $sht(ipban=>$si) = $null;
                        if (is_method("REGISTER")) {
                                save("location");
                                exit;
                        }
                } else {
                        if (!auth_check("$fd", "subscriber", "1")) {
                                auth_challenge("$fd", "1");
                                $sht(ipban=>$si) = $sht(ipban=>$si) + 1;
                                exit;
                        }
                        $sht(auth=>$Au) = $avp(pass);
                        $sht(ipban=>$si) = $null;
                        consume_credentials();
                        if (is_method("REGISTER")) {
                                save("location");
                                exit;
                        }
                }
}

Как ранее говорилось, обращение к хэш-таблице происходит через $sht(имя_таблицы=>значение). В данном случае мы берем в качестве имени IP адрес через псевдопеременную $si и добавляем в значение единичку при неуспешной авторизации. В случае успешной авторизации мы сбрасываем счетчик с помощью $null.

Убедимся, что счетчик работает корректно, перезапустим Kamailio и намеренно попытаемся зарегистрироваться с неправильным паролем:

flood sip kamailio

Посмотрим на содержимое хэш-таблицы с помощью команды kamcmd htable.dump ipban:

htable in kamailio

Как видно, счетчик действительно считает количество неудачный попыток авторизации. Теперь давайте добавим проверку в конфигурационный файл Kamailio, при которой мы будем блокировать все запросы с IP адреса при значении неудачных авторизаций больше 5. Для этого мы добавим в пользовательский маршрут REQINIT следующие строки:

route[REQINIT] {
        if($sht(ipban=>$si) > 5) {
                exit;
        }
        force_rport;
}

Здесь мы в условном операторе if проверяем счетчик попыток для IP адреса (через псевдопеременную $si) и в случае, если значение счетчика больше 5 (больше 5 неудачных авторизаций) мы прекращаем какую-либо обработку запросов с этого IP адреса на время жизни указанное в параметре htable (мы указали 300 секунд). Давайте посмотрим трассировки при неудачных авторизациях с моего IP адреса:

fail2ban in kamailio

Как видно, сервер полностью перестал отвечать на запросы с моего IP адреса. Так будет до тех пор, пока время жизни значения в хэш-таблице не истечет и запись с моим IP адресом не будет удалена.

Но как быть, если нам нужно удалить определенный IP адрес из черного списка? Все просто, для этого мы используем kamcmd и с его помощью мы удалим IP адрес из черного списка. Для этого необходимо ввести:

kamcmd htable.delete ipban 18*.165.***.51

Где 18*.165.***.51 — IP адрес который необходимо удалить, ipban — название хэш таблицы из которой необходимо удалить IP адрес.

После этого регистрация проходит успешно:

На этом реализация простого fail2ban завершена. Также есть отдельный модуль pike, который реализует примерно тоже самое. А также есть возможность блокировки по User Agent, но это материал для отедльной статьи.

Заключение

В этой статье мы познакомились с хэш-таблицами в Kamailio. Реализовали кэширование логина/домена/пароля для авторизации, а также сделали простой fail2ban на основе того же модуля htable.

Полный конфигурационный файл kamailio.cfg доступен по ссылке.

Если есть желание сделать пожертвование, то это можно сделать кликнув по ссылке или же нажав на кнопку. Спасибо за поддержку !)

Для зарубежных платежных систем/карт

8 ответов к “Kamailio часть 5. HTABLE в Kamailio, введение в хэш-таблицы и реализация кэширования SIP REGISTER + fail2ban с примерами”

Методом тыка понял что Kamailio не стартует с этой строчкой вообще ни как

if (!auth_check(«$fd», «subscriber», «1»)) {
auth_challenge(«$fd», «1»);
exit;
}
База postgres работает
Тока убираю этот код запуск нормальный

0(5428) CRITICAL: [core/cfg.y:3589]: yyerror_at(): parse error in config file /etc/kamailio/kamailio.cfg, line 583, column 12-27: Can’t get from cache: $sht(ipban=>$si)
ERROR: bad config file (1 errors)

Вообщем сбивала столк эта строчка
#!ifdef WITH_ANTIFLOOD
loadmodule «htable.so»
loadmodule «pike.so»
#!endif

А нужно было к основным добавить
loadmodule «jsonrpcs.so»
loadmodule «kex.so»
loadmodule «corex.so»
loadmodule «tm.so»
loadmodule «tmx.so»
loadmodule «sl.so»
loadmodule «rr.so»
loadmodule «pv.so»
loadmodule «maxfwd.so»
loadmodule «usrloc.so»
loadmodule «registrar.so»
loadmodule «textops.so»
loadmodule «textopsx.so»
loadmodule «siputils.so»
loadmodule «xlog.so»
loadmodule «sanity.so»
loadmodule «ctl.so»
loadmodule «cfg_rpc.so»
loadmodule «acc.so»
loadmodule «counters.so»
loadmodule «db_postgres.so»
loadmodule «dialog.so»
loadmodule «uac.so»
loadmodule «htable.so»

Остался вопрос почему
kamcmd htable.dump ipban
error: 500 — No such htable
root@kama:/etc/kamailio# kamcmd htable.dump auth
error: 500 — No such htable

Немного не понятно Есть таблица htable там есть колонки
id — key_name — key_type — value_type — key_value — expires

я должен что туда внести
1 — auth — — — 1800
2 — ipban —-
Подскажите как есть ?
Или kama сама создает и заносит данные ??

Вот код из pastebin работает — но это только для авторизации -посмотрите что будет если вам прилетает такое
https://ibb.co/1vPxjV8

Это все попадает в астериск
https://ibb.co/d7tss3z

Он им еще и отвечет
kamailio: Call (UDP:192.168.20.33:5060) to extension ‘00011442080890870’ rejected because extension not found in context ‘from_kamailio’.

Как защитится от таких атак ?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *