В процессе разработки или в ops-задачах, мне часто нужно, чтобы локальный ресурс — dev-сервер, тулза, какой либо еще софт который надо открыть коллеге или клиенту через интернет. Иногда на десять минут, иногда на постоянно.
Почти всегда я беру один из этих вариантов:
- NAT-правило на роутере — ниже пример для MikroTik;
- обратный SSH-туннель — через свой сервер или бесплатный релей вроде pinggy / localhost.run / serveo;
- правило nftables / iptables на linux сервере;
- готовый туннель — localtunnel, cloudflared, ngrok.
Что выбрать — зависит от того, что у вас есть: подконтрольный роутер, машина с белым IP, root на внешний сервер или только ноутбук за CGNAT. Разберём каждый вариант и ту деталь, на которой он обычно спотыкается.
Как это работает
Под капотом у всех четырёх — один и тот же приём. Адреса в пакете переписываются так, чтобы запрос на публичный адрес и порт попал на ваш приватный сервис: сначала меняется адрес назначения (DNAT), затем на выходе — адрес источника (SNAT / MASQUERADE), и ответ уходит обратно тем же путём. conntrack держит эту пару в памяти, так что правило нужно задать всего раз.
1. На роутере
Если роутер ваш и сервис должен оставаться доступным — делайте там. На домашнем роутере это форма Port Forwarding / Virtual Server. На MikroTik — правило в NAT-таблице; пробросим внешний 8080 на сервис внутри:
1/ip firewall nat add chain=dstnat protocol=tcp dst-port=8080 \2 in-interface=ether1 action=dst-nat to-addresses=192.168.88.10 to-ports=80
Важен in-interface — без него правило поймает и внутренний трафик. Что оно вообще срабатывает, проверяется по счётчикам; ноль значит, что трафик до правила не доходит, и дело не в правиле:
1/ip firewall nat print stats
Снаружи работает, изнутри — нет
Классика. Из своей же локалки пакет уходит на роутер, тот делает DNAT на сервер — тоже в локалке — и сервер отвечает напрямую вам, минуя роутер. Вы ждёте ответ от внешнего IP, получаете от локального и выбрасываете его. Добавляем hairpin (loopback) NAT, чтобы ответ вернулся через роутер:
1/ip firewall nat add chain=srcnat protocol=tcp dst-address=192.168.88.10 \2 dst-port=80 src-address=192.168.88.0/24 action=masquerade
Тот же сломанный обратный путь, что и в правиле выше, — «туда» всё это время было в порядке.
2. Обратный SSH-туннель
Нет доступа к роутеру или нужно открыть наружу сервис на десять минут? Можно воспользоваться механизмом проброса порта SSH. Опция -R открывает порт на удалённой машине с белым IP и заворачивает его на ваш localhost:
1ssh -R 3000:localhost:3000 user@server
Это весь приём, если у вас есть свой VPS. Один момент, который стоит знать:
Порт слушает только localhost
По умолчанию sshd биндит проброшенный порт на 127.0.0.1 и игнорирует переданный bind_address — защита от случайной публикации туннеля. Включаем на сервере:
1# /etc/ssh/sshd_config на сервере, затем: systemctl restart sshd2GatewayPorts clientspecified
no(по умолчанию) — всегда 127.0.0.1;yes— всегда 0.0.0.0, все интерфейсы;clientspecified— адрес задаёт клиент (вменяемый вариант).
С clientspecified можно привязаться ровно к одному интерфейсу — например, отдать локальный сервис контейнерам на сервере, не выставляя его в интернет:
1ssh -N -R 172.22.0.1:8086:127.0.0.1:8080 user@server
Туннель умирает вместе с сессией; заверните в autossh или systemd-юнит с Restart=always, если он нужен постоянно.
Ещё две опции из того же семейства: наружу localhost отдаёт только -R, -L тянет удалённый порт к вам, а -D поднимает локальный SOCKS-прокси:
1ssh -L 5432:127.0.0.1:5432 user@server # притянуть удалённый порт к себе2ssh -D 1080 user@server # локальный SOCKS-прокси
Нет своего сервера? Используйте публичный релей
Свой сервер не обязателен — тот же ssh -R работает и с публичными релеями:
1# Pinggy2ssh -p 443 -R0:localhost:3000 free.pinggy.io34# localhost.run5ssh -R 80:localhost:3000 [email protected]67# Serveo8ssh -R 80:localhost:3000 serveo.net
Всем трём нужен только ssh: ни клиента ставить, ни аккаунт заводить. Pinggy сразу выдаёт HTTPS-ссылку, для чистого TCP хост пишется с префиксом [email protected], а бесплатный туннель живёт час. У localhost.run префикс nokey@ пропускает проверку SSH-ключа, и домен на бесплатном тарифе меняется каждые несколько часов. У Serveo кроме ssh есть WireGuard и расширение для браузера. Удобно, когда надо быстро показать коллеге вебхук или превью.
3. Постоянное правило: nftables / iptables
Если Linux-машина сама работает шлюзом, проброс делаем средствами ядра. Сначала разрешаем ей пересылать транзитный трафик:
1sysctl -w net.ipv4.ip_forward=12echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-forward.conf
На современных дистрибутивах (Debian 10+, Ubuntu 20.10+, RHEL 8+) nftables — стандарт, а iptables — обёртка над ним. Для нового пишите так:
1table ip nat {2 chain prerouting {3 type nat hook prerouting priority dstnat;4 tcp dport 8080 dnat to 192.168.99.10:805 }6 chain postrouting {7 type nat hook postrouting priority srcnat;8 ip saddr 192.168.99.0/24 oifname "eth0" masquerade9 }10}
В старых статьях и на форумах чаще попадается вариант для iptables. Правил там три, и все три нужны:
1# 1. подменить адрес назначения на входе2iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 \3 -j DNAT --to-destination 192.168.99.10:8045# 2. разрешить пересылку — DNAT сам по себе ничего не разрешает6iptables -A FORWARD -p tcp -d 192.168.99.10 --dport 80 -j ACCEPT78# 3. подменить адрес источника, чтобы ответ вернулся через нас9iptables -t nat -A POSTROUTING -d 192.168.99.10 -p tcp --dport 80 -j MASQUERADE
- Без #1 пакет не найдёт сервис.
- Без #2 его отбросит фильтр — DNAT меняет адрес, но не даёт разрешения.
- Без #3 сервис ответит клиенту напрямую, и ответ потеряется. Снова сломанный обратный путь.
Опустить #3 можно, только если сервер и так маршрутизирует ответы через эту машину. Правила живут до перезагрузки — iptables-persistent или nft list ruleset > /etc/nftables.conf.
4. Готовые туннели
За CGNAT, без белого IP и без своего сервера? Готовый туннель сам открывает соединение наружу с вашей машины и отдаёт публичный URL. Сеть настраивать не нужно вообще.
localtunnel — на расстоянии одного npx:
1npx localtunnel --port 3000
Туннель Cloudflare — для быстрого запуска аккаунт не нужен:
1cloudflared tunnel --url http://localhost:3000
ngrok — классика; бесплатный токен, задаётся один раз:
1ngrok config add-authtoken $YOUR_AUTHTOKEN2ngrok http 3000
или то же самое через питоновский uv, ставить ничего не нужно:
1# ставить ничего не нужно, если уже есть uv2uvx pyngrok config add-authtoken $YOUR_AUTHTOKEN3uvx pyngrok http 3000
Самый быстрый вариант — и самый открытый: трафик идёт через чужие серверы, а URL публичен с той секунды, как появился в терминале. Для демо и вебхуков отлично, но реальные данные туда лучше не пускать.
Когда не соединяется
Не переписывайте правило — идите по ходу пакета, сверху вниз:
ss -tulpn | grep 8080— сервис вообще слушает, и на 0.0.0.0? Бинд на 127.0.0.1 снаружи недостижим, сколько правил ни пиши.nc -vz host 8080— порт хотя бы отвечает локально?tcpdump -ni eth0 port 8080— пакеты доходят до шлюза? Тишина значит, что проблема раньше: провайдер, security group, роутер выше.iptables -t nat -nvL --line-numbers— смотрим счётчики пакетов; ноль значит, что правило не совпало.conntrack -L | grep host— пустая таблица под нагрузкой значит, что трафик не дошёл, что бы ни показывал контрольный уровень.
Безопасность и как выбрать
- URL туннеля или открытый порт доступен любому, кто его найдёт. Привязывайтесь к конкретному интерфейсу и ставьте авторизацию перед тем, как выставлять dev-сервис наружу.
- Ограничивайте источник, где можно —
-s 203.0.113.7в правиле; у большинства туннель-CLI есть флаг basic-auth. - Помните про Docker:
-p 8080:80пишет своё NAT-правило раньше UFW, поэтому порт публичен, даже когда фаервол говорит «закрыто». Пишите-p 127.0.0.1:8080:80. - Выключите UPnP — он позволяет любому приложению в сети молча открыть себе порт наружу.
Выбирайте по тому, что у вас есть: свой роутер → правило на нём; свой VPS → ssh -R; Linux-шлюз → nftables; только ноутбук за CGNAT → готовый туннель или ssh-релей. Последние два сами устанавливают соединение с вашей стороны, и это единственный рабочий вариант, когда открывать снаружи просто нечего.



