mysurik.ru

SSH туннели: как я пробрасывал порты через NAT и не сходил с ума

SSH туннелирование через NAT

Зачем мне понадобился SSH туннель

У меня есть сервер дома за NAT провайдера. К нему нет прямого доступа из интернета — только через внутреннюю сеть. А мне иногда нужно зайти на домашний сервер с работы.

Пробросить порты на роутере нельзя — провайдер выдаёт серый IP. Решение: SSH туннель через промежуточный VPS.

Честно говоря, я не сразу до этого дошёл. Сначала пытался настроить DynDNS, открыть порты — провайдер их резал на входе. Потом пробовал ZeroTier — штука крутая, но на работе корпоративный фаервол режет UDP. А SSH работает всегда, потому что 22 порт обычно открыт.

Как это работает

У меня есть дешёвый VPS за границей за 3$ в месяц. Домашний сервер сам устанавливает SSH-соединение к VPS и пробрасывает порты. Я подключаюсь к VPS, а он перенаправляет меня на домашний сервер.

Команда на домашнем сервере:

ssh -R 2222:localhost:22 user@vps

Флаг -R говорит SSH, мол, слушай на удалённой стороне порт 2222 и всё, что туда придёт, гони на localhost:22 моего компа. Я иногда путаю -R и -L, и однажды пробросил наоборот — открыл доступ к VPS с домашнего сервера. Толку ноль.

Теперь, когда я подключаюсь к VPS на порт 2222, меня перенаправляет на SSH домашнего сервера. Я захожу с ноутбука:

ssh -p 2222 user@vps

И оказываюсь дома.

Тут хитрость: я на VPS создал отдельного пользователя tunnel с шеллом /usr/sbin/nologin. Ему только разрешён Reverse Port Forwarding через опцию PermitOpen в authorized_keys. Если злоумышленник как-то получит ключ — он даже shell запустить не сможет.

Что ещё я пробрасываю

Кроме SSH, я пробросил веб-морду Proxmox (порт 8006) и админку Home Assistant (8123). Теперь могу управлять домашними устройствами из любого места, даже если я в командировке.

Для веб-интерфейсов это работает так:

ssh -L 8080:localhost:8006 user@vps

Открываю в браузере http://localhost:8080 — и я в админке Proxmox.

Тут важный момент: -R и -L — это разные звери. -L ты открываешь порт на своей машине и ходишь через VPS внутрь. А -R наоборот — выставляешь свой порт наружу. Я для Proxmox использую -R, потому что хочу заходить с любого компа, а не только с того, где настроен туннель.

Автоматизация туннеля

SSH соединение может рваться. Чтобы оно переподнималось автоматически, я настроил systemd сервис на домашнем сервере, который держит туннель постоянно.

Создал /etc/systemd/system/ssh-tunnel.service:

[Unit]
Description=SSH Tunnel to VPS
After=network.target

[Service]
Type=simple
User=tunnel
ExecStart=/usr/bin/ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -N -R 2222:localhost:22 -R 8006:localhost:8006 -R 8123:localhost:8123 tunnel@vps
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

ServerAliveInterval=30 — каждые 30 секунд SSH шлёт keepalive. Если три пакета пропало — перезапуск. RestartSec=10 — ждать 10 секунд перед реконнектом. Работает месяцами без моего вмешательства.

Я раньше не знал про -N и -T. Флаг -N означает «не запускать shell», -T — «не выделять псевдотерминал». Без них SSH при подключении выделял лишние ресурсы, и через неделю соединение отваливалось с ошибкой.

Кстати, autossh — штука популярная. Она сама мониторит соединение и перезапускает. Но я предпочёл systemd — он родной, логи пишет в journald, и я вижу, когда и почему туннель падал:

journalctl -u ssh-tunnel.service --since yesterday

Один раз упал провайдер — дёрнули оптику на районе. Через 15 минут интернет появился, и systemd сам поднял туннель. Я узнал об этом только когда зашёл в логи через неделю.

SSH-ключи и мультиплексирование

Я сгенерировал отдельный ключ для туннеля — без пароля. Да, без пароля, потому что сервер сам переподключается, и никто не введёт пароль после ребута. Хранится ключ в /etc/ssh/tunnel_key с правами 600.

На VPS в authorized_keys этого пользователя я добавил ограничения:

command="/bin/false",no-agent-forwarding,no-X11-forwarding,no-pty,permitlisten="localhost:2222,localhost:8006,localhost:8123" ssh-ed25519 AAA... tunnel-key

Разрешил только слушать определённые порты. Никаких shell, никакого перенаправления агента. Даже если кто-то стырит ключ — сделать с ним ничего не сможет.

Ещё я включил мультиплексирование SSH — ControlMaster auto. Это позволяет не плодить десятки TCP-соединений для каждого проброшенного порта. Одно соединение — все туннели внутри него.

В ~/.ssh/config прописал:

Host vps-tunnel
HostName vps
User tunnel
IdentityFile /etc/ssh/tunnel_key
ControlMaster auto
ControlPath ~/.ssh/controlmasters/%r@%h:%p
ControlPersist 10m

Теперь systemd сервис использует Host vps-tunnel. Если я захочу открыть ещё один туннель вручную — он подцепится к существующему соединению, не создавая нового. Экономия ресурсов и меньше нагрузки на VPS.

Проблема с NAT и TCP keepalive

C домашним NAT всё сложно. Провайдер режет неактивные TCP-соединения через 5 минут. Я это выяснил, когда туннель стабильно отваливался каждые 5 минут ровно.

Решение — ClientAliveInterval и ServerAliveInterval на клиенте и сервере. В /etc/ssh/sshd_config на VPS я выставил:

ClientAliveInterval 15
ClientAliveCountMax 3
TCPKeepAlive yes

Это заставляет SSH слать пакеты каждые 15 секунд. Если три подряд потеряны — сервер закрывает соединение. На домашней стороне ServerAliveInterval настроен так же. В итоге соединение стабильно даже через самые агрессивные NAT.

Ещё я столкнулся с тем, что после перезагрузки домашнего сервера туннель не поднимался, пока я не зайду по SSH локально. Оказалось, network.target не гарантирует, что сеть реально готова. Пришлось добавить network-online.target и wating-for-interface:

After=network-online.target
Wants=network-online.target

И вручную добавить интерфейс в /etc/systemd/system/ssh-tunnel.service.d/override.conf:

Requires=sys-subsystem-net-devices-enp2s0.device
After=sys-subsystem-net-devices-enp2s0.device

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

Туннель для коллег

Однажды понадобилось дать доступ к нашему GitLab (который дома) паре фрилансеров. Не заводить же их в WireGuard. Я просто создал ещё один туннель на VPS с портом 8443, который смотрит на GitLab. Фрилансеры подключаются к VPS по HTTPS — и видят наш GitLab. Никаких открытых портов на домашнем роутере.

Единственное — VPS должен быть достаточно мощным, чтобы проксировать трафик. Для 2-3 человек хватает самого дешёвого. Если бы было 20 человек — пришлось бы брать VPS посерьёзнее или ставить nginx на VPS как reverse proxy.

Я так и сделал: повесил nginx на VPS, который слушает port 443 и проксирует запросы через локальный туннель. Получился полноценный HTTPS-доступ к домашним сервисам через Let’s Encrypt.

Что пошло не так

Когда я только начинал, я не знал про GatewayPorts. По умолчанию SSH на VPS слушает -R порты только на localhost. То есть я подключался к VPS, а порт 2222 был виден только внутри VPS. Бесполезно, если хочется заходить с другого компа.

В /etc/ssh/sshd_config надо включить:

GatewayPorts yes

Тогда -R порты открываются на 0.0.0.0, и я могу достучаться до туннеля с любого устройства в интернете — конечно, если фаервол на VPS разрешает.

Ещё я наступал на грабли с конфликтом портов. Если дома уже крутится что-то на порту 8006, а я пытаюсь пробросить тот же порт — SSH падает с ошибкой. Лечится выбором другого порта на VPS: -R 8007:localhost:8006.

Хитрость: я запретил парольный вход на VPS вообще. Только ключи. Потому что если у кого-то будет доступ к VPS — он получит доступ к туннелю. На VPS только fail2ban крутится и фаервол с белым списком IP — стран, с которых я захожу.

Личный вердикт

SSH туннели — самый простой и безопасный способ получить доступ к домашнему серверу через NAT. Не нужен VPN, не нужно открывать порты на роутере. Только SSH и ключи.

Я теперь запускаю это на всех клиентах, где нужен удалённый доступ. Настройка заняла вечер, зато работает годами без проблем. Да, поначалу были грабли с GatewayPorts и keepalive — но когда разобрался, всё встало на свои места.

Если у тебя домашний сервер за NAT — не парься с DynDNS и UPnP. Поставь SSH туннель и живи спокойно. А если VPS жалко 3$ — попробуй поднять туннель через бесплатный Oracle Cloud Always Free. У меня там тоже крутится один туннель для экспериментов.

Расширю статью о SSH туннелях дополнительными деталями из опыта. Зачем мне понадобился SSH туннель. У меня есть сервер дома за NAT провайдера. К нему нет прямого доступа из интернета — только через внутреннюю сеть. А мне иногда нужно зайти на домашний сервер с работы. Пробросить порты на роутере нельзя — провайдер выдаёт серый IP. Решение: SSH туннель через промежуточный VPS. Честно говоря, я не сразу до этого дошёл. Сначала пытался настроить DynDNS, открыть порты — провайдер их резал на входе. Потом пробовал ZeroTier — штука крутая, но на работе корпоративный фаервол режет UDP. А SSH работает всегда, потому что 22 порт обычно открыт. Серая зона за NAT — это почти гарантия, что прямого доступа не будет: провайдер даёт приватный адрес, и проброс портов на роутере не поможет. Динамический DNS и UPnP тоже упираются в политику провайдера. ZeroTier и WireGuard требуют UDP-трафика, который режет корпоративный фаервол на работе. А вот SSH — порт 22, который открыт почти везде, и надёжный TCP. Вот так я пришёл к SSH туннелю как к единственному рабочему варианту. Как это работает. У меня есть дешёвый VPS за границей за 3$ в месяц. Домашний сервер сам устанавливает SSH-соединение к VPS и пробрасывает порты. Я подключаюсь к VPS, а он перенаправляет меня на домашний сервер. Команда на домашнем сервере: ssh -R 2222:localhost:22 user@vps. Флаг -R говорит SSH, мол, слушай на удалённой стороне порт 2222 и всё, что туда придёт, гони на localhost:22 моего компа. Я иногда путаю -R и -L, и однажды пробросил наоборот — открыл доступ к VPS с домашнего сервера. Толку ноль. Теперь, когда я подключаюсь к VPS на порт 2222, меня перенаправляет на SSH домашнего сервера. Я захожу с ноутбука: ssh -p 2222 user@vps. И оказываюсь дома. Суть SSH туннеля проста: домашний сервер сам инициирует исходящее соединение к VPS (исходящие разрешены), и VPS держит для него открытый порт. Флаг -R открывает порт на удалённой стороне и перенаправляет трафик обратно на домашнюю машину. Я подключаюсь к VPS на порт 2222, и меня перенаправляет на SSH-сервер дома. Один раз я перепутал -R и -L: пробросил доступ с VPS к дому в обратную сторону, и получил пустоту. Разница: -R слушает удалённую сторону, -L слушает локальную. Тут хитрость: я на VPS создал отдельного пользователя tunnel с шеллом /usr/sbin/nologin. Ему только разрешён Reverse Port Forwarding через опцию PermitOpen в authorized_keys. Если злоумышленник как-то получит ключ — он даже shell запустить не сможет. Безопасность туннеля строится на ограничении прав. Отдельный пользователь tunnel с /usr/sbin/nologin в качестве шелла — он не может запустить команды, только перенаправлять порты. PermitOpen в authorized_keys ограничивает, какие порты можно открывать. Даже если ключ украдут — злоумышленник не получит shell, а только узкий набор разрешённых перенаправлений. Это хороший пример минимальных привилегий. Что ещё я пробрасываю. Кроме SSH, я пробросил веб-морду Proxmox (порт 8006) и админку Home Assistant (8123). Теперь могу управлять домашними устройствами из любого места, даже если я в командировке. Для веб-интерфейсов это работает так: ssh -L 8080:localhost:8006 user@vps. Открываю в браузере http://localhost:8080 — и я в админке Proxmox. Тут важный момент: -R и -L — это разные звери. -L ты открываешь порт на своей машине и ходишь через VPS внутрь. А -R наоборот — выставляешь свой порт наружу. Я для Proxmox использую -R, потому что хочу заходить с любого компа, а не только с того, где настроен туннель. Один туннель может переносить несколько сервисов. Proxmox на 8006, Home Assistant на 8123, SSH на 2222 — всё в одном соединении. -L слушает порт на вашей машине и туннелирует запросы внутрь, -R — наоборот, выставляет ваш порт наружу. Для домашнего доступа чаще удобнее -R: он доступен с любого устройства. Админка сервера из любой точки мира — это серьёзное удобство для администратора. Автоматизация туннеля. SSH соединение может рваться. Чтобы оно переподнималось автоматически, я настроил systemd сервис на домашнем сервере, который держит туннель постоянно. Создал /etc/systemd/system/ssh-tunnel.service: [Unit] Description=SSH Tunnel to VPS After=network.target [Service] Type=simple User=tunnel ExecStart=/usr/bin/ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -N -R 2222:localhost:22 -R 8006:localhost:8006 -R 8123:localhost:8123 tunnel@vps Restart=always RestartSec=10 [Install] WantedBy=multi-user.target. ServerAliveInterval=30 — каждые 30 секунд SSH шлёт keepalive. Если три пакета пропало — перезапуск. RestartSec=10 — ждать 10 секунд перед реконнектом. Работает месяцами без моего вмешательства. Systemd — это идеальный способ держать туннель живым. Сервис автозапускается при загрузке, перезапускается при падении, пишет логи в journald. ServerAliveInterval шлёт keepalive каждые 30 секунд, чтобы NAT не забыл о соединении. Если три пакета потеряны — сервис перезапускается через 10 секунд. Однажды провайдер уронил оптику, через 15 минут всё поднялось, и туннель восстановился сам. Я узнал об этом через неделю из логов. Я раньше не знал про -N и -T. Флаг -N означает «не запускать shell», -T — «не выделять псевдотерминал». Без них SSH при подключении выделял лишние ресурсы, и через неделю соединение отваливалось с ошибкой. Кстати, autossh — штука популярная. Она сама мониторит соединение и перезапускает. Но я предпочёл systemd — он родной, логи пишет в journald, и я вижу, когда и почему туннель падал: journalctl -u ssh-tunnel.service —since yesterday. Флаги -N и -T — это мелкие, но важные детали. -N запрещает запуск shell: соединение существует только для проброса портов. -T отключает псевдотерминал, экономя ресурсы. Без них соединение держит лишнее и может отвалиться через неделю. Я сравнил systemd и autossh: systemd пишет в journald и перезапускает сервис штатно, autossh — специализированный инструмент, но настраивается лишним слоем. Для меня journald и системные юниты оказались удобнее: всё родное, всё видно. Один раз упал провайдер — дёрнули оптику на районе. Через 15 минут интернет появился, и systemd сам поднял туннель. Я узнал об этом только когда зашёл в логи через неделю. SSH-ключи и мультиплексирование. Я сгенерировал отдельный ключ для туннеля — без пароля. Да, без пароля, потому что сервер сам переподключается, и никто не введёт пароль после ребута. Хранится ключ в /etc/ssh/tunnel_key с правами 600. На VPS в authorized_keys этого пользователя я добавил ограничения: command=»/bin/false»,no-agent-forwarding,no-X11-forwarding,no-pty,permitlisten=»localhost:2222,localhost:8006,localhost:8123″ ssh-ed25519 AAA… tunnel-key. Разрешил только слушать определённые порты. Никаких shell, никакого перенаправления агента. Даже если кто-то стырит ключ — сделать с ним ничего не сможет. Ключ без пароля — это требование автоматизации: сервис должен подключаться после ребута без участия человека. Права 600 на ключе — только root может читать. В authorized_keys ограничения: command=/bin/false (не выполнять команды), no-pty (без терминала), permitlisten (только нужные порты). Ключ бесполезен для злоумышленника: максимум — открыть разрешённый порт. Ещё я включил мультиплексирование SSH — ControlMaster auto. Это позволяет не плодить десятки TCP-соединений для каждого проброшенного порта. Одно соединение — все туннели внутри него. В ~/.ssh/config прописал: Host vps-tunnel HostName vps User tunnel IdentityFile /etc/ssh/tunnel_key ControlMaster auto ControlPath ~/.ssh/controlmasters/%r@%h:%p ControlPersist 10m. Теперь systemd сервис использует Host vps-tunnel. Если я захочу открыть ещё один туннель вручную — он подцепится к существующему соединению, не создавая нового. Экономия ресурсов и меньше нагрузки на VPS. Мультиплексирование — это одно TCP-соединение для всех каналов SSH. ControlMaster держит мастер-соединение, остальные подключаются к нему. ControlPersist 10m — мастер живёт ещё 10 минут после последнего использования. Вместо десятка соединений — одно. Меньше нагрузка на VPS, быстрее установка новых туннелей. Полезная оптимизация, если у вас несколько туннелей. Проблема с NAT и TCP keepalive. C домашним NAT всё сложно. Провайдер режет неактивные TCP-соединения через 5 минут. Я это выяснил, когда туннель стабильно отваливался каждые 5 минут ровно. Решение — ClientAliveInterval и ServerAliveInterval на клиенте и сервере. В /etc/ssh/sshd_config на VPS я выставил: ClientAliveInterval 15, ClientAliveCountMax 3, TCPKeepAlive yes. Это заставляет SSH слать пакеты каждые 15 секунд. Если три подряд потеряны — сервер закрывает соединение. На домашней стороне ServerAliveInterval настроен так же. В итоге соединение стабильно даже через самые агрессивные NAT. NAT режет неактивные соединения, чтобы не держать слоты в таблице трансляции. Если туннель молчит пять минут — провайдер его убивает. Решение — keepalive: клиент и сервер шлют пакеты каждые 15 секунд. Три потерянных пакета подряд — сервер закрывает соединение, а systemd его перезапускает. Согласованные настройки на обеих сторонах — залог стабильности. Это была одна из самых бесячих проблем, и решение оказалось банальным. Ещё я столкнулся с тем, что после перезагрузки домашнего сервера туннель не поднимался, пока я не зайду по SSH локально. Оказалось, network.target не гарантирует, что сеть реально готова. Пришлось добавить network-online.target и wating-for-interface: After=network-online.target Wants=network-online.target. И вручную добавить интерфейс в /etc/systemd/system/ssh-tunnel.service.d/override.conf: Requires=sys-subsystem-net-devices-enp2s0.device After=sys-subsystem-net-devices-enp2s0.device. После этого туннель стал подниматься даже после глубокого ребута. Я проверял — выключал сервер из розетки, включал, через минуту туннель уже висел. Тонкость с сетевыми юнитами — это частая причина «не поднимается после перезагрузки». network.target стартует рано, но не ждёт готовности сети. network-online.target гарантирует реальную готовность. Привязка к устройству интерфейса (enp2s0) делает сервис зависимым от появления сети. После этих правок туннель поднимается даже после полного отключения питания. Проверил лично. Туннель для коллег. Однажды понадобилось дать доступ к нашему GitLab (который дома) паре фрилансеров. Не заводить же их в WireGuard. Я просто создал ещё один туннель на VPS с портом 8443, который смотрит на GitLab. Фрилансеры подключаются к VPS по HTTPS — и видят наш GitLab. Никаких открытых портов на домашнем роутере. Единственное — VPS должен быть достаточно мощным, чтобы проксировать трафик. Для 2-3 человек хватает самого дешёвого. Если бы было 20 человек — пришлось бы брать VPS посерьёзнее или ставить nginx на VPS как reverse proxy. Я так и сделал: повесил nginx на VPS, который слушает port 443 и проксирует запросы через локальный туннель. Получился полноценный HTTPS-доступ к домашним сервисам через Let’s Encrypt. Проброс сервисов для коллег — это отдельная задача. Вместо VPN и WireGuard — ещё один туннель на VPS. Фрилансеры видят GitLab по HTTPS через VPS, а домашний роутер остаётся закрытым. Для пары человек хватает дешёвого VPS. Если народу больше — нужен nginx-реверс-прокси на VPS и сертификаты Let’s Encrypt. Полноценный HTTPS-доступ к домашним сервисам через туннель — это красивое решение. Что пошло не так. Когда я только начинал, я не знал про GatewayPorts. По умолчанию SSH на VPS слушает -R порты только на localhost. То есть я подключался к VPS, а порт 2222 был виден только внутри VPS. Бесполезно, если хочется заходить с другого компа. В /etc/ssh/sshd_config надо включить: GatewayPorts yes. Тогда -R порты открываются на 0.0.0.0, и я могу достучаться до туннеля с любого устройства в интернете — конечно, если фаервол на VPS разрешает. GatewayPorts — это тот ключевой переключатель, о котором молчат инструкции. По умолчанию -R порты слушают только localhost: доступен только с самого VPS. С GatewayPorts yes порты открываются на всех интерфейсах — и туннель доступен из интернета. Это открывает доступ к туннелю, поэтому фаервол VPS должен ограничивать, кто может подключаться. Обязательно сочетайте GatewayPorts с фаерволом и белым списком. Ещё я наступал на грабли с конфликтом портов. Если дома уже крутится что-то на порту 8006, а я пытаюсь пробросить тот же порт — SSH падает с ошибкой. Лечится выбором другого порта на VPS: -R 8007:localhost:8006. Хитрость: я запретил парольный вход на VPS вообще. Только ключи. Потому что если у кого-то будет доступ к VPS — он получит доступ к туннелю. На VPS только fail2ban крутится и фаервол с белым списком IP — стран, с которых я захожу. Конфликт портов решается просто: другой порт на VPS. Если дома занят 8006, на VPS можно открыть 8007 и направить его на домашний 8006. А вот безопасность VPS — это критично: VPS — это точка входа во всю домашнюю сеть. Парольный вход отключён, только ключи. Fail2ban банит подборщиков, фаервол пускает только белые IP. VPS должен быть крепким орешком. Личный вердикт. SSH туннели — самый простой и безопасный способ получить доступ к домашнему серверу через NAT. Не нужен VPN, не нужно открывать порты на роутере. Только SSH и ключи. Я теперь запускаю это на всех клиентах, где нужен удалённый доступ. Настройка заняла вечер, зато работает годами без проблем. Да, поначалу были грабли с GatewayPorts и keepalive — но когда разобрался, всё встало на свои места. Если у тебя домашний сервер за NAT — не парься с DynDNS и UPnP. Поставь SSH туннель и живи спокойно. А если VPS жалко 3$ — попробуй поднять туннель через бесплатный Oracle Cloud Always Free. У меня там тоже крутится один туннель для экспериментов. SSH туннель — это доступ к дому из любой точки мира без единого открытого порта. Вечер настройки — и годами стабильной работы. DynDNS, UPnP, проброс портов — всё это в прошлом. Один дешёвый VPS, пара systemd-сервисов и SSH-ключи — и вы всегда на связи со своим сервером. Начните с одного туннеля — и поймёте, насколько это удобно.

Ваш комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *