Перейти к содержимому
Port forwarding diagram: a packet crossing a NAT boundary by four routes inward — a router NAT rule, an SSH reverse tunnel, an nftables/iptables rule, and a hosted tunnel CLI

Проброс портов: от роутера до готового туннеля

Maks
5 минут чтения

В процессе разработки или в 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 на сервис внутри:

bash
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 — без него правило поймает и внутренний трафик. Что оно вообще срабатывает, проверяется по счётчикам; ноль значит, что трафик до правила не доходит, и дело не в правиле:

bash
1/ip firewall nat print stats

Снаружи работает, изнутри — нет

Классика. Из своей же локалки пакет уходит на роутер, тот делает DNAT на сервер — тоже в локалке — и сервер отвечает напрямую вам, минуя роутер. Вы ждёте ответ от внешнего IP, получаете от локального и выбрасываете его. Добавляем hairpin (loopback) NAT, чтобы ответ вернулся через роутер:

bash
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:

bash
1ssh -R 3000:localhost:3000 user@server

Это весь приём, если у вас есть свой VPS. Один момент, который стоит знать:

Порт слушает только localhost

По умолчанию sshd биндит проброшенный порт на 127.0.0.1 и игнорирует переданный bind_address — защита от случайной публикации туннеля. Включаем на сервере:

bash
1# /etc/ssh/sshd_config на сервере, затем: systemctl restart sshd
2GatewayPorts clientspecified
  • no (по умолчанию) — всегда 127.0.0.1;
  • yes — всегда 0.0.0.0, все интерфейсы;
  • clientspecified — адрес задаёт клиент (вменяемый вариант).

С clientspecified можно привязаться ровно к одному интерфейсу — например, отдать локальный сервис контейнерам на сервере, не выставляя его в интернет:

bash
1ssh -N -R 172.22.0.1:8086:127.0.0.1:8080 user@server

Туннель умирает вместе с сессией; заверните в autossh или systemd-юнит с Restart=always, если он нужен постоянно.

Ещё две опции из того же семейства: наружу localhost отдаёт только -R, -L тянет удалённый порт к вам, а -D поднимает локальный SOCKS-прокси:

bash
1ssh -L 5432:127.0.0.1:5432 user@server # притянуть удалённый порт к себе
2ssh -D 1080 user@server # локальный SOCKS-прокси

Нет своего сервера? Используйте публичный релей

Свой сервер не обязателен — тот же ssh -R работает и с публичными релеями:

bash
1# Pinggy
2ssh -p 443 -R0:localhost:3000 free.pinggy.io
3
4# localhost.run
5ssh -R 80:localhost:3000 [email protected]
6
7# Serveo
8ssh -R 80:localhost:3000 serveo.net

Всем трём нужен только ssh: ни клиента ставить, ни аккаунт заводить. Pinggy сразу выдаёт HTTPS-ссылку, для чистого TCP хост пишется с префиксом [email protected], а бесплатный туннель живёт час. У localhost.run префикс nokey@ пропускает проверку SSH-ключа, и домен на бесплатном тарифе меняется каждые несколько часов. У Serveo кроме ssh есть WireGuard и расширение для браузера. Удобно, когда надо быстро показать коллеге вебхук или превью.

3. Постоянное правило: nftables / iptables

Если Linux-машина сама работает шлюзом, проброс делаем средствами ядра. Сначала разрешаем ей пересылать транзитный трафик:

bash
1sysctl -w net.ipv4.ip_forward=1
2echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-forward.conf

На современных дистрибутивах (Debian 10+, Ubuntu 20.10+, RHEL 8+) nftables — стандарт, а iptables — обёртка над ним. Для нового пишите так:

bash
1table ip nat {
2 chain prerouting {
3 type nat hook prerouting priority dstnat;
4 tcp dport 8080 dnat to 192.168.99.10:80
5 }
6 chain postrouting {
7 type nat hook postrouting priority srcnat;
8 ip saddr 192.168.99.0/24 oifname "eth0" masquerade
9 }
10}

В старых статьях и на форумах чаще попадается вариант для iptables. Правил там три, и все три нужны:

bash
1# 1. подменить адрес назначения на входе
2iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 \
3 -j DNAT --to-destination 192.168.99.10:80
4
5# 2. разрешить пересылку — DNAT сам по себе ничего не разрешает
6iptables -A FORWARD -p tcp -d 192.168.99.10 --dport 80 -j ACCEPT
7
8# 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:

bash
1npx localtunnel --port 3000

Туннель Cloudflare — для быстрого запуска аккаунт не нужен:

bash
1cloudflared tunnel --url http://localhost:3000

ngrok — классика; бесплатный токен, задаётся один раз:

bash
1ngrok config add-authtoken $YOUR_AUTHTOKEN
2ngrok http 3000

или то же самое через питоновский uv, ставить ничего не нужно:

bash
1# ставить ничего не нужно, если уже есть uv
2uvx pyngrok config add-authtoken $YOUR_AUTHTOKEN
3uvx 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-релей. Последние два сами устанавливают соединение с вашей стороны, и это единственный рабочий вариант, когда открывать снаружи просто нечего.

Поделиться этой публикацией