Pull to refresh

Настройка wifi авторизации через sms под ubuntu 16.04

Reading time24 min
Views56K
Привет Хабр! Не так давно, в нашей организации встала задача узаконить wifi доступ, но чтобы в дальнейшем использование системы было бесплатно. (Согласно постановлению Правительства №758 от 31 июля 2014г. и №801 от 12 августа 2014 г. — все публичные WIFI сети обязаны производить идентификацию пользователей). У нас 10 залов для мероприятий (от 30 до 400 человек), а в день в среднем проходит от 4 до 12, плюс постоянная текучка народа и капризные пользователи.

image

Для начала расскажу, как у нас организована сеть. Если не вдаваться в подробности, то развешены точки доступа HP MSM430(J9651), управление через HP MSM760(J9420A) и шлюз HP F1000-EI (JG214A). Выбор оборудования не удачен, но работаем с тем, что имеем.

Так сложилось, что по жизни я больше полюбил Windows системы, но получив задачу, начитавшись кучу статей, пришел к выводу, что для этой цели лучше всего подойдет *nix система. Выбор пал на Ubuntu сервер 16.04. Дальше было несколько дней мучений, но в итоге все получилось.

Эта статья для тех, кто любит Windows, смотрит на Ubuntu и ставит с нуля кучу софта.

Как я решил все организовать:

Имеется открытая сеть, назовем ее Free. Пользователь при подключении переадресуется на хотспот со страницей авторизации. Там ему выдается код доступа к интернету и номер телефона, на которое он должен отправить сообщение (немного необычно, но стояла задача экономии, в том числе и на смс). Как только сообщение приходит к нам, доступ в интернет сразу открывается. Оговорюсь, симку и номер в модеме лучше использовать МТС (не реклама), так как только у них не нашел возможности отправить СМС с сайта без указания левого номера (хотя это проблема, но в процессе решения).

Что нам потребуется:

  • лошадка Ubuntu Server 16.04
  • прокси Squid в прозрачном режиме
  • Mysql (MariaDB)
  • Nginx
  • PHP FPM
  • USB redirector (виртуальная машинка под HyperV)
  • модем Huawei E153 МТС
  • SMS tools для чтения смс
  • и тд. и тп.

Начнем.

Установка Ubuntu Server 16.04


Здесь все довольно банально:

  1. Создаем виртуальную машинку (2 ядра, 4 Гб памяти, 2 сетевых интерфейса, 30 Гб диск)
  2. Качаем последний дистрибутив, подключаем, ставим...
  3. Настраиваем сетевые интерфейсы, один смотрит в vlan сети wifi, второй в сторону шлюза

Настройка сети и форвардинг пакетов



Смотрим, какие у нас присутствуют сетевые карты и какие назначены им логические имена:
cat /proc/net/dev 

В моем случае, это eth0 и eth1:

Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo: 632643353 3624368    0    0    0     0          0         0 632643353 3624368    0    0    0     0       0          0
  eth1: 8789521059 30492824    0    0    0     0          0         0 65843784529 28992970    0    0    0     0       0          0
  eth0: 65798728800 56063700    0    0    0     0          0         0 8382628950 29920038    0    0    0     0       0          0


Редактируем файл настройки сетевых интерфейсов:
nano /etc/network/interfaces

auto eth0
iface eth0 inet static
        # в сторону интернет шлюза
        address 10.66.66.6
        netmask 255.255.255.240
        network 10.66.66.0
        broadcast 10.66.66.15
        gateway 10.66.66.1
        dns-nameservers 10.66.66.1
auto eth1
iface eth1 inet static
        # в сторону wifi пользователей
        address 10.0.87.254
        netmask 255.255.248.0
        network 10.0.80.0
        broadcast 10.0.87.255

Убедимся в том, что в системе действительно присутствуют IPv6 интерфейсы:

ip a | grep inet

Также можно увидеть, что некоторые приложения вывешивают TCP прослушиватели на интерфейсах IPv6. Посмотреть все прослушиваемые в системе порты можно командой:

sudo ss -lnptu | sort

Чтобы выключить поддержку IPv6 на всех сетевых интерфейсах сразу, открываем на редактирование файл sysctl.conf

sudo nano -Y sh /etc/sysctl.conf

В конец файла добавляем строки для включения форвардинга и отключения IPv6:

net.ipv4.ip_forward=1
net.ipv6.conf.all.disable_ipv6 = 1

Для проверки того, что наша опция сможет быть прочитана sysctl во время загрузки выполним:

sudo sysctl -p

/etc/init.d/networking restart

Установка MySQL


В качестве сервера баз данных, выбрал MaridDB. По функционалу в чем-то даже лучше MySQL, но статья не об этом.

apt-get install mariadb-server
#настраиваем пароли на доступ
mysql_secure_installation
#запускаем mysql и сбрасываем привилегии
 mysql -u root
 use mysql;
 update user set plugin='' where User='root';
 flush privileges;

Проверяем, все ли хорошо у нас запустилось:

service mysql status

Установка Squid с поддержкой SSL и отключением IPv6


Есть много статей, как собирать и устанавливать прокси, но этот этап оказался наверное самый муторный. По умолчанию, в репозитарии убунты лежит Squid без поддержки SSL. Решил пересобрать, и так прошло 2 дня… В итоге получился свой мануал, как собрать под x64 последнюю версию 3.5.20.

Ставим необходимый софт для сборки:

apt-get install git fakeroot checkinstall build-essential devscripts patch libssl-dev libgnutls28-dev
apt-cache policy squid3
apt-get update
apt-get build-dep squid3

Расcкомментируем репозитарии исходников и добавим новый:

nano /etc/apt/sources.list
deb-src http://ftp.de.debian.org/debian/ testing main contrib non-free

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

gpg --keyserver keyserver.ubuntu.com --recv 8B48AD6246925553
gpg --export --armor 8B48AD6246925553 | sudo apt-key add -
gpg --keyserver keyserver.ubuntu.com --recv 7638D0442B90D010
gpg --export --armor 7638D0442B90D010 | sudo apt-key add -

Не забываем обновить информацию о репозитариях:

apt-get update

Чтобы не захламить рабочую папку, переходим в tmp и скачиваем из testing последнюю версию squid с правилами для сборки под debian

cd /tmp/
apt-get source squid3

Текущая версия 3.5.19, обновляемся до последней 3.5.21

wget http://www.squid-cache.org/Versions/v3/3.5/squid-3.5.21.tar.gz
tar -xf squid-3.5.21.tar.gz
mkdir ./squid-3.5.21/debian/
cp -r ./squid3-3.5.19/debian/*  ./squid-3.5.21/debian/
cd squid-3.5.21/
nano debian/rules

Добавляем строчки (не забываем указать путь к openssl.cnf, у меня это /etc/ssl)

--disable-ipv6 \
--enable-icap-client \
--enable-ssl-crtd \
--with-openssl=/etc/ssl \

Исправляем ошибку (постоянные сообщения в логах: SECURITY ALERT: Host header forgery detected on local=...:443 remote=...:* ), хорошо описанную в статье, с оценкой всех рисков безопасности. (В статье описано о версии 3.5.12, здесь немного изменился код).

nano ./src/client_side_request.cc

Ищем функцию hostHeaderIpVerify и немного изменяем ее код:

void
ClientRequestContext::hostHeaderIpVerify(const ipcache_addrs* ia, const DnsLookupDetails &dns)
{
    Comm::ConnectionPointer clientConn = http->getConn()->clientConnection;
    // note the DNS details for the transaction stats.
    http->request->recordLookup(dns);
    if (ia != NULL && ia->count > 0) {
        // Is the NAT destination IP in DNS?
        for (int i = 0; i < ia->count; ++i) {
            if (clientConn->local.matchIPAddr(ia->in_addrs[i]) == 0) {
                debugs(85, 3, HERE << "validate IP " << clientConn->local << " possible from Host:");
                http->request->flags.hostVerified = true;
				http->doCallouts();
                return;
            }
            debugs(85, 3, HERE << "validate IP " << clientConn->local << " non-match from Host: IP " << ia->in_addrs[i]);
        }
    }
    
    // patch for SECURITY ALERT: Host header forgery detected
    http->request->flags.hostVerified = true;
    http->doCallouts();
    return;
				
    debugs(85, 3, HERE << "FAIL: validate IP " << clientConn->local << " possible from Host:");
    hostHeaderVerifyFailed("local IP", "any domain IP");
}

Подтверждаем патч и собираем (ждем примерно 10-15 минут)

dpkg-source --commit
    #patch update to squid 3.5.20
debuild

Смотрим, какие пакеты у нас собрались:

ls -l /tmp/ | grep .deb$

Если пакетов нет, значит проверить папку
ls -l /tmp/squid3-3.5.19/ | grep .deb$

И начинаем установку Squid:

apt-get install squid-langpack  libdbi-perl
dpkg -i squid-common_3.5.19-1_all.deb
dpkg -i squid_3.5.19-1_amd64.deb
dpkg -i squid3_3.5.19-1_all.deb
dpkg -i squidclient_3.5.19-1_amd64.deb

если по каким-либо причинам, подвис установщик, то сбрасываем блокировку:

fuser -vki /var/lib/dpkg/lock

запускаем и проверяем статус

service squid start
systemctl status -l squid

в ответ должны увидеть что-то похожее

squid.service - LSB: Squid HTTP Proxy version 3.x
   Loaded: loaded (/etc/init.d/squid; bad; vendor preset: enabled)
   Active: active (running)

Проверяем установленную версию Squid

/usr/sbin/squid -v

Squid Cache: Version 3.5.21
Service Name: squid
Ubuntu linux


Теперь займемся его настройкой, для начала создадим SSL сертификат и сохраним конфиг по умолчанию:

cd /etc/squid
openssl req -new -newkey rsa:1024 -days 365 -nodes -x509 -keyout squidCA.pem -out squidCA.pem

mv ./squid.conf ./squid.conf.default
nano ./squid.conf

Файл конфигурации, почти без изменений взят из статьи.

acl localnet src 10.0.80.0/21
acl SSL_ports port 443
acl Safe_ports port 80          # http
acl Safe_ports port 21          # ftp
acl Safe_ports port 443         # https
acl Safe_ports port 70          # gopher
acl Safe_ports port 210         # wais
acl Safe_ports port 1025-65535  # unregistered ports
acl Safe_ports port 280         # http-mgmt
acl Safe_ports port 488         # gss-http
acl Safe_ports port 591         # filemaker
acl Safe_ports port 777         # multiling http
acl CONNECT method CONNECT

dns_nameservers 10.66.66.1
http_access deny !Safe_ports

http_access deny CONNECT !SSL_ports

http_access allow localhost manager
http_access deny manager

http_access allow localnet
http_access allow localhost
http_access deny all

#http_port 3128
#прозрачный порт указывается опцией intercept
http_port 10.0.87.254:3128 intercept options=NO_SSLv3:NO_SSLv2

#также нужно указать непрозрачный порт, ибо если захотите вручную указать адрес
#прокси в браузере, указав прозрачный порт, вы получите ошибку доступа, поэтому нужно
#указывать непрозрачный порт в браузере, если конечно такое желание будет, к тому же в логах #сыпятся ошибки о том, что непрохрачный порт не указан=)
http_port 10.0.87.254:3130 options=NO_SSLv3:NO_SSLv2

#и наконец, указываем HTTPS порт с нужными опциями
https_port 10.0.87.254:3129 intercept ssl-bump options=ALL:NO_SSLv3:NO_SSLv2 connection-auth=off cert=/etc/squid/squidCA.pem

always_direct allow all
sslproxy_cert_error allow all
sslproxy_flags DONT_VERIFY_PEER

#укажем правило со списком блокируемых ресурсов (в файле домены вида .domain.com)
acl blocked ssl::server_name  "/etc/squid/blocked_https.txt"
acl step1 at_step SslBump1
ssl_bump peek step1

#терминируем соединение, если клиент заходит на запрещенный ресурс
ssl_bump terminate blocked
ssl_bump splice all

sslcrtd_program /usr/lib/squid/ssl_crtd -s /var/lib/ssl_db -M 4MB

coredump_dir /var/spool/squid
refresh_pattern ^ftp:           1440    20%     10080
refresh_pattern ^gopher:        1440    0%      1440
refresh_pattern -i (/cgi-bin/|\?) 0     0%      0
refresh_pattern .               0       20%     4320
cache_dir aufs /var/spool/squid 2048 49 256
maximum_object_size 61440 KB
minimum_object_size 3 KB

cache_swap_low 90
cache_swap_high 95
maximum_object_size_in_memory 512 KB
memory_replacement_policy lru

#logfile_rotate 31
logfile_daemon /usr/lib/squid/log_db_daemon
access_log daemon:/127.0.0.1:3306/base/table/user/password squid

Создаем файл со списком блокируемых ресурсов

nano ./blocked_https.txt

Обращаю внимание, что логи будем писать в базу данных. Для этого создаем таблицу:

CREATE TABLE `access_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `time_since_epoch` decimal(15,3) DEFAULT NULL,
  `time_response` int(11) DEFAULT NULL,
  `ip_client` char(15) DEFAULT NULL,
  `ip_server` char(15) DEFAULT NULL,
  `http_status_code` varchar(10) DEFAULT NULL,
  `http_reply_size` int(11) DEFAULT NULL,
  `http_method` varchar(20) DEFAULT NULL,
  `http_url` varchar(500) DEFAULT NULL,
  `http_username` varchar(20) DEFAULT NULL,
  `http_mime_type` varchar(50) DEFAULT NULL,
  `squid_request_status` varchar(50) DEFAULT NULL,
  `squid_hier_status` varchar(20) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

запускаем и проверяем статус

service squid start
systemctl status -l squid

в ответ должны увидеть что-то похожее

 squid.service - LSB: Squid HTTP Proxy version 3.x
   Loaded: loaded (/etc/init.d/squid; bad; vendor preset: enabled)
   Active: active (running) 

Проверяем используемые порты и версию установленного squid

sudo ss -lnptu | grep :3128 
sudo ss -lnptu | grep :3129
sudo ss -lnptu | grep :3130

squid -version
Squid Cache: Version 3.5.20

Ставим USB-Redirector и модем


Так как, используется кластер под HyperV 2012R2, то возникает проблема, а именно как нам воткнуть модем. В нашей организации довольно успешно используется: USB-Redirector. Ставим его под Ubuntu

cd /tmp/
wget http://www.incentivespro.com/usb-redirector-linux-x86_64.tar.gz
tar -xf usb-redirector-linux-x86_64.tar.gz
./usb-redirector-linux-x86_64/installer.sh install-client

подключаемся к серверу, куда подключили модем


usbclnt -addserver 10.X.X.X:32032
usbclnt -autoconnect on 1

смотрим перечень всех usb устройств

usbclnt -l
================= USB CLIENT OPERATION SUCCESSFUL ===============
List of USB servers and devices:

   1: USB server at 10.X.X.X:32032
      Mode: auto-connect     Status: connected
   -   7: HUAWEI Mobile
           Vid: 12d1   Pid: 1001   Port: 3-2
           Mode: manual-connect   Status: disconnected

устанавливаем драйверы модема


apt-get install usb-modeswitch usb-modeswitch-data

подключаемся и проверяем, установлен ли модем


usbclnt -connect 1-7
ls /dev | grep ttyUSB

в ответ должны увидеть:

ttyUSB0
ttyUSB1
ttyUSB2


USB устройство требуется перевести в режим модема, для этого ставим программу minicon:

apt-get install minicom

Запускаем ее настройку:

minicom -s

Выбираем «Настройка последовательного порта» и в пункт «последовательный порт» ставим /dev/ttyUSB0
Больше ничего не меняем в настройках. Для переключения режимов работы в модемах huawei используются следующие at-команды:
AT^U2DIAG=0 -устройство в режиме только модем
AT^U2DIAG=1 -устройство в режиме модем + CD-ROM
AT^U2DIAG=255 -устройство в режиме модем + CD-ROM + Card Reader
AT^U2DIAG=256 -устройство в режиме модем + Card Reader
Включаем режим «только модем»:
AT^U2DIAG=0
В ответ получаем «ОК». Выходим из программы, для этого жмем Ctrl+A и Q.

Устанавливаем SMS-tools


Информацию о пакеты можно найти на сайте разработчика.
Устанавливаем и настраиваем:

apt-get install smstools
nano /etc/smsd.conf

находим строчку [GSM1]

[GSM1]
 device = /dev/ttyUSB0
 incoming = yes
 baudrate = 9600
 eventhandler = /var/www/sms_recieve.php

создаем файл обработчика входящих смс и делаем его исполняемым

nano /var/www/sms_recieve.php
chmod 755 /var/www/sms_recieve.php
service smstools restart

Настраиваем DHCP сервер


apt-get install isc-dhcp-server
nano /etc/default/isc-dhcp-server
#указываем, на каком интерфейсе будет слушать dhcp сервер
INTERFACES="eth1"
sudo nano /etc/dhcp/dhcpd.conf 
# указываем
authoritative;
subnet 10.0.80.0 netmask 255.255.248.0 {
  range 10.0.80.1 10.0.86.254;
  option domain-name-servers 10.0.87.254;
  option domain-name "wifi-free";
  option subnet-mask 255.255.248.0;
  option routers 10.0.87.254;
  option broadcast-address 10.0.87.255;
  default-lease-time 7200; #2h
  max-lease-time 72000; #20h
}

/etc/init.d/isc-dhcp-server restart 

Настраиваем Nginx и PHP 5.6 FPM


Так как php обновилось до 7 версии и во всех репозитариях ubuntu уже лежит php7, то добавляем новый и ставим:

add-apt-repository ppa:ondrej/php
apt-get install php5.6-cli php5.6-common php5.6-mysql php5.6-gd php5.6-fpm php5.6-cgi php-pear 

останавливаем установленный сервис и редактируем файлы настройки

service php5.6-fpm stop

nano /etc/php/5.6/fpm/php.ini
	cgi.fix_pathinfo = 0
	post_max_size = 200M
	upload_max_filesize = 200M

nano /etc/php/5.6/fpm/pool.d/www.conf
	security.limit_extensions = .php .php3 .php4 .php5
	listen = /run/php/php5.6-fpm.sock
	listen.owner = www-data
	listen.group = www-data
	listen.mode = 0660

service php5.6-fpm start

Можно убедится в том, что права доступа к сокету установлены верно:

ls -la /run/php/php5.6-fpm.sock
#srw-rw---- 1 www-data www-data 0 May  2 16:36 /run/php/php5.6-fpm.sock

Проверяем:

php -v

PHP 5.6.23-2+deb.sury.org~xenial+1 (cli)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
    with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies

Переходим к Nginx

apt-get install nginx nginx-extras

Основные настройки Nginx хранятся в файле /etc/nginx/nginx.conf.

Настройки базового сайта хранятся в файле /etc/nginx/sites-available/default.

Базовый конфигурационный файл сайта принято помещать в папку /etc/nginx/sites-available/ и затем включить его путём добавления символической ссылки на этот файл в папке /etc/nginx/sites-enabled/.

touch /etc/nginx/sites-available/hotspot.domain.com
ln -s /etc/nginx/sites-available/hotspot.domain.com /etc/nginx/sites-enabled/
mkdir /etc/nginx/common

Дальше буду краток, так как пояснения можно почитать по ссылке. Создаем общие файлы настройки сервера, в которых описываем настройки безопасности, сжатия, кэширования и php.

touch /etc/nginx/common/upstream
nano /etc/nginx/common/upstream

upstream php-fpm
{
	# PHP5.6-FPM сервер
	server unix:/run/php/php5.6-fpm.sock;
}

touch /etc/nginx/common/security
nano /etc/nginx/common/security

add_header		X-Frame-Options		"SAMEORIGIN";
add_header		X-Content-Type-Options	"nosniff"; 

touch /etc/nginx/common/gzip
nano /etc/nginx/common/gzip

gzip		on;
gzip_disable	"msie6";
gzip_comp_level	6;
gzip_min_length	1100;
gzip_buffers	16 8k;
gzip_proxied	any;
gzip_types	text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/javascript application/json application/xml+rss; 

touch /etc/nginx/common/php-fpm
nano /etc/nginx/common/php-fpm

# Настройки порта или сокета PHP-FPM производятся в файле "/etc/php/5.6/fpm/pool.d/www.conf"
fastcgi_pass	php-fpm;
# Порядок важен - строчка "include fastcgi_params" должна быть первой
include fastcgi_params;
fastcgi_split_path_info			^(.+?\.php)(/.*)?$;
# Вместо переменной "$document_root" можно указать адрес к корневому каталогу сервера и это желательно (см. http://wiki.nginx.org/Pitfalls)
fastcgi_param	SCRIPT_FILENAME		$document_root$fastcgi_script_name;
fastcgi_param	PATH_TRANSLATED		$document_root$fastcgi_script_name;
# См. http://trac.nginx.org/nginx/ticket/321
set		$path_info		$fastcgi_path_info;
fastcgi_param	PATH_INFO		$path_info;
# Additional variables
fastcgi_param	SERVER_ADMIN		email@example.com;
fastcgi_param	SERVER_SIGNATURE	nginx/$nginx_version;
fastcgi_index	index.php;

touch /etc/nginx/common/cache
nano /etc/nginx/common/cache

location ~* ".+\.(?:ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|css|swf|js|atom|jpe?g|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$"
{
	access_log	off;
	log_not_found	off;
	expires		max;
}

В моем случаи, у меня был публичный wildcard сертификат для моего домена. Распаковываю файл сертификата и устанавливаю права доступа

 openssl pkcs12 -in certname.pfx -nocerts -out  /etc/nginx/ssl/key.pem -nodes
 openssl pkcs12 -in certname.pfx -nokeys -out /etc/nginx/ssl/cert.pem
 openssl rsa -in /etc/nginx/ssl/key.pem -out /etc/nginx/ssl/domain.key 

cd /etc/nginx/ssl/
chown www-data:www-data domain.key
chmod 400 domain.key

touch /etc/nginx/common/ssl
nano /etc/nginx/common/ssl

ssl_certificate /etc/nginx/ssl/domain.crt;
ssl_certificate_key /etc/nginx/ssl/domain.key;

ssl_session_timeout                     20m;    # время 20 минут
ssl_session_cache                       shared:SSL:20m; # размер кеша 20МБ
ssl_protocols                           TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers               on;
ssl_ciphers                             ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AE$

Настраиваем наш сервер:


nano /etc/nginx/sites-available/hotspot.domain.com
include common/upstream;
server
{
	listen	80;
	server_name hotspot.domain.com;
	root	/var/www;
	index	index.php index.html index.htm;
	client_max_body_size	200m;	# увеличение максимального объема файла для загрузки до 200МБ
	# Buffers
	fastcgi_buffers 64 4K;
	include common/security;
	include common/gzip;
	location "/"
	{
		index index.php index.html index.htm;	# варианты индексных файлов если имя файла в запросе не задано
		try_files	$uri $uri/	=404;	# проверить есть ли файл из запроса на диске, иначе - вернуть ошибку 404
		include common/deny;
		include common/cache;
		include common/php-fpm;
	}
}

и делаем редирект с сервера по умолчанию (не забываем, что DNS сервер должен резолвить имя hotspot.domain.com):

nano /etc/nginx/sites-available/default
server {
        listen 80 default_server;    
        #listen [::]:80 default_server; 
        listen 443 ssl default_server;      
        #listen [::]:443 ssl default_server;  
        include common/ssl;
        rewrite ^ http://hotspot.domain.com?url=$scheme://$host$request_uri? redirect;
...
}


Настраиваем phpmyadmin


Для более удобной работы с базой данных, устанавливаем phpmyadmin.

apt-get install phpmyadmin
apt-get install mcrypt php5.6-mcrypt php5.6-mbstring php-gettext

touch /etc/nginx/common/phpmyadmin
nano /etc/nginx/common/phpmyadmin

 location /phpmyadmin {
        root /usr/share/;
        index index.htm index.html index.php;
        location ~ ^/phpmyadmin/(.+.php)$ {
            try_files $uri = 404;
            root /usr/share/;
            fastcgi_pass unix:/run/php/php5.6-fpm.sock; 
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $request_filename;
            include /etc/nginx/fastcgi_params;
        }
        location ~* ^/phpmyadmin/(.+.(html|ico|xml|css|jpg|png|js|txt|gif|jpeg))$ {
            root /usr/share/;
        }
    }
   location /phpMyAdmin {
        rewrite ^/* /phpmyadmin last;
    }

и аналогично подключаем к nginx

nano /etc/nginx/sites-available/hotspot.domain.com
	include common/phpmyadmin;

перезапускаемся

service nginx restart
service php5.6-fpm restart

Настройка фаервола iptables


В данном разделе, нам требуется ограничить доступ к серверу и завернуть трафик на прокси. Для начала, настроим сохранение всех правил после перезагрузки:

apt-get install iptables-persistent ipset

так как в нашей задаче, нам необходимо, чтобы сохранялись и таблицы ipset, то немного правим следующий файл:

nano /usr/share/netfilter-persistent/plugins.d/15-ip4tables
#в функцию save_rules()
	touch /etc/iptables/rules.v4
	touch /etc/iptables/ipset.rules
	chmod 0642 /etc/iptables/ipset.rules
	chmod 0640 /etc/iptables/rules.v4
	iptables-save > /etc/iptables/rules.v4
	ipset save > /etc/iptables/ipset.rules
#в функцию load_rules()
	ipset restore < /etc/iptables/ipset.rules
    iptables-restore < /etc/iptables/rules.v4 2> /dev/null

Для сохранения правил на фаерволе используем команду:

netfilter-persistent save

Для отслеживания и сброса установленных соединений используем conntrack

apt-get install conntrack

Настраиваем правила iptables

#создаем таблицу авторизованных mac-ip
ipset --create authorized macipmap --network 10.0.80.0/21
ipset -L authorized

#создаем дополнительные цепочки обработки правил
iptables -t nat -N toSQUID
iptables -t nat -N toHOTSPOT
#очищаем все ранее созданные правила
iptables -t nat -F PREROUTING

#пробрасываем DNS на наш сервер
iptables -t nat -A PREROUTING -i eth1 -p udp --dport 53 -j DNAT --to 10.66.66.1
#если ip,mac авторизован, то перенаправляем на squid
iptables -t nat -A PREROUTING -i eth1 -m set --match-set authorized src,src -j toSQUID
#если ip,mac не авторизован, то перенаправляем на заглушку
iptables -t nat -A PREROUTING -i eth1 -j toHOTSPOT

iptables -t nat -F toHOTSPOT
# в заглушке, все запросы перенаправляем на наш сервер nginx
iptables -t nat -A toHOTSPOT -p tcp -m multiport --dports 80,8080 -j DNAT --to-destination 10.0.87.254:80
iptables -t nat -A toHOTSPOT -p tcp -m multiport --dports 443 -j DNAT --to-destination 10.0.87.254:443
iptables -t nat -A toHOTSPOT -j RETURN

iptables -t nat -F toSQUID 
# перенаправляем разрешенные порты на прокси
#создаем обход хотспота для telegram (так и не смог заставить работать через squid)
iptables -t nat -A toSQUID -p tcp -d 149.154.164.0/22 --dport 443 -j ACCEPT
#аналогично для whatsapp
iptables -t nat -A toSQUID -p tcp -m multiport --dport 4244,5242,5228,5223,5222 -j ACCEPT
iptables -t nat -A toSQUID -p tcp -m multiport --dports 80,25,465,110,995,119,563,8080 -j REDIRECT --to-ports 3128
iptables -t nat -A toSQUID -p tcp -m multiport --dports 443 -j REDIRECT --to-ports 3129
iptables -t nat -A toSQUID -j RETURN

#Включаем NAT для прошедших пакетов
iptables -t nat -A POSTROUTING -s 10.0.80.0/21 -o eth0 -j MASQUERADE

#политика по умолчанию для входящих пакетов
iptables -P INPUT ACCEPT
iptables -F INPUT 
#разрешаем траффик на lo
iptables -A INPUT -i lo -j ACCEPT
#разрешаем доступ к портам управления из вышестоящей сети
iptables -A INPUT -i eth0 -p tcp -m multiport --dports 22,80,443 -j ACCEPT
#разрешаем пинг и трасерт
iptables -A INPUT -p icmp -m icmp --icmp-type 0 -j ACCEPT
iptables -A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT
#разрешаем работу уже открытых соединений
iptables -A INPUT -p TCP -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p UDP -m state --state ESTABLISHED,RELATED -j ACCEPT
#разрешаем доступ на прокси
iptables -A INPUT -i eth1 -p tcp -m multiport --dport 3128,3129,3130 -j ACCEPT
iptables -A INPUT -i eth1 -p udp -m multiport --dport 3128,3129,3130 -j ACCEPT
№разрешаем доступ на заглушку
iptables -A INPUT -i eth1 -p tcp -m multiport --dports 80,443 -j ACCEPT
#политика по умолчанию для входящих пакетов
iptables -P INPUT DROP

#создаем цепочку для проблемных сервисов, которым разрешена пересылка
iptables -N FORWARD_AUTHORIZED
iptables -F FORWARD_AUTHORIZED 
# разрешаем пересылку telegram
iptables -A FORWARD_AUTHORIZED -p tcp -d 149.154.164.0/22 --dport 443 -j ACCEPT
# разрешаем пересылку whatsapp
iptables -A FORWARD_AUTHORIZED -p tcp -m multiport --dport 4244,5242,5228,5223,5222 -j ACCEPT
iptables -A FORWARD_AUTHORIZED -j RETURN

#политика по умолчанию для пересылаемых пакетов
iptables -P FORWARD ACCEPT
iptables -F FORWARD 
#разрешаем пересылку на dns
iptables -A FORWARD -i eth1 -p udp --dport 53 -d 10.66.66.1 -j ACCEPT
#разрешаем работу уже открытых соединений
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
#перенаправляем авторизованных клиентов на проверку в цепочку FORWARD_AUTHORIZED
iptables -A FORWARD -i eth1 -m set --match-set authorized src,src -j FORWARD_AUTHORIZED
#политика по умолчанию для пересылаемых пакетов
iptables -P FORWARD DROP

Для отлавливания проблем используем следующие команды

#сброс установленных соединений с заданного ip
conntrack -D conntrack --orig-src 10.0.8X.XXX
#просмотр новых записей в логе
tail -f /var/log/firewall | grep 10.0.8X.XXX

Настраиваем php файлы обработки запросов и скрипты


Создаем таблицы в нашей базе данных:

SET FOREIGN_KEY_CHECKS=0;
CREATE TABLE IF NOT EXISTS `mac-auth` (
  `mac` char(17) NOT NULL COMMENT 'мак адрес устройства',
  `code` int(6) NOT NULL COMMENT 'код авторизации',
  `phone` varchar(15) NOT NULL DEFAULT '' COMMENT 'телефонный номер с которого пришло смс',
  `updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'дата последнего изменения',
  `created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'дата создания',
  UNIQUE KEY `mac` (`mac`),
  KEY `code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `mac-ip` (
  `mac` char(17) NOT NULL COMMENT 'мак адрес устройства',
  `ip` varchar(15) NOT NULL COMMENT 'ip адрес устройства',
  `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'дата изменения',
  KEY `mac` (`mac`),
  KEY `ip` (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `mac-phone` (
  `mac` char(17) NOT NULL COMMENT 'мак адрес устройства',
  `phone` varchar(15) NOT NULL COMMENT 'номер телефона',
  `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'дата изменения',
  KEY `mac` (`mac`),
  KEY `ip` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `mac-ip`
  ADD CONSTRAINT `mac` FOREIGN KEY (`mac`) REFERENCES `mac-auth` (`mac`) ON DELETE CASCADE ON UPDATE CASCADE;

SET FOREIGN_KEY_CHECKS=1;

создаем скрипт, при входе в систему, для проверки работы всех служб:

nano /var/www/check_services.sh

#!/bin/bash
function check {
	printf %-30s "check $1"
	if (( $(ps -ef | grep -v grep | grep $1 | wc -l) > 0 ))
	then
		echo -e "[\033[32;1m OK \033[0m]"
	else
		echo -e "[\033[31;1m ERROR \033[0m]"
	fi
}

check dhcpd
check nginx
check mysql
check php-fpm 
check smstools
check squid
check usbsrvd

устанавливаем права запуска и добавляем на старт

chmod 755 /var/www/check_services.sh
nano  ~/.profile
     /var/www/check_services.sh

а теперь исходники php файлов:

nano /var/www/sms_recieve.php

#!/usr/bin/php
<?php
require_once 'hotspot/connect.php';

$sms_type = $argv[1];
$sms_file = $argv[2];

$sms_file_content = file_get_contents($sms_file);
$i = strpos($sms_file_content, "\n\n");
$sms_headers_part = substr($sms_file_content, 0, $i);
$sms_message_body = substr($sms_file_content, $i + 2);
$sms_header_lines = split("\n", $sms_headers_part);
$sms_headers = array();

foreach ($sms_header_lines as $header)
{
  $i = strpos($header, ":");
  if ($i !== false)
    $sms_headers[substr($header, 0, $i)] = substr($header, $i + 2);
}

$phone = (float)$sms_headers['From'];
$code = (int)$sms_message_body;

//проверяем базу на старые записи
$interval = '30 DAY';
//$interval = '1 MINUTE';

$macs = sql_getRows('SELECT `mac` FROM `mac-auth` WHERE `code` = 1 AND `updated` < DATE_SUB(NOW(),INTERVAL '.$interval .')');
if (!empty($macs)){
	$data = sql_getColumn('SELECT DISTINCT(`ip`) as `ip`,`mac` FROM `mac-ip` WHERE `mac` IN ("'.implode('","',$macs).'") ORDER BY `date` DESC','mac');
	foreach ($data as $mac=>$ip){
		sql_query('UPDATE `mac-auth` SET `code` = "0" WHERE `mac` IN ("'.implode('","',$macs).'")');
		//удаляем авторизацию на фаерволе
		shell_exec('sudo ipset -D authorized '.$ip .','.$mac);	
		//сбрасываем все подключения данного ip
		shell_exec('sudo conntrack -D conntrack --orig-src '.$ip);		
	}
	shell_exec('sudo ipset save > /etc/iptables/ipset.rules');
}

echo $phone.'-'.$code;
$mac = sql_getValue('SELECT `mac` FROM `mac-auth` WHERE `code` = '.$code);
if ($mac){
	$ip = sql_getValue('SELECT DISTINCT(`ip`) FROM `mac-ip` WHERE `mac` = "'.$mac.'" ORDER BY `date` DESC');
	echo '-'.$mac.'-'.$ip;
	//вносим в базу информацию о номере телефона
	sql_query('INSERT `mac-phone` (`mac`,`phone`) VALUES("'.$mac.'","'.$phone.'")');
	sql_query('UPDATE `mac-auth` SET `phone` = "'.$phone.'", `code` = "1" WHERE `mac` = "'.$mac.'"');
	//разрешаем доступ на фаерволе	
	shell_exec('sudo ipset -A authorized '.$ip .','.$mac);	
	shell_exec('sudo ipset save > /etc/iptables/ipset.rules');	
	shell_exec('sudo conntrack -D conntrack --orig-src '.$ip);	
}

//удаляем сообщение
unlink($sms_file);
?>

nano /var/www/hotspot/index.php

<?php
//устанавливаем подключение с базой данных
require_once 'connect.php';

//определяем ip адрес
$ip = '';
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
	$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} 
if (empty($ip)) $ip = $_SERVER['REMOTE_ADDR'];
//проверяем правильность ip адреса
if (!preg_match('/^10\.0\.8[0-7]\.\d{1,3}$/', $ip)) {
	$ip = false;
}

//определяем мак адрес
if ($ip) $mac = trim(shell_exec("arp -a ".$ip." | awk '{print $4}'"));
if (!isset($mac) || $mac == "entries") $mac = false;

//определяем адрес по которому обращался пользователь
$url = $_GET['url'];
//if (strpos($url, 'gstatic.com')!==false) $url = 'http://www.domain.ru';

//выключаем кэширование браузером
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Дата в прошлом

// строка ошибки
$error = false;
if (!$mac){
	$error = 'Ошибка с Вашей сетевой картой. <br/>Доступ в интернет не может быть предоставлен.<br/><font class="eng error">Error with your network card. Internet access can not be granted</font>';
} else {
	//проверяем, не изменился ли ip адрес у абонента
	$base_ip = sql_getValue('SELECT DISTINCT(`ip`) FROM `mac-ip` WHERE `mac` = "'.$mac.'" ORDER BY `date` DESC');
	if ($base_ip != $ip){
		sql_query('INSERT INTO `mac-ip` (`mac`,`ip`) VALUES ("'.$mac.'","'.$ip.'")');
	}
	//проверяем авторизацию мак адреса
	$code = sql_getValue('SELECT `code` FROM `mac-auth` WHERE `mac` = "'.$mac.'"');
	switch ($code){
		case '1':
			//абонент авторизован
			shell_exec('sudo ipset -A authorized '.$ip .','.$mac);	
			shell_exec('sudo ipset save > /etc/iptables/ipset.rules');	
			//сбрасываем все подключения данного ip, иначе TOO_MANY_REDIRECTS
			shell_exec('sudo conntrack -D conntrack --orig-src '.$ip);						
			header('Location: '.$url);		
			die;
			break;
		case '':
			//новый абонент
			sql_query('INSERT INTO `mac-auth` (`mac`,`code`) VALUES ("'.$mac.'",0)');
		case '0':
			//абонент без авторизации и без кода доступа
			//генерируем код		
			do {
				$code = rand(10000,99999);
			} while(sql_getValue('SELECT `mac` FROM `mac-auth` WHERE `code` = "'.$code.'"'));	
			sql_query('UPDATE `mac-auth` SET `code` = '.$code.' WHERE `mac` = "'.$mac.'"');			
		default: 
			//абонент без авторизации, но с генерированным кодом
			
			break;		
	}	
}
?>
<!DOCTYPE HTML>
<html>
<head>
<title>Хотспот</title>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<meta name="viewport" content="width=device-width, initial-scale=0.8, maximum-scale=1" />
</head>
<body>
<style type="text/css">

html, body {
	width: auto;
	margin: 0px;
	padding: 0px;
}
* {
    font-family: "Tahoma", sans-serif;
	font-size: 14px;
	text-align:center; 
	font-style: normal;
    font-variant: normal;
    font-weight: normal;
	color: #000;
}
.center {
	width: 400px; 
	padding: 10px; 
	margin: auto; 
}
.logo {
	background-image: url("/logo2.gif");
	width: 177px;
	height: 209px;
	margin: auto;
}
.header {
	font-size: 16px;
	padding: 10px;
}
.repeat {
	padding: 10px; 
}
.sms {
	font-size: 11px;
	color: #4c4b4b;
	padding: 10px; 
}
.law {
	font-size: 11px;
	color: #4c4b4b;
	padding: 10px; 
}
.code {
	font-size: 16px;
	font-weight: bold;
}
.num {
	font-size: 16px;
	font-weight: bold;
}
.internet-disabled {
    background-color: #dadbdb;
    color: white;
    padding: 7px 20px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 16px;
    border: 2px solid #ffffff;
    border-radius: 4px;
    box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);         
}
.internet {
    background-color: #3e315f;
    color: white;
    padding: 7px 20px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 16px;
    border: 2px solid #ffffff;
    border-radius: 4px;
    box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);	
}
.internet:hover {
    background-color: #ffffff;
    color: #3e315f;
	border: 2px solid #3e315f;
	box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
}
.eng {
	font-size: 12px;
	line-height: 12px;
}
.error {
	color: #d11313;
}
</style>
<?php if (!$error) {?>
<script type="text/javascript">
window.onload = function(){
    window.htime = 60;   
    check = function(){
        if (window.htime <=0 ) {
            return true;
        } else {
            return false;
        }
    }          
    window.htimer = window.setInterval(function(){
        document.getElementById('button').innerHTML = 'осталось ' + window.htime + ' сек.';
        window.htime -= 1;
        if (window.htime <= 0) {
            window.clearInterval(window.htimer);
            document.getElementById('button').className  = "internet";
            document.getElementById('button').innerHTML = 'перейти в интернет';
        }
    }, 1000);           
}
</script>
<?php } ?>

<div class="center">
	<div class="logo"></div>
    <div class="header">Добро пожаловать в открытую сеть <br/>Общественной палата Российской Федерации. <font class="eng">Welcome to the open network of the Civic Chamber of the Russian Federation.</font></div>
   <?php if ($error) { echo '<div class="error">'.$error.'</div>';} else { ?>
	<div>Для получения доступа в интернет вам необходимо отправить SMS с кодом: <font class="code"><?php echo $code;?></font> на номер: <font class="num">+7 917 XXX-XX-XX.</font><br/><font class="eng">To access Internet, text this code <b><?php echo $code;?></b> to number <b>+7 917 XXX-XX-XX.</b></font></div>
	<div class="repeat">После отправки SMS нажмите на кнопку: <br/><font class="eng">After you've sent SMS with code, please click this button:</font></div>
	<a target="_self" href="<?php echo $url;?>" id="button" class="internet-disabled" onclick="return check();"> </a>
	<div class="repeat">Доставка и обработка вашего SMS может занять некоторое время. Пожалуйста, подождите несколько секунд.<br/><font class="eng">Please allow a minor delay in providing you access — delivery and processing of your SMS might take a few moments. Thank you for your patience.</font></div>
	
	<div class="sms">Стоимость SMS-сообщений тарифицируется согласно вашему тарифному плану.<br/>Cost of SMS-messages is charged according to your tariff plan.</div>
	<div class="law">Согласно постановлению Правительства №758 от 31 июля 2014г. и №801 от 12 августа 2014 г. - все публичные WIFI сети обязаны производить идентификацию пользователей.<br/>We require our WiFi users to authenticate following the provisions of the Russian law, stated by enactments 758 of 31st July 2014 and 801 of 12th August 2014. </div>
   <?php } ?>
</div>
</body>
</html>

nano /var/www/hotspot/connect.php

# mysql login params
$mysql_host = '';
$mysql_db = '';
$mysql_login = '';
$mysql_password = '';

$sql_link = mysqli_connect($mysql_host, $mysql_login, $mysql_password, $mysql_db) or die(mysql_error());
function sql_query($sql, $unbuffered = false){		
	global $sql_link;
	$resource = $unbuffered ? mysqli_real_query($sql_link, $sql) : mysqli_query($sql_link, $sql);
	if (mysqli_errno($sql_link) === 1016) {
		if (preg_match("/'(\S*)\.MYD'\. \(errno\: 145\)/", mysqli_error($sql_link), $m)) {
			mysqli_unbuffered_query("REPAIR TABLE ".$m[1]);
			$resource = $unbuffered ? mysqli_unbuffered_query($sql_link, $sql) : mysqli_query($sql_link, $sql);
		}
	}
	return $resource;
}

function sql_getValue($sql, $unbuffered = false){
	$resource = sql_query($sql, $unbuffered);
	$ret = false;
	if (is_resource($resource) || is_object($resource)) {
		$row = mysqli_fetch_row($resource); 
		$ret = $row[0];
	}
	return $ret;
}

function sql_getRows($sql, $use_key = false, $unbuffered = false){
	$resource = sql_query($sql, $unbuffered);
	$ret = false;
	if (is_resource($resource) || is_object($resource)) {
		$row = true;
		while ($row) { 			 
			$row = mysqli_fetch_array($resource, MYSQLI_ASSOC);  
			if (!$row) continue;
			$_row = $row;
			if ($use_key) {
				if (isset($_row[$use_key])) {
					$link = &$ret[$_row[$use_key]];
					$link = $_row;
				}
			} else {
				$link = &$ret[];
				$link = (count($_row) == 1) ? array_shift($_row) : $_row;
			}
		}
	}
	return $ret;
}	

function sql_getColumn($sql, $id_field = 'id', $unbuffered = false){
	$resource = sql_query($sql, $unbuffered);
	$ret = false;
	if (is_resource($resource) || is_object($resource)) {
		$row = true;
		while ($row) { 			 
			$row = mysqli_fetch_array($resource, MYSQLI_ASSOC);  
			if (!$row) continue;
			$_row = $row;
			$id = $_row[$id_field];
			unset($_row[$id_field]);
			$value = current($_row);			
			$ret[$id] = $value;				 			
		}
	}
	return $ret;
}

function sql_getRow($sql, $unbuffered = false){
	$resource = sql_query($sql, $unbuffered);
	$ret = false;
	if (is_resource($resource) || is_object($resource)) {
		$ret = mysqli_fetch_assoc($resource);          
	}
	return $ret;
}

Ну а теперь, добавим в sudoers права для управления iptables из php


nano /etc/sudoers
	smsd	ALL=(ALL) NOPASSWD: /sbin/ipset
	smsd	ALL=(ALL) NOPASSWD: /sbin/iptables
	www-data	ALL=(ALL) NOPASSWD: /sbin/ipset
	www-data	ALL=(ALL) NOPASSWD: /sbin/iptables
	smsd	ALL=(ALL) NOPASSWD: /usr/sbin/conntrack
	www-data	ALL=(ALL) NOPASSWD: /usr/sbin/conntrack

На этом вроде всё. Система в работе уже месяц, полёт нормальный. Спасибо за внимание!
Tags:
Hubs:
+30
Comments35

Articles