Публикация локального сервера из дома в интернет

Оригинал поста на хабре

Приветики. Надеюсь, все отошли от новогодних, и можно писать и читать дальше. Как хозяин умного дома, я состою в чатике по Home Assistant, там прекрасное отзывчивое комьюнити, но периодически задаётся вопрос по тому, как собственно выставить свой веб сервис в интернет. И оказывается, что в двух словах тут не ответишь, а вменяемой инструкции на которую можно дать ссылку - нет. Так что теперь она будет здесь.

Рокет сайнса здесь не встретите, и в целом все эти вещи справедливы и работают уже минимум лет 10, просто не так тривиально понять, какой именно запрос нужно задать в гугл, и что делать.

Мы рассмотрим здесь несколько сценариев - статический белый айпи, динамический белый айпи, и серый. Для серого рассмотрим варианты с готовыми сервисами, с помощью Keenetic и с помощью ssh туннеля. Погнали!

Дисклеймер. Если вы собираетесь хостить обычный веб сайт, визитку, магазин и так далее - автор настоятельно рекомендует вам не страдать фигнёй, а развернуться целиком где-то в облаке. Домашний сервер оправдан для локальных сценариев вроде умного дома или хранилища (которое при этом резервируется в веб), но в долгосрочной перспективе принесёт вам боль и страдания, если вы положите туда что-то, что не должно там лежать и имеет требования по отказоустойчивости.

1. Белый статический айпи

Если вы являетесь счастливым обладателем статического внешнего айпи адреса, то и делать-то особо ничего не надо. Просто настраиваем свой сервер или роутер как веб сервер, максимум - пробрасываем с роутера порты.

Минусы такого варианта - нужно озаботиться безопасностью сервера, закрыть все лишние порты, а ещё вы оказываетесь прибиты гвоздями к этому физическому месту и своему провайдеру.

Если у вас нет белого статического айпи - переходим к пункту 2.

2. Белый динамический айпи

Кажется, это уже экзотика, но встречается до сих пор. Проще всего настроиться через KeenDNS, который есть на роутерах Keenetic, DynDNS, DuckDNS или любой другой аналогичный сервис. Минусы такого решения те же, что со статическим адресом.

Если у вас нет белого динамического айпи - переходим к пункту 3. Он подойдёт для любой ситуации.

3. Серый айпи или закрытые порты на доступ извне

Первые два варианта я упомянул скорее для формальности, потому что большая часть пользователей находится именно на серых айпи адресах, и без каких-то нестандартных финтов ушами опубликовать себя в интернет не сможет. Что же делать? Тут есть несколько вариантов.

3.1 Используем возможности роутера

Как я уже упомянул ранее, у Keenetic есть сервис KeenDNS. И у него есть два режима работы. Direct - это собственно обычный DNS сервис, который поможет, если у вас публичный айпи адрес. И Cloud, который на самом деле не имеет никакого отношения к DNS, а просто прокидывает туннель к вашей локальной сети через облако Keenetic.

Вот вариант с облаком нам поможет в этой ситуации - парой галок можно получить свой облачный домен для сервиса, прокинуть порт от устройства в локальной сети наружу, сделать там Basic Auth, и ещё и SSL сертификат на него навесить. Просто сказка!

Лет 8 им пользовался, ни единого разрыва. Однако, минусы тоже есть:

  • По умолчанию не прокидывается заголовок хоста. Поэтому я все эти годы мучался и вешал сервисы на разные порты. Прямо перед написанием статьи я всё же смог нагуглить решение и прокинуть заголовок - но это прям неочевидно.
  • А вот Referer и Origin прокинуть так и не удаётся. Хотя в реальной жизни я на это наступил ровно один раз, когда Authentik отказался пропускать мои запросы, защитившись от них при помощи CSRF. Пришлось руками прописать то, что должно быть в заголовках. Это, конечно, сводит защиту CSRF на ноль, но… Защита без токенов всё равно такое себе.
  • Ничто не вечно под луной, и в какой-то момент сервис может прилечь или прекратить работу, и тогда весь ваш веб станет резко недоступен
  • Паранойя может помешать вам гонять личный трафик через облако вендора роутера
  • Вы не сможете поменять роутер на роутер другой фирмы, и получите вендор лок
  • Скорее всего, на пропускную способность такого туннеля есть какое-то ограничение. В моих юз кейсах я с ним не сталкивался, но здравый смысл подсказывает, что его не может не быть.

Ну и основная проблема при использовании такого решения - у вас может быть роутер другой фирмы. Не знаю, есть ли у них аналогичные сервисы, не встречал, может кто в комментариях поделится. Но если нету - идём дальше по списку решений!

3.2 Используем сторонний сервис

Есть немало сервисов, которые бесплатно или за символические деньги заниматся тем же, что KeenDNS - прокидывают вам туннель наружу.

Навскидку могу перечислить:

  • pinggy.io
  • localtonet.com
  • cloudflare tunnel
  • cloudflare zero trust
  • zrok кажется тоже умеет при self hosted установке, но я не осилил найти инструкцию
  • вроде у google тоже есть какое-то туннелирование
  • ngrok может за деньги дать вам кастомный домен, или бесплатно - постоянный, но случайно сгенерированный.

В общем, вариантов много, можно найти что понравится. Минусы примерно те же самые, что у KeenDNS, за исключением отвязки от вендора вашего роутера.

Не нравится? Ну что же, для упоротых упорных есть уровень хардкора!

3.3 Используем собственное облако и SSH туннель

Здесь должна быть картинка с троллейбусом из буханки хлеба, но мне лень её искать - вспомните и представьте самостоятельно.

Для данного решения вам потребуется:

  • Сервер на внешнем хостинге с белым айпи на Linux
  • Домашний сервер на Linux
  • Домен, в котором вы управляете зонами и можете наклепать себе удобных поддоменов
  • Базовое знание Linux

А кто говорил, что будет легко.

3.3.1 Настраиваем доступ по сертификату на свой сервер

Для начала нам понадобится настроить доступ по SSH с помощью сертификата. Под это есть много инструкций, больших и подробных, под любой дистрибутив - так что я не буду здесь пытаться их переплюнуть. Просто загуглите.

На выходе у вас должна с домашнего сервера отрабатывать команда ssh user_login@mydomain.com -i /home/path_to_home_dir/.ssh/private_key и по ней вы должны оказываться на своём сервере.

3.3.2 Прокидываем порт

Теперь мы воспользуемся особой серверной магией - при помощи ssh прокинем локальный порт на удалённый сервер.

ssh -R 8080:localhost:9000 user_login@mydomain.com -i /home/path_to_home_dir/.ssh/private_key -N

В этом примере порт 8080 нашей локальной машины прокидывается на 9000 порт удалённого сервера.

Подставьте вместо 8080 порт вашего приложения, а вместо 9000 - какой-нибудь свободный порт удалённого сервера. Тогда после выполнения команды ваш сервис должен стать доступен через ваш удалённый сервер по указанному порту.

Если не получилось - выставьте AllowTcpForwarding yes и GatewayPorts yes в sshd_config удалённого сервера и перезапустите ssh там.

Если всё ещё не получилось - временно выключите там фаервол или добавьте новый порт в его исключения.

Если всё ещё не получилось - stack overflow вам в помощь, задача супер стандартная.

3.3.2 Проксируем туннель через nginx

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

Предположим, что локальный сервис у вас крутится на порту 8123. Тогда конфиг сайта локального nginx будет примерно такой:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
upstream some_service {
server 127.0.0.1:8123;
}

server {
listen 8080;
server_name subdomain.mydomain.com;
access_log /var/log/nginx/service-access-remote.log main;
error_log /var/log/nginx/service-error-remote.log;

location / {
proxy_pass http://some_service;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
}

То есть, все запросы, которые придут через туннель на порт 8080, будут проверены на домен, и в случае, если домен равен subdomain.mydomain.com - будут переданы на порт 8123.

Так же нам потребуется настроить удалённый nginx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
upstream tunnel {
server 127.0.0.1:9000;
}

server {
listen 80;
server_name subdomain.mydomain.com;
access_log /var/log/nginx/service-access.log main;
error_log /var/log/nginx/service-error.log;

location / {
proxy_pass http://tunnel;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
}

Теперь у нас есть полная цепочка:

  1. Запрос приходит на 80 порт удалённого сервера с указанием домена subdomain.mydomain.com
  2. Nginx на удалённом сервере слушает порт, видит домен, пишет логи доступа и отправляет запрос в туннель на порту 9000
  3. Туннель “выныривает” на вашем локальном сервере на порту 8080 и попадает в локальный Nginx
  4. Локальный nginx опять же видит домен и на его основании пересылает запрос уже в конечное приложение на порту 8123

Звучит на первый взгляд сложно, зато позволяет через один туннель гонять сколько угодно запросов к различным сайтам сервера.

3.3.3 Ни единого разрыва!

Всё готово? Как бы не так! SSH туннель может упасть при обрыве коннекта, и вместо доступного извне сервера у вас окажется тыква.

Чтобы этого не случилось, можно воспользоваться пакетом autossh из штатного репозитория - он обеспечит автоматическое переподнятие туннеля.

Ставим пакет, и запускаем его примерно так: autossh -NR 8080:localhost:9000 -M 0 -o "ServerAliveInterval 10" -o "ServerAliveCountMax 3" user_login@mydomain.com -i /home/path_to_home_dir/.ssh/private_key

Теперь туннель не умрёт. Но… Подождите, что же будет при ребуте локального сервера?

3.3.4 Переживший ребут

Можно добавить команду в @reboot от cron, но мне кажется более правильным добавить новый системный сервис.

Добавляем настройку демона, например, в такой файл - /etc/systemd/system/autossh.service:

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=AutoSSH to My Server
After=network.target

[Service]
Environment="AUTOSSH_GATETIME=0"
Environment="AUTOSSH_LOGFILE=/var/log/autossh"
ExecStart=autossh -NR 8080:localhost:9000 -M 0 -o "ExitOnForwardFailure=yes" -o "ServerAliveInterval=180" -o "ServerAliveCountMax=3" -o "PubkeyAuthentication=yes" -o "PasswordAuthentication=no" -i /home/path_to_home_dir/.ssh/private_key user_login@mydomain.com -p 22
Restart=always

[Install]
WantedBy=multi-user.target

После этого выполняем systemctl daemon-reload и systemctl enable --now autossh, проверяем логи в /var/log/autossh, и радуемся, что нам больше не страшны ребуты.

3.3.5 Нет пределов совершенству

Если вы думаете, что на этом всё - совсем нет! По хорошему, стоит так же защитить ваш сайт при помощи SSL - например, через Lets Encrypt, а если он приватный (например, управление умным домом) - то закрыть снаружи дополнительной авторизацией вроде Authentik.

Про это есть много хороших статей, так что я просто добавлю, что всё это можно делать уже только на удалённом сервере, поскольку ваш домашний и так закрыт вашей собственной сетью.

В итоге у вас может получиться какой-то такой конфиг внешнего nginx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

upstream tunnel {
server 127.0.0.1:9000;
}

server {
server_name subdomain.mydomain.com;
access_log /var/log/nginx/tun-access.log main;
error_log /var/log/nginx/tun-error.log;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/subdomain.mydomain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/subdomain.mydomain.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot


location / {
proxy_pass http://tunnel;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
##############################
# authentik-specific config
##############################
auth_request /outpost.goauthentik.io/auth/nginx;
error_page 401 = @goauthentik_proxy_signin;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;

# translate headers from the outposts back to the actual upstream
auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
auth_request_set $authentik_email $upstream_http_x_authentik_email;
auth_request_set $authentik_name $upstream_http_x_authentik_name;
auth_request_set $authentik_uid $upstream_http_x_authentik_uid;

proxy_set_header X-authentik-username $authentik_username;
proxy_set_header X-authentik-groups $authentik_groups;
proxy_set_header X-authentik-email $authentik_email;
proxy_set_header X-authentik-name $authentik_name;
proxy_set_header X-authentik-uid $authentik_uid;

}
# all requests to /outpost.goauthentik.io must be accessible without authentication
location /outpost.goauthentik.io {
proxy_pass https://127.0.0.1:7443/outpost.goauthentik.io;
# ensure the host of this vserver matches your external URL you've configured
# in authentik
proxy_set_header Host $host;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
add_header Set-Cookie $auth_cookie;
auth_request_set $auth_cookie $upstream_http_set_cookie;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}

# Special location for when the /auth endpoint returns a 401,
# redirect to the /start URL which initiates SSO
location @goauthentik_proxy_signin {
internal;
add_header Set-Cookie $auth_cookie;
return 302 /outpost.goauthentik.io/start?rd=$request_uri;
# For domain level, use the below error_page to redirect to your authentik server with the full redirect path
# return 302 https://authentik.company/outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri;
}

Вот теперь вы действительно великолепны.

3.3.6 Плюсы и минусы решения с туннелем

Плюсы:

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

Минусы:

  • Вы сегодня много узнали
  • Первичная настройка занимает некоторое время
  • Придётся платить за облачный сервер и домен

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

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

Ссылочки

SSH туннели

Обсуждения

Апдейт по мотивам комментариев

  1. Можно ещё воспользоваться специализированными сервисами для конкретно вашего кейса - например, для Home Assistant есть Dataplicity и Nabu Casa
  2. Вместо SSH туннеля можно использовать VPN к внешнему серверу и опять же прокидывать порты через Nginx. На первый взгляд кажется, что равнозначное ssh решение, в том числе по трудоёмкости.
  3. А можно использовать VPN и не прокидывать его наружу через nginx. Тогда ваш сервис будет доступен снаружи откуда угодно - но только через VPN соединение. Что в некоторых случаях имеет смысл.
  4. Говорят, что ещё одна альтернатива SSH и VPN - это zerotier. Не слышал про этого зверя, но кажется про него есть на хабре хорошая статья.