Содержание
Гостевой доступ в интернет через wifi с авторизацией
Введение
В данной статье я хотел бы рассказать об одном из способов раздачи wifi-интернета вашим гостям в офисе, кафе, дома и вообще где угодно. Особенность данного способа в том, что он требует минимум оборудования и простой в настройке. Сама по себе идея не оригинальна и схема не нова, я узнал о таком способе из книги «The book of PF» и я тут же занялся своей реализацией данной идеи. Сама идея заключается в следующем: мы переводим нашу точку доступа в режим сетевого моста, на wifi пароль не устанавливаем, т.е. делаем его открытым, наш сервер(шлюз) выдает wifi-клиентам IP-адреса и прочие настройки, после чего весь веб-трафик от клиента перенаправляется на определенный порт нашего шлюза, где работает специальное приложение, которое проводит авторизацию пользователей. После успешной авторизации IP-адрес пользователя добавляется в специальную таблицу в пакетном фильтре, таким образом, ему предоставляется доступ в интернет. В чем отличие данного способа от традиционных? Например, при использовании wpa2-psk и подобных алгоритмов для ограничения доступа к wifi-сети мы имеем всего один общий пароль, который необходимо сообщать каждому вашему гостю, также мы не можем ограничить по времени пользования сетью и т.д. Еще один вариант - IEEE 802.1X, но он достаточно громоздкий и не любое оборудование его поддерживает, плюс также необходима некоторая доработка для контроля и ограничения ваших пользователей.
Требования
Веб-интерфейс авторизации, мониторинга и управления акаунтами написан на языке программирования python 2.6 (2.7 также подойдет) c использование веб-фреймворка django 1.3. Веб-сервер nginx + uwsgi. В качестве серверной ОС используется FreeBSD, а в качестве пакетного фильтра PF. Я не вижу никаких препятствий, чтобы использовать мой веб-интерфейс в связке с linux/iptables или FreeBSD/ipfw, доработки потребует только исполняемый файл wificmd, который выполняет добавление/удаление записей в пакетный фильтр.
Реализация
Начнем с настройки сети. В целях безопасности нам необходимо сделать так, чтобы точка доступа имела доступ только к шлюзу. Реализовать это можно несколькими способами: напрямую подключить точку доступа к отдельной сетевой карте шлюза, либо настроить VLAN, если ваше сетевое оборудование это поддерживает. В моей реализации используется второй вариант. Создан отдельный VLAN с именем openwifi, точка доступа подключена к нетегированному порту, а порт шлюза добавлен как тегированный. Логическая схема подключения показана на рисунке ниже.
Теперь, как уже говорилось выше, точка доступа переводится в режим сетевого моста. Далее нужно установить весь сопутствующий софт: nginx, uwsgi, pip (/usr/ports/devel/py-pip), isc-dhcpd, python, sudo. Плюс на свой вкус надо выбрать СУБД, в которой будут храниться данные, самый простой вариант – это sqlite3, для этого надо поставить py-sqlite3. Как это делается, описывать не буду, думаю и так понятно. Заключительным этапом ставим фреймворк django и проверяем, что все установлено корректно:
<konsole> # pip install django ... # python Python 2.6.7 (r267:88850, Jun 14 2011, 18:59:44) [GCC 4.2.1 20070719 [FreeBSD]] on freebsd8 Type "help", "copyright", "credits" or "license" for more information. >>> import django >>> django.VERSION (1, 3, 1, 'final', 0) >>>quit() </konsole>
На этом установка всех необходимых программ завершена, переходим к настройке. Сперва настроим dhcp-server. Допустим наша wifi-сеть 192.168.40.0/24, интерфейс сети em2, тогда конфиг сервера будет примерно таким:
default-lease-time 43200; max-lease-time 43200; ddns-update-style none; authoritative; log-facility local7; option routers 192.168.40.1; option domain-name-servers 8.8.8.8, 8.8.4.4; subnet 192.168.40.0 netmask 255.255.255.0 { range 192.168.40.2 192.168.40.253; }
43200 – 12 часов в секундах, на какой срок выдаем адрес в аренду, днс-сервера указываем любые, можно публичные гугла, но я предпочитаю использовать свой внутри сети, поднятый на том же шлюзе, в случае использования внешних, необходимо будет разрешить к ним dns-запросы от wifi-сети. Ну а 192.168.40.1 – соответственно IP-адрес нашего шлюза в wifi-сети. И последний штрих, т.к. у меня в локальной сети уже есть dhcp-сервер, то нужно сделать так, чтобы они не конфликтовали, в rc.conf прописываем:
dhcpd_enable="Yes" dhcpd_ifaces="em2"
Таким образом dhcp-сервер будет работать только на wifi-интерфейсе. Nginx у нас будет работать по двум протоколам: http и https. На http будут перенаправляться все web-запросы неавторизованных wifi-клиентов, а затем nginx будет делать перенаправление с http на наш сервис авторизации работающий через https, ведь наша wifi-сеть открытая, а это значит, что все передаваемые по ней данные доступны любому желающему, поэтому мы обезопасим процесс авторизации таким образом. Почему было не сделать сразу перенаправление на https? Потому что мы бы увидели сообщение об ошибке, ведь клиент делает http-запрос, а сервер ждет https. Итак, конфиг nginx.conf будет выглядеть примерно так:
error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 444; server_name 192.168.40.1; rewrite ^(.*)$ https://wifi.company.com permanent; } server { listen 443; server_name localhost; ssl on; ssl_certificate ssl/cert.pem; ssl_certificate_key ssl/cert.key; ssl_session_timeout 5m; ssl_protocols SSLv2 SSLv3 TLSv1; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; ssl_prefer_server_ciphers on; location / { uwsgi_pass unix:///tmp/uwsgi.sock; include uwsgi_params; } location /static/ { root /www/wifiadmin/wifiadmin; } }
Протокол http прослушивается на 444 порту (у меня 80-ый порт занят апачем) и все запросы перенаправляются на https://wifi.company.com, на этом адресе у нас работает сервис авторизации. Вместо доменного имени можно указать IP-адрес. Дальше в конфиге описывается https-сервер на стандартном порту 443, тут все стандартно, далее прописываем чтобы nginx контактировал с uwsgi через локальный сокет /tmp/uwsgi.sock и наконец прописываем статические файлы сервиса авторизации, считаем, что архив распакован в каталог /www.
Теперь нужно сгенерить ssl-сертификаты cert.pem и cert.key для nginx. Выполняем следующие команды, при генерации сертификата указываем свои данные: <konsole> # mkdir /usr/local/etc/nginx/ssl # cd /usr/local/etc/nginx/ssl # openssl req -new -x509 -days 9999 -nodes -out cert.pem -keyout cert.key Generating a 1024 bit RSA private key ....++++++ ......++++++ writing new private key to 'cert.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:RU State or Province Name (full name) [Some-State]:Voronezh Locality Name (eg, city) []:Voronezh Organization Name (eg, company) [Internet Widgits Pty Ltd]:Roga & ko Organizational Unit Name (eg, section) []:unit Common Name (eg, YOUR name) []:Andrey Email Address []:admin@domain.com </konsole>
Переходим к самому сервису авторизации. Скачиваем архив wifiadmin.tar и распаковываем его в /www, должно получиться следующее: <konsole> # ls -Rl1 /www/ wifiadmin /www/wifiadmin: uwsgi.xml webapp.py wifiadmin wificmd wificmd.c /www/wifiadmin/wifiadmin: __init__.py cron.py main manage.py settings.py static templates urls.py /www/wifiadmin/wifiadmin/main: __init__.py admin.py forms.py models.py static tests.py utils.py views.py /www/wifiadmin/wifiadmin/main/static: wifiadmin /www/wifiadmin/wifiadmin/main/static/wifiadmin: css images js /www/wifiadmin/wifiadmin/main/static/wifiadmin/css: style.css /www/wifiadmin/wifiadmin/main/static/wifiadmin/images: info.png /www/wifiadmin/wifiadmin/main/static/wifiadmin/js: utils.js /www/wifiadmin/wifiadmin/static: /www/wifiadmin/wifiadmin/templates: 500.html base.html home.html info.html pass.html </konsole>
Редактируем файл /www/wifiadmin/uwsgi.xml, тут уже все прописано, единственное что нужно поменять - это параметр pythonpath, если вы распаковали архив не в /www.
<uwsgi> <pythonpath>/www/wifiadmin</pythonpath> <module>webapp</module> <socket>/tmp/uwsgi.sock</socket> <process>1</process> <master/> <enable-threads/> <uid>80</uid> <gid>80</gid> <pidfile>/var/run/uwsgi.pid</pidfile> </uwsgi>
Теперь можно проверить uwsgi: <konsole> # uwsgi -x /www/wifiadmin/uwsgi.xml [uWSGI] parsing config file /www/wifiadmin/uwsgi.xml *** Starting uWSGI 1.0 (64bit) on [Wed Feb 22 11:19:39 2012] *** compiled with version: 4.2.1 20070719 [FreeBSD] on 21 February 2012 17:40:42 current working directory: /www/wifiadmin writing pidfile to /var/run/uwsgi.pid detected binary path: /usr/local/bin/uwsgi uWSGI running as root, you can use --uid/--gid/--chroot options setgid() to 80 setuid() to 80 your memory page size is 4096 bytes uwsgi socket 0 bound to UNIX address /tmp/uwsgi.sock fd 3 Python version: 2.6.7 (r267:88850, Jun 14 2011, 18:58:41) [GCC 4.2.1 20070719 [FreeBSD]] Python main interpreter initialized at 0x801849240 threads support enabled your server socket listen backlog is limited to 100 connections *** Operational MODE: single process *** added /www/wifiadmin/ to pythonpath. WSGI application 0 (mountpoint='') ready on interpreter 0x801849240 pid: 41081 (default app) *** uWSGI is running in multiple interpreter mode *** spawned uWSGI master process (pid: 41081) spawned uWSGI worker 1 (pid: 41082, cores: 1) </konsole>
Если он запустился без ошибок, останавливаем Ctrl + C и прописываем в rc.conf:
uwsgi_enable="YES" uwsgi_flags="-x /www/wifiadmin/uwsgi.xml"
и запускаем <konsole> # /usr/local/etc/rc.d/uwsgi start </konsole>
Теперь надо скомпилировать небольшую программку(wificmd), которая будет добавлять/удалять IP-адреса клиентов из таблицы пакетного фильтра. Она принимает два аргумента, действие add/delete и сам IP-адрес. Написана программа на языке Си, на самом деле это не важно, главное то, что она должна быть бинарной, т.к. ее будет вызывать сервис авторизации, который работает через nginx, у которого в свою очередь ограничены права, а для того, чтобы внести изменения в пакетный фильтр требуются права суперпользователя. Получается у нас два варианта как разрешить веб-серверу запускать wificmd, либо установить sticky bit, либо настроить sudo. Ни первый ни второй вариант мы не можем реализовать со скриптом, поэтому пришлось сделать небольшую отдельную программу, очень ограниченную в возможностях, которая только умеет добавлять и удалять адреса из жестко прописанной в коде таблицы. Итак, выполняем команду:
<konsole> # gcc /www/wifiadmin/wificmd.c -o /www/wifiadmin/wificmd </konsole>
Второй вариант с sudo более красивый, т.к. выполнять wificmd смогут не все непривилегированные пользователи, а только один - www, поэтому я использовал именно его. Ставим sudo и в sudoers прописываем wificmd так, чтобы пользователь www мог выполнять ее от имени суперпользователя без запроса пароля:
www ALL=NOPASSWD: /www/wifiadmin/wificmd
Теперь приступим к настройке wifiadmin, редактируем файл settings.py, ниже минимальный список опций, которые нужно изменить, все остальное можно оставить без изменений:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'dbnama', 'USER': 'user', 'PASSWORD': 'pass', 'HOST': '1.1.1.1', 'PORT': '', } } WIFI_NETWORK = '192.168.40.0/24'
Здесь мы задаем настройки подключения к БД, в моем случае это MySQL и последний параметр - wifi-сеть.
Теперь нам надо собрать статические файлы django и синхронизировать БД. Во время синхронизации БД нам будет предложено создать суперпользователя, соглашаемся, под этим пользователем мы будем администрировать учетные записи wifi.
<konsole> # cd /www/wifiadmin/wifiadmin # python manage.py collectstatic You have requested to collect static files at the destination location as specified in your settings file. This will overwrite existing files. Are you sure you want to do this? Type 'yes' to continue, or 'no' to cancel: yes ........ 83 static files copied to '/www/wifiadmin/wifiadmin/static'. # python manage.py syncdb Creating tables ... Creating table auth_permission Creating table auth_group_permissions Creating table auth_group Creating table auth_user_user_permissions Creating table auth_user_groups Creating table auth_user Creating table auth_message Creating table django_content_type Creating table django_session Creating table django_site Creating table django_admin_log Creating table main_myuser Creating table main_mysession Creating table main_log You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no): yes Username (Leave blank to use 'root'): admin E-mail address: admin@domain.com Password: Password (again): Superuser created successfully. Installing custom SQL ... Installing indexes ... No fixtures found. </konsole>
Все, теперь можно попробовать залогиниться в административный интерфейс по адресу https://my_gw_address/admin/
Следующим этапом будет настройка пакетного фильтра, в моем случае - это PF. Ниже приведен примерный минимальный набор правил, все остальные правила фильтрации необходимо добавит исходя из ваших потребностей.
openwifi_net="192.168.40.0/24" ext_if="em0" int_if="em1" wifi_if="em2"
# Заставляем пакетный фильтр создать пустую таблицу table <openwifi> persist # Наши локальные сети table <not_routable> { 10.0.0.0/8, 192.168.0.0/16 } scrub in all fragment reassemble scrub out all random-id max-mss 1400 set skip on $int_if set skip on lo # Разрешаем нат от клиентов из таблицы openwifi nat from <openwifi> to any -> $ext_if # Но если клиент не в таблице openwifi, то перенаправляем его на сервис авторизации rdr pass on $wifi_if proto tcp from ! <openwifi> to any port 80 -> 127.0.0.1 port 444 # Это если у вас нет своего DNS-сервера nat proto udp from $openwifi_net to { 8.8.8.8, 8.8.4.4 } port 53 -> $ext_if block in pass out keep state # Это если у вас свой DNS pass in proto udp from $openwifi_net to $wifi_if port 53 # Открываем доступ к 443 порту от wifi-сети на wifi-интерфейсе pass in proto tcp from $openwifi_net to $wifi_if port 443 # Ну и разрешаем доступ в интернет pass in proto { tcp udp icmp } from <openwifi> to { !<not_routable> }
В заключении нужно прописать в crontab скрипт, который будет выполняться каждые 10 минут и закрывать истекшие сесии:
. */10 * * * * www /www/wifiadmin/wifiadmin/cron.py
На этом настройка закончена, теперь, если все было настроено корректно, при попытке входа на любой сайт через wifi, пользователя будет перекидывать на страничку авторизации.
Скриншоты
Как работает wifiadmin
Принцип работы следующий: клиенты подключаются к открытой wifi-сети, автоматически получают сетевые настройки и при первой попытке открыть любой веб-узел перенаправляются на страницу авторизации. После успешной авторизации адрес клиента добавляется в специальную таблицу в фаерволе, в БД добавляется информация в таблицу сессий и пользователю предоставляется доступ в интернет. Специальный скрипт cron.py выполняется каждые 10 минут и проверяет активные сессии, если есть сессии время которых превышает максимальное время сессии, указанное в конфиге, то она закрывается. Если пользователь, сессия которого активна, авторизуется с другого компьютера с другим IP-адресом, то его старая сессия закроется и откроется новая для текущего компьютера.
pfctl -t openwifi -T show
В админке в wifiadmin есть несколько разделов: Auth и Main. В первом разделе создаются пользователи и группы, это стандартный раздел django admin, по большому счету там можно создавать только пользователей и давать им права суперпользователя, эти акаунты нужны только для входа в админку. Второй раздел main относится к wifi, там как раз создаются пользователи для доступа в интернет. У пользователей есть несколько параметров, такие как срок действия акаунта, активен он или нет и т.д. В журнал в обычном режиме пишутся события безопасности, а в режиме отладки подробный отчет о всех выполненных действиях на каждом этапе. Ну и наконец раздел «сессии» содержит информацию о подключениях, дата подключения, активна ли сессия и с какого адреса было открыто подключение. Тут же есть функция ручного отключения пользователя, если сессия активна.
Известные проблемы
- В некоторых браузерах, например firefox кеширует перенаправление на сервис авторизации и даже после успешной авторизации, при попытке входа на тот сайт, с которого было выполнено перенаправление, пользователь все равно попадает на сервис авторизации. Лечится очисткой кеша для данного сайта.
План работ
- Возможно, стоит сделать еще один cron-скрипт, который будет каждый час или каждые сутки проверять, нет ли в таблице openwifi адресов, которые не числятся в БД в таблице сессий как активные, таким образом, они никогда не будут удалены из таблицы маршрутизации. Такая ситуация маловероятна, но теоретически возможна из-за какого-либо сбоя.
~~DISCUSSION~~