Pull to refresh

Безопасный доступ к умному дому при отсутствии публичного IP (часть 2)

Reading time7 min
Views6.2K

Вступление


В первой части я писал о постановке задачи и как трансформировались хотелки. В итоге я решил использовать OpenVPN, но, всвязи с тем, что решил все запускать в Docker контейнерах, это получилось не так-то просто.

Сразу скажу, что потом я опять все переделал, в итоге отказался от внешнего VPS. Однако, поскольку все в контейнерах, в процессе столкнулся с рядом интересных особенностях, о коих и пойдет речь.

Установка


Опишу только ключевые моменты.

ioBroker


docker run -d --name iobhost  --net=host -v /opt/iobroker/:/opt/iobroker/ --device=/dev/ttyACM0 --env-file /opt/ioBroker_env.list --restart=always buanet/iobroker:latest

Поскольку у меня есть MiHome Gateway, к нему подключены датчики, даже настроено несколько сценариев, которые я не хочу пока ломать, я подключил к нему ioBroker. Он увидел датчики, не пришлось их перепривязывать к Zigbee stick (хотя тоже есть, и кое-какие кнопки подключены к нему).

Вот для того, чтобы ioBroker связался с MiHome Gateway, и пршлось запускать его с параметром --net=host. Т.е. он использует интерфейс хоста, указывать, какие порты пробрасывать в контейнер не нужно. Без этого он шлюза не видел, ибо тот работает через мультикасты.

Параметр --device=/dev/ttyACM0 нужен для проброса Zigbee USB stick в контейнер. Также в /opt/ioBroker_env.list пришлось добавить строчку USBDEVICES="/dev/ttyACM0". Важно, что эта строчка должна присутствовать в момент первого запуска контейнера, когда он видит пустую директорию и начинает свою первичную настройку.

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

MQTT сервер


На внешнем VPS запустил eclipse mosquito. Сначала настроил ему TLS, выписав сертификат Let’s encrypt. Потом решил, что клиенты должны обязательно предъявить сертификат, и только потом уже имя и пароль (защита от bruteforce). Так что переделал на самоподписанный, чтобы клиентам выписывать сертификаты.

OpenVPN


Использовал популярные образы с Docker Hub. Для сервера (VPS) kylemanna/openvpn. Для клиента (домашний сервер, где установлен ioBroker) — ekristen/openvpn-client.

Настройка


Вот здесь пришлось повозиться. В процессе хорошо прочувствовал сетевые аспекты докера, работа с iptables, узанл новое, в т.ч. netplan, с которым раньше дела не имел. Собственно поэтому и решил написать эту статью.

Сервер VPS


С установкой OpenVPN и настройкой все стандартно, как на https://hub.docker.com/r/kylemanna/openvpn написано.

Что сделал дополнительно, так это поднял дополнительные loopback на VPS и домашнем сервере.

/etc/netplan/01-netcfg.yaml

	# This file describes the network interfaces available on your system
	# For more information, see netplan(5).
	network:
	  version: 2
	  renderer: networkd
	  <b>ethernets:
	    lo:
	      renderer: networkd
	      match:
	        name: lo
	      addresses:
        - 192.168.16.1/32</b>

Жирным выделил «добавку». Пробелы, похоже, не отображаются, хотя они очень важны.

Адреса в примерах буду использовать 192.168.6.0 для домашней сети и 192.168.16.0 для loopback. Постараюсь нигде не ошибиться.

В openvpn.conf добавил

server 192.168.16.192 255.255.255.192
push "dhcp-option DNS 192.168.16.6"
push "route 192.168.16.0 255.255.255.128"
client-to-client
client-config-dir       /etc/openvpn/ccd/

Специально сделал loopback из 192.168.16.0/25, а выдаю адреса клиентам из 192.168.16.128/25, чтобы впоследствии ращзрешающие правила в iptables настраивать одной сеткой 192.168.16.0/24

Итак, у VPS loopback 192.168.16.1, на нем же mqtt.

У домашнего сервера 192.168.16.6. Там же iobroker, он же dns для домашних клентов и переопределяет ряд доменных имен для подключающихся из домашней сети или через VPN.
Была мысль везде прописать его «реальный» IP из сети. Типа в ccd/iobroker указать
iroute 192.168.6.6 255.255.255.255

Но, видимо от того, что это адрес из сети на WiFi интерфейсе домашнего ноутбука, да еще является во многих случаях шлюзом по умолчанию, были глюки. В том числе со смартфоном. А мне хотелось, чтобы из домашней сети работало как с активным клиентом, так и без него. И не приходилось судорожно отключать клиента, придя домой, чтобы получить доступ к другим ресурсам.

Поэтому настроил, чтобы этот сервер и его контейнеры всегда взаимодействовали с loopback, независимо от того, активен ли VPN. И к этому же адресу шло обращение.

Так что в ccd/iobroker

iroute 192.168.16.6 255.255.255.255

Хорошо, все VPN клиенты знают, что сеть 192.168.16.0/24 доступна через VPN. Если они шлют пакеты на 192.168.16.1 (loopback VPS), пакет шифруется, попадает в контейнер openvpn, расшифровывается, по маршруту по умолчанию идет на 172.17.0.1 (default gateway в контейнерах по умолчанию), попадает на хост, все хорошо.

Но как мне с VPS хоста «пингануть» VPN клиента или обратиться к домашнему серверу с адресом 192.168.16.6 (а не его временному IP на VPN туннеле, который находится внутри контейнера OpenVPN)?

Очевидно, что сетку 192.168.16.0 надо направить в контейнер OpenVPN. Я, конечно, могу посмотреть, что он 172.17.0.3. Но в один прекрасный день может и поменяться.

Был бы OpenVPN развернут прямо на сервере, а не в контейнере, все заработало бы само. А тут пришлось делать хитро. Создаю скрипт, который отрабатывается самым последним в system, и в него помещаю:

ipaddr=$(docker inspect -f '{{.NetworkSettings.IPAddress}}' vpn-client)
route add -net 192.168.16.0/24 gw $ipaddr

Т.е. через docker inspect узнаю IP адрес запущенного контейнера, а потом на него маршрутизирую сетку обычным порядком.

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

Создать файл /etc/systemd/system/custom.target:

[Unit] 
Description=My Custom Target
Requires=multi-user.target
After=multi-user.target
AllowIsolate=yes

Создать файл /etc/systemd/system/last_command.service

[Unit]
Description=My custom command
After=multi-user.target
[Service]
Type=simple
ExecStart=/usr/local/bin/my_last_command.sh
[Install]
WantedBy=custom.target

Создать директорию /etc/systemd/system/custom.target.wants

ln -s /etc/systemd/system/last_command.service \   /etc/systemd/system/custom.target.wants/last_command.service
systemctl daemon-reload
systemctl set-default custom.target

При желании запустить сразу, не дожидаясь перезагрузки: systemctl isolate custom.target
Вот теперь после перезагрузки последним будет запускаться файл /usr/local/bin/my_last_command.sh, описанный в ExecStart.

Iptables


На mqtt сервере у меня поднято 2 порта: 8883 с TLS и аутентификацией, доступен из Интернета для удаленных датчиков. Да и сам могу каким-нибудь MQTT Explorer подключиться и проверить, что и как.

1883 уже без TLS, требует только имя и пароль. Нужен для домашнего Sonoff rfBridge, который TLS не умеет.

Это не страшно, поскольку из дома трафик пойдет на сервер с iobroker, который является шлюзом по умолчанию для 192.168.16.0, он перешлет пакет в контейнер с OpenVPN и т.п. Однако необходимо разрешить доступ к порту 1883 только «изнутри». Т.е. iptables.

Стандартный подход – запретить доступ к этому порту отовсюду, потом выполнить правило, разрешающее доступ из внутренних сетей.

iptables -I INPUT -p tcp -m tcp --dport 1883 -j DROP
iptables -I INPUT -s 172.17.0.0/24 -p tcp -m tcp --dport 1883 -j ACCEPT

Сетка здесь указана 172.17.0.0, поскольку из OpenVPN «к соседям» я в итоге хожу как hide NAT (что контейнер iobroker что rfBridge из WiFi сети), а не с оригинальных.

И так работает. Но есть нюанс. У меня порт 1883 проброшен в контейнер mqtt. И, как оказалось, iptables сначала отрабатывает цепочку DOCKER-USER.

Т.е при таком правиле доступ к порту 1883 разрешался _до_ блокирующего правила. И из Интернета к нему тоже можно было спокойно подключиться.

Блокирующее правило надо создавать в цепочке DOCKER-USER!

iptables -I DOCKR-USER -p tcp -m tcp --dport 1883 -j DROP
iptables -I INPUT -s 172.17.0.0/24 -p tcp -m tcp --dport 1883 -j ACCEPT

А вот нижнее, разрешающее доступ из внутренних сетей, почему-то требует INPUT.

Домашний сервер


Основная часть такая же. Однако он хоть и клиент, но должен был маршрутизировать трафик из WiFi сети (rfBridge) в mqtt на VPS. Т.е. движение трафика:

rfBridge (192.168.6.8) -> iobroker host (192.168.6.6) -> контейнер vpn-client (172.17.0.?) -> контейнер opevpn на VPS -> loopback VPS (192.168.16.1) -> контейнер mqtt (порт 1883)

Разрешаем «форвардить» пакеты для сетки с клиентами и лупбэками:

iptables -A FORWARD -d 192.168.16.0/24 -j ACCEPT

и переходим к контейнерной специфике.

Задача 1


Как вы помните, взаимодействие между подсистемами у меня настроено через loopback. Т.е. нужно, чтобы пакеты на 192.168.16.6 из vpn-clientушли на хост (172.17.0.1), а не в VPN туннель.

Если в запущенном контейнере выполнить такую команду, все заработает. После перезагрузки это забудется, но в конфигурационном файле iobroker.ovpn можно указать
route 192.168.16.6 255.255.255.255 172.17.0.1.

И openvpn этот маршурт будет устанавливать при старте контейнера. Это решено легко стандартным способом.

Задача 2


Пакеты из домашней сети 192.168.6.0 (например, от rfBridge) попадают на хость iobroker и форвадятся в контейнер vpn-client.

Однако сеть 192.168.6.0 я умышленно не включаю в домен шифрования, контейнер OpenVPN на VPS не знает, что с этой сеткой делать. Очевидное решение – сделать NAT внутри vpn-client, чтобы пакет на VPS пришел с его адреса. Но есть нюанс. Как сохранить требуемые команды iptables после рестарта контейнера? Iptables-persistent туда так просто не поставишь.

Можно, конечно, собрать новый контейнер с добавками. Но не хочется, ибо усложнится процедура апгрейда. Вместо «убил и запустил latest, а конфигурацию он подтянул из подмонтированной папки» нужно будет запускать сборку… Не для того я с контейнерами связываюсь.

Поэтому решил «после старта контейнера принудительно выполнять в нем команды, указывающие iptables делать NAT». Для этого воспользовался командой
docker events --filter "container=vpn-client" --filter "event=start".

Она висит и ждет события, заданного в фильтрах. В моем случае старта контейнера. После чего через docker exec выполняю в нем с хоста требуемые команды.

Для этого по аналогии с VPS настраиваю /usr/local/bin/my_last_command.sh

#!/bin/bash
cont="vpn-client"
ipaddr=$(docker inspect -f '{{.NetworkSettings.IPAddress}}' $cont)
route add -net 192.168.16.0/24 gw $ipaddr
for (( ; ; ))
do
  docker events --filter "container=$cont" --filter "event=start"
  docker exec -it $cont iptables -t nat -I POSTROUTING -s 192.168.16.0/255.255.255.0 -j MASQUERADE
  docker exec -it $cont iptables -t nat -I POSTROUTING -s 192.168.6.0/255.255.255.0 -j MASQUERADE
done

Есть примеры, в которых вывод docker events передается на вход awk, который выполняет команды. Но мне показалось проще «повисеть» до наступления события, выполнить команды, и опять ждать события.

Заключение


Честно говоря, мне не хотелось писать этот пост. Интересный опыт приобрел, но «не красиво» получилось, слишком сложно, я так не люлю. Так что я опять все переделал, отказался от VPS вообще. Но раз уж обещал вторую часть … Кроме того, меня впечатлил подход с docker events, захотелось им поделиться. Думаю, он еще пригодится.

В итоге я решил так.
Коль скоро мне не удалось опубликовать vis через reverse proxy, а mqtt я запросто могу взять «извне» как сервис, VPS для этой задачи мне и не нужен. Выкладывать прошивки для обновления по OTA я могу и на хостинг, благо тоже есть.

Поэтому.

Mqtt взял на wqtt.ru. TLS есть (пароли шлются в защищенном виде). Скорость прекрасная (10ms против 80ms у mymqtthub). Топики переписывать на '$device/<безумный ID>/events' (как у Яндекса) не требуется. Т.е. в случае чего перескочить куда-то можно элементарно. Цена копеечная (300 руб в год).

Firmware для OTA выкладываю на имеющийся на хостинг.

Доступ к vis – все-таки через Zerotier. Уж очень просто и удобно. А если их и сломают, так вряд ли ради посмотреть уровень CO2 у меня дома. И даже если такое произойдет, это скорее станет известным, чем если ломанут меня лично.

Все красиво, работает без сбоев, изменения при необходимости внести легко, лишних серверов, за которыми нужен уход, не появилось, я доволен.
Tags:
Hubs:
0
Comments8

Articles