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. У меня там тоже крутится один туннель для экспериментов.