Двухфакторая аутентификация VPN/Mikrotik – просто и масштабируемо

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Здравствуйте!

На написание данной статьи меня побудило прочтение аналогичного содержания статьи пользователя nkusnetsov. По количеству просмотров видно, что сообществу интересна данная тема.

Освоить MikroTik вы можете с помощью онлайн-курса «Настройка оборудования MikroTik». В курсе изучаются все темы из официальной программы MTCNA. Автор – официальный тренер MikroTik. Материал подходит и тем, кто уже давно работает с оборудованием MikroTik, и тем, кто еще не держал его в руках. В состав входят 162 видеоурока, 45 лабораторных работ, вопросы для самопроверки и конспект.

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

  • Низким уровнем вхождения и простотой кода (для понимания/отладки другим сотрудником)
  • Простые скрипты ROS не создают никакой нагрузки и работают даже на hAP Lite
  • Масштабируемость – возможность подключения большого количества VPN-шлюзов с целью снижения нагрузки или географического распределения
  • Возможность использования Mikrotik CHR в качестве VPN-сервера
  • «1хN» – 1 SMS-шлюз на неограниченное количество роутеров с возможностью расширения при росте нагрузки
  • Возможность привязки отдельного роутера к «конкретному» модему (для чего? – об этом позже)
  • Использование всего одного php скрипта на удаленном сервере
  • Не важно какое устройство инициировало VPN-соединение, авторизация по ссылке из SMS

При несложной доработке кода:

  • Возможность вести log авторизаций сотрудников на сервере (возможность реализована в расширенной версии, если есть интерес – выложу)
  • Увеличить отказоустойчивость и снижение нагрузки системы путем отправки SMS рандомно с нескольких модемов


Постановка задачи и решение


Задача 1


В процессе разработки данного решения была поставлена задача минимизировать количество usb-модемов – уменьшив стоимость владения, упростить администрирование, локализовать модемы в одном месте и тем самым улучшить ремонтопригодность системы.

Было решено использовать один роутер установленный на «надежном канале» как базовый SMS-шлюз. Единственное я не знал какова будет нагрузка на систему, справятся модемы и не заблокирует ли оператор. Поэтому я заложил возможность подключения нескольких модемов и дальнейшего разбиения системы на группы. Причем дополнительные модемы могут быть подключены к базовому шлюзу через простой usb-hub. Но для полноценного функционирования, в минимальном функционале, нужен один модем в не зависимости от количества роутеров.

Задача 2


Предусмотреть возможность использования локальных сим-карт в зоне установки роутера.
Пример: широкая филиальная сеть с несколькими магазинами в Казахстане. Отправка sms-сообщения из РФ будет стоить достаточно дорого. Данное решение позволяет сотрудникам из РК получать sms с локального номера.

Но в процессе размышлений оказалось, что решение уже было найдено и реализовано в Задаче 1.

Задача 3


Авторизация туннеля с мобильного устройства без необходимости физически находиться на авторизуемом устройстве.

Цель – возможность авторизовывать не только пользовательские туннели, но и любые vpn-соединения: Mikrotik->Mikrotik, Сервер->Mikrotik и т.д При этом пользователю, ответственному за данные туннели, необходимо просто перейти по ссылке из SMS сообщения, в которой также отображается какой туннель хочет авторизоваться.

Для возможности авторизации таким способом появилась необходимость вынести скрипт на внешний ресурс – сервер, доступный из любой точки а также код авторизации использовать для целей маркировки адресного листа в firewall и последующей разблокировки.

Решение данной задачи уже однозначно подразумевало использование RouterOS API (или SSH). Победу одержало API, как наиболее простой вариант.

Логика


  1. Пользователь авторизуется с заранее добавленным логином и паролем
  2. VPN-шлюз заносит его ip в адресный лист ожидания и отправляет POST запрос на сервер
  3. Сервер получает данные, проверяет, формирует код-авторизации и отправляет на контактный номер SMS вида: «To autorize user 79001112233 connection open – http_://synome.ru/?ruid=vrG7yYMbZ6&auth=YU6zc»
  4. Пользователь переходит по ссылке с любого устройства
  5. Сервер делает запрос на роутер, проверяет наличие в адрес-листе записи с кодом авторизации в комментарии. Если запись найдена, удаляет ее, а пользователю отображает страницу удачной авторизации

Во всех остальных случаях, если что-то не прошло проверку пользователь получит Request error.

Настройка и код


Теперь перейдем от идейной части к настройке Mikrotik, коду и их описанию

Добавляем на Mikrotik:


Firewall


Обязательно создаем список разрешенных ресурсов, среди которых: собственный адрес MT, любой публичный DNS, адрес сервера на котором будет происходить авторизация
/ip firewall address-list
add address=10.10.0.1 list=Allow-list
add address=8.8.8.8 list=Allow-list
add address=synome.ru list=Allow-list


Добавляем правило блокирующее трафик VPN-пользователей из списка ожидания на любой хост кроме разрешенных
/ip firewall raw
add action=drop chain=prerouting dst-address-list=!Allow-list src-address-list=VPN-blocked disabled=no


PPP Profile


Создаем профиль в котором устанавливаем idle-таймаут соединения. Время должно быть меньше чем указанное в следующем скрипте On Up, в противном случае, если авторизация не была произведена, то после удаления списка из address-list пользователь получит доступ на время равное разнице (idle-таймаут минус address-list timeout)

Добавляем профиль
/ppp profile
add dns-server=10.10.0.1 idle-timeout=59m local-address=10.10.1.100 name=2F-VPN use-compression=no use-encryption=no use-mpls=no


Далее на последней вкладке Script добавляем:

On Up
:global pass "19RuOU89";
:global ruid "vrG7yYMbZ6";
:local userip [/ppp active get [find name=$user] address];
# if phone number stored in comment
#:local userphone [/ppp secret get [find name=$user] comment];
# if phone number = username
:local userphone $user;

:local authkey [/tool fetch http-method=post http-data="ruid=$ruid&pass=$pass&tel=$userphone" url="http://synome.ru/" mode=http as-value output=user];

/ip firewall address-list remove [find address=$userip];
/ip firewall address-list add address=$userip list=VPN-blocked timeout=1h comment=($authkey->"data");

:log info message="User connect:";
:log info message=$userphone;
:log info message=$userip;
:log info message=($authkey->"data");


On Down
:local userip [/ppp secret get [find name=$user] remote-address];
/ip firewall address-list remove [find address=$userip];
:log info message="User disconnect:";
:log info message=$user;
:log info message=$userip;


Смысл скриптов

При подключении: в самом начале мы задаем логин и пароль роутера, которые будут проверяться на сервере. При авторизации пользователя получаем его номер телефона (может быть в имени или в комментарии) и локальный ip-адрес. Отправляем POST-запрос на сервер и получаем в ответе код авторизации. Добавляем ip-адрес в address-list VPN-blocked с кодом авторизации в комментарии и тайм-аутом на 1 минуту больше чем в профиле. Выводим все в лог.

При отключении: получаем ip-адрес пользователя, находим его в address-list и удаляем. Все выводим в лог.


PPP Secrets


Добавляем пользователя
/ppp secret
add comment="70001112233" name=70001112233 password=testuser profile=2F-VPN remote-address=10.10.1.100


Номер телефона можно указать прямо в name, но если хотим иметь возможность задавать один номер на несколько аккаунтов (для авторизации нескольких туннелей), то номер указываем в комментарии, при этом в скрипте On Up нужно изменить закомментированность строк

Изменение (вторую открываем, четвертую закрываем)
# if phone number stored in comment
:local userphone [/ppp secret get [find name=$user] comment];
# if phone number = username
#:local userphone $user;


Ну и самое главное включаем PPTP или L2TP сервер.

На этом с Mikrotik работа закончена.

Серверная часть на PHP


Ниже приведен код. Он достаточно подробно комментирован, поэтому не буду писать лишний текст. Самое главное – заменить данные в $host и $ruid_data на свои.

index.php
<?php

// ------------------------------------------------------------------------------
//  Copyright (с) 2020
//  Author: Dmitri Agababaev, d.agababaev@duncat.net
//
//  Copyright by authors for used RouterOS PHP API class in the source code files
//
//  Redistributions and use of source code, with or without modification, are
//  permitted that retain the above copyright notice
// ------------------------------------------------------------------------------

require_once('routeros_api.class.php');

// Адрес по которому доступен данный скрипт
$host = 'http://synome.ru/';
// МАССИВ ДАННЫХ ВСЕХ РОУТЕРОВ, А ТАКЖЕ РОУТЕРА ЯВЛЯЮЩЕГОСЯ SMS-ШЛЮЗОМ
$ruid_data = array(
    // роутеры учавствующие в авторизации
    // пароль в md5 , глобальный ip-адрес, логин входа на роутер, пароль, SMS-шлюз через который происходит отправка SMS
    'vrG7yYMbZ6' => array('mdpass' => '5568ba82f332494d9ff8754b51e7b28a', 'ip' => '10.10.0.1', 'login' => 'user_vpn', 'password' => 'kji&@11az', 'smsgw' => 'SMS_gw1'),
    // SMS-шлюзы
    // ip-адрес шлюза (глобальный или локальный если в одной сети с сервером), логин, пароль, порт USB-модема, канал USB-модема
    'SMS_gw1' => array('ip' => '172.16.1.3', 'login' => 'sms2F', 'password' => 'skIU8w!0', 'port' => 'usb1', 'channel' => '0')
);


// ВХОДНЫЕ ПРОВЕРКИ ЗАПРОСОВ
if (!$_REQUEST) die('Request error'); // если запроса нет – сброс
if (!$_REQUEST['ruid']) die('Request error'); // если не указан ruid - сброс
if (!array_key_exists($_REQUEST['ruid'], $ruid_data)) die('Request error'); // если роутер не существует – сброс
if ($_REQUEST['auth']) autorize(); // если запрос на авторизацию, то пускаем без пароля и проверяем авторизацию
if (!ruid_auth()) die('Request error'); // проверяем пароль роутера для отправки SMS
if ($_REQUEST['tel']) send_authcode(); // если задан номер телефона, отправляем SMS

// ПРОВЕРКА НА НАЛИЧИЕ РОУТЕРА В СПИСКЕ РАЗРЕШЕННЫХ и пароля авторизации
function ruid_auth() {
  global $ruid_data;
  if (!$_REQUEST['pass']) return false; // если пароль не задан – сброс
  // проверяем md5-хэш пароля
  if (md5($_REQUEST['pass']) == $ruid_data[$_REQUEST['ruid']]['mdpass']) return true;
  return false;
}

// ФУНКЦИЯ ОТПРАВКИ ССЫЛКИ С КОДОМ АВТОРИЗАЦИИ ЧЕРЕЗ ROS API
function send_authcode() {
  global $ruid_data;
  global $host;
  $sms_gw = $ruid_data[$_REQUEST['ruid']]['smsgw']; // данные sms-шлюза
  // генерируем код авторизации
  $auth_code = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789'), 0, 5);
  // подключаем класс
  $API = new RouterosAPI();
  // Формируем сообщение отправляемое пользователю – только eng или транслит
  $message = 'To autorize user '.$_REQUEST['tel'].' connection open – '.$host.'?ruid='.$_REQUEST['ruid'].'&auth='.$auth_code;
  // если подключились отправляем SMS
  if ($API->connect($ruid_data[$sms_gw]['ip'], $ruid_data[$sms_gw]['login'], $ruid_data[$sms_gw]['password'])) {
      // Команда отправки SMS
      $ARRAY = $API->comm("/tool/sms/send", array(
      "port"=>$ruid_data[$sms_gw]['port'],
      "channel"=>$ruid_data[$sms_gw]['channel'],
      "phone-number"=>$_REQUEST['tel'],
      "message"=>"To autorize user ".$_REQUEST['tel']." connection open – ".$host."?ruid=".$_REQUEST['ruid']."&auth=".$auth_code,));
      // если отправка не удалась и получили ошибку модема, то выполняем сброс питания usb для перезагрузки модема
      if($ARRAY['!trap']) {
        $API->comm("/system/routerboard/usb/power-reset");
        die('Stop with error: '.$ARRAY['!trap'][0]['message'].' Making power reset of usb-port');}
  }

  $API->disconnect();
  die($auth_code);
}

function autorize() {
  global $ruid_data;
  // подключаем класс
  $API = new RouterosAPI();
  if ($API->connect($ruid_data[$_REQUEST['ruid']]['ip'], $ruid_data[$_REQUEST['ruid']]['login'], $ruid_data[$_REQUEST['ruid']]['password'])) {
    // если подключились отправляем команду
    $API->write('/ip/firewall/address-list/print', false);
    $API->write('?comment='.$_REQUEST['auth'], false);
    $API->write('=.proplist=.id');
    // получаем ответ
    $ARRAYS = $API->read();
    // ЕСЛИ ЗАПИСЬ НЕ СУЩЕСТВУЕТ В АДРЕС-ЛИСТЕ - СБРОС
    if (!$ARRAYS[0]) die('Request error');
    // удаляем запись
    $API->write('/ip/firewall/address-list/remove', false);
    $API->write('=.id=' . $ARRAYS[0]['.id']);
    $READ = $API->read();
  }
  $API->disconnect();

  // ИНФОРМИРУЕМ ПОЛЬЗОВАТЕЛЯ ОБ УСПЕШНОЙ АВТОРИЗАЦИИ
  die('
      <html>
      <body style="background-color: #282c34; color: #fff; height: 100vh; display: flex;">
        <div style="margin: auto; max-width: 50%;">
          <p style="font-size: 24pt; font-weight: bold; margin: -300px 0 50px;">
            VPN-соединение установлено и авторизовано, можете продолжить работу
          </p>
          <p style="font-size: 14pt; color: #aaa;">
             В случае недоступности сервисов обратитесь к вашему системному администратору<br/>
          </p>
        </div>
      </body>
      </html>');
}

?>


RouterOS API class PHP используемый в коде можно взять на GitHub.

Благодарю за внимание. Буду рад любым комментариям.

Освоить MikroTik вы можете с помощью онлайн-курса «Настройка оборудования MikroTik». В курсе изучаются все темы из официальной программы MTCNA. Автор – официальный тренер MikroTik. Материал подходит и тем, кто уже давно работает с оборудованием MikroTik, и тем, кто еще не держал его в руках. В состав входят 162 видеоурока, 45 лабораторных работ, вопросы для самопроверки и конспект.
Источник: https://habr.com/ru/post/506708/


Интересные статьи

Интересные статьи

Часто от программистов PHP можно услышать: «О нет! Только не „Битрикс“!». Многие специалисты не хотят связываться фреймворком, считают его некрасивым и неудобным. Однако вакансий ...
Ася Патрышева — первая девушка в Петербурге, получившая нодовый адрес Фидо, первая в истории российского интернета обладательница частного домена kenga.ru. Ася работала дизайнером в Nevalink,...
Недавно мне довелось поработать над приложением, которое должно было контролировать скорость своих исходящих подключений. Например, подключаясь к одному URL приложение должно было ограничить се...
На сегодняшний день у сервиса «Битрикс24» нет сотен гигабит трафика, нет огромного парка серверов (хотя и существующих, конечно, немало). Но для многих клиентов он является основным инструментом ...
Автокэширование в 1с-Битрикс — хорошо развитая и довольно сложная система, позволяющая в разы уменьшить число обращений к базе данных и ускорить выполнение страниц.