Проброс портов на VPS: iptables и nftables по шагам
Как пробросить порт с VPS на домашний сервер с серым IP или на вторую машину: DNAT, forward и обратный путь в iptables и nftables, плюс ловушки Docker и ufw.
Проброс портов на VPS: короткий ответ
Проброс портов на VPS работает так: пакет приходит на публичный порт сервера, ядро меняет в нём адрес назначения и отправляет его дальше, на другую машину. В iptables и nftables за это отвечает правило DNAT (destination NAT, подмена адреса назначения). Одного правила DNAT мало. Проброс работает, только когда выполнены сразу четыре условия. Почти любая жалоба «правило есть, но ничего не происходит» сводится к одному невыполненному пункту.
- Ядро пересылает чужие пакеты:
net.ipv4.ip_forward = 1, и это значение записано в/etc/sysctl.d/, а не выставлено только до перезагрузки. - В цепочке prerouting есть правило DNAT, которое меняет адрес назначения.
- Цепочка forward (в iptables это
FORWARD) пропускает уже изменённый пакет. - У ответа есть обратный путь: бэкенд отправляет ответ через VPS, а не мимо него.
Ниже каждый шаг показан в двух вариантах: командами iptables и файлом для nft. Выберите один синтаксис и держитесь его на всём сервере.
Зачем пробрасывать порт с VPS
Самая частая ситуация: дома стоит сервер или NAS, а провайдер выдаёт серый IP. Серый адрес означает, что у вашего роутера нет своего публичного адреса. Провайдер прячет сотни абонентов за одним адресом через CGNAT (carrier-grade NAT, трансляция адресов на стороне оператора). Снаружи до такой машины не достучаться. Белый IP, то есть публичный адрес, провайдер обычно продаёт как отдельную платную услугу, а некоторые не продают вовсе.
Дешёвый VPS с публичным адресом решает задачу иначе. Домашняя машина сама поднимает туннель до VPS, а VPS пробрасывает свой порт в этот туннель. Соединение начинает домашняя сторона, поэтому серый IP ему не мешает. Если туннеля ещё нет, начните с настройки WireGuard на своём VPS. Если нужен доступ не к одной машине, а ко всей домашней сети, прочитайте как пустить трафик WireGuard в домашнюю локальную сеть. Если нужен только один веб-сервис и туннель поднимать не хочется, посмотрите на обратный SSH-туннель из-за CGNAT.
Важная оговорка. Ни один туннель не проходит гарантированно от любого провайдера до зарубежного VPS. Протоколы VPN блокируют по-разному и в разное время. Выбор протокола под такие условия разобран отдельно: что запускать, когда VPN блокируют. Здесь туннель уже работает, и мы строим проброс поверх него.
Те же правила подходят ещё для двух случаев. Первый: второй сервер в частной сети провайдера, про неё есть статья как связать два VPS частной сетью. Второй: виртуальная машина или контейнер на самом VPS.
Пример, на котором построены все команды
- VPS с публичным адресом
203.0.113.10. - Внешний интерфейс VPS:
ens3. Ваш может называться иначе. Узнайте имя командойip route show default, оно стоит после словаdev. - Туннель WireGuard
wg0: у VPS адрес10.8.0.1, у домашнего сервера10.8.0.2. - Цель: TCP-порт 8080 на VPS ведёт на порт 80 домашнего сервера.
Везде, где встречаются эти адреса и имена интерфейсов, подставьте свои.
Что выбрать: iptables или nftables
На Ubuntu 24.04 команда iptables по умолчанию указывает на iptables-nft. Она принимает старый синтаксис, но записывает правила в nftables. Подробно это разобрано в статье iptables и nftables на Ubuntu: что работает на самом деле, здесь повторять не будем.
Практический вывод один: не смешивайте синтаксисы. Если на сервере есть ufw или Docker, они управляют правилами через iptables, и проще остаться в этом синтаксисе. Если сервер чистый и вам нужен один понятный файл, берите nft. Смешивать опасно вот почему. Правила iptables-nft и ваша собственная таблица nftables живут рядом, и пакет проходит через все базовые цепочки на одном хуке. Разрешение в одной таблице не отменяет запрет в другой. Значит, accept в вашей таблице не поможет, если цепочка FORWARD от iptables этот пакет отбрасывает.
Шаг 1. Как включить пересылку пакетов
По умолчанию Linux отбрасывает пакеты, адресованные не ему. Пересылку включает параметр ядра net.ipv4.ip_forward.
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-port-forward.conf
sudo sysctl --system
sysctl net.ipv4.ip_forwardПоследняя команда должна вывести net.ipv4.ip_forward = 1. Файл в /etc/sysctl.d/ нужен потому, что sysctl -w меняет значение только до перезагрузки. После обновления ядра сервер перезагрузится, и проброс тихо перестанет работать.
Шаг 2. Правило DNAT в prerouting
DNAT ставят в цепочку prerouting. Ядро проходит её до того, как решает, куда отправить пакет. Если поменять адрес назначения здесь, решение о маршруте примет уже новый адрес 10.8.0.2, и пакет уйдёт в wg0.
Вариант iptables:
sudo iptables -t nat -A PREROUTING -i ens3 -p tcp --dport 8080 \
-j DNAT --to-destination 10.8.0.2:80Вариант nftables, фрагмент файла /etc/nftables.conf (полный файл ниже):
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "ens3" tcp dport 8080 counter dnat to 10.8.0.2:80
}Слово counter считает пакеты, попавшие под правило. Оно понадобится при отладке. Условие -i ens3 или iifname "ens3" ограничивает проброс внешним интерфейсом. Без него пакет на порт 8080, пришедший из туннеля, тоже уйдёт на домашний сервер, а это обычно лишнее.
Шаг 3. Почему пакет нужно разрешить в forward, а не в input
После DNAT пакет попадает не в input, а в forward, потому что теперь он адресован другой машине. Отсюда частая ошибка. Правило ufw allow 8080 или tcp dport 8080 accept в цепочке input открывает порт для самого VPS. На пересылаемый трафик оно не влияет.
Второй важный момент: forward видит пакет уже после DNAT. Правило должно говорить об адресе 10.8.0.2 и порте 80, а не о порте 8080.
sudo iptables -A FORWARD -i ens3 -o wg0 -p tcp -d 10.8.0.2 --dport 80 \
-m conntrack --ctstate NEW -j ACCEPT
sudo iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTВторое правило пропускает ответы и все следующие пакеты уже открытого соединения. Без него первый пакет пройдёт, а ответ на него будет отброшен.
Шаг 4. Почему ответ уходит мимо VPS: обратный путь
Это главная часть всей схемы. Пакет дошёл до домашнего сервера. Адрес отправителя в нём прежний: публичный адрес клиента, например 198.51.100.7. Домашний сервер отвечает на этот адрес и смотрит в свою таблицу маршрутов. Маршрут по умолчанию у него ведёт на домашний роутер, а не в туннель. Ответ уходит мимо VPS, через провайдера, и с адресом отправителя, которого клиент не ждёт. Клиент такой ответ не примет, и соединение не установится.
У WireGuard есть вторая преграда. Для пира-VPS домашний сервер обычно держит AllowedIPs = 10.8.0.0/24. Этот список работает и как список доступа. Пакет из туннеля с адресом отправителя 198.51.100.7 в него не входит, поэтому WireGuard отбрасывает пакет ещё до того, как сервер успеет ответить.
Решений два. У каждого своя цена.
Вариант А: маскарадинг (SNAT) на VPS
VPS подменяет адрес отправителя на свой адрес в туннеле. Это SNAT (source NAT, подмена адреса отправителя), а MASQUERADE его разновидность, которая сама берёт адрес выходного интерфейса. Домашний сервер видит запрос от 10.8.0.1 и отвечает на 10.8.0.1, то есть через туннель. Ответ приходит на VPS. Ядро находит соединение в conntrack (таблице отслеживания соединений), отменяет обе подмены и отправляет ответ клиенту.
sudo iptables -t nat -A POSTROUTING -o wg0 -p tcp -d 10.8.0.2 --dport 80 -j MASQUERADEЦена: бэкенд больше не видит адрес клиента. В журналах веб-сервера все запросы приходят с 10.8.0.1. fail2ban на бэкенде в такой схеме опасен: после нескольких неудачных попыток входа он заблокирует 10.8.0.1, то есть сам VPS, и проброс перестанет работать для всех. Для веб-сервисов настоящий адрес можно передать на уровне приложения, через заголовок X-Forwarded-For или протокол PROXY. Это уже настройка прокси, а не фаервола.
Зато этот вариант ничего не требует от бэкенда. Для второго VPS в частной сети провайдера это обычно самый простой путь.
Вариант Б: ответы через туннель, адрес клиента сохраняется
Маскарадинга нет. Вместо этого домашний сервер отправляет в туннель все пакеты со своего туннельного адреса. Это маршрутизация по источнику (policy routing): отдельная таблица маршрутов для пакетов с адресом отправителя 10.8.0.2.
Конфигурация /etc/wireguard/wg0.conf на домашнем сервере:
[Interface]
PrivateKey = <приватный ключ домашнего сервера>
Address = 10.8.0.2/24
Table = off
PostUp = ip route add default dev %i table 200
PostUp = ip rule add from 10.8.0.2 table 200
PreDown = ip rule del from 10.8.0.2 table 200
PreDown = ip route del default dev %i table 200
[Peer]
PublicKey = <публичный ключ VPS>
Endpoint = 203.0.113.10:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25Разберём по строкам. AllowedIPs = 0.0.0.0/0 разрешает принимать из туннеля пакеты от любых адресов клиентов. Обычно такая строка заставляет wg-quick пустить через туннель весь трафик машины. Table = off запрещает wg-quick трогать основную таблицу маршрутов. Маршрут по умолчанию через туннель живёт только в таблице 200, а правило ip rule отправляет в эту таблицу только пакеты с адресом отправителя 10.8.0.2. Остальной домашний трафик идёт как раньше.
Перезапустите интерфейс командой sudo systemctl restart wg-quick@wg0 и проверьте ip rule show. В списке должно появиться правило from 10.8.0.2 lookup 200.
Цена: настройка на бэкенде сложнее, а VPS должен пропускать ответы назад. Правило ESTABLISHED,RELATED из шага 3 как раз это делает. Зато журналы и fail2ban на домашнем сервере видят настоящие адреса клиентов.
Когда обратный путь уже есть
Если VPS и так служит шлюзом по умолчанию для бэкенда, четвёртое условие выполнено само. Так обычно устроены виртуальные машины и контейнеры на самом VPS: их маршрут по умолчанию ведёт на хост. Тогда маскарадинг не нужен, и бэкенд видит реальный адрес клиента.
Полный файл nftables для проброса порта
Этот вариант для чистого сервера без ufw и Docker. Файл начинается с flush ruleset, то есть удаляет все действующие правила, включая правила ufw и Docker. Пока применяете файл, держите открытой вторую SSH-сессию. Политика drop в input вместе с опечаткой в правиле для SSH закроет вам доступ к серверу.
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif lo accept
tcp dport 22 accept
udp dport 51820 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
iifname "ens3" oifname "wg0" ip daddr 10.8.0.2 tcp dport 80 ct state new counter accept
}
}
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "ens3" tcp dport 8080 counter dnat to 10.8.0.2:80
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "wg0" ip daddr 10.8.0.2 tcp dport 80 counter masquerade
}
}Проверьте синтаксис без применения, затем включите службу:
sudo nft -c -f /etc/nftables.conf
sudo systemctl enable nftables
sudo systemctl restart nftablesnft -c только читает файл и сообщает об ошибках, правила при этом не меняются. Для варианта Б удалите цепочку postrouting целиком. Если VPS одновременно служит выходом в интернет для клиентов VPN, политика drop в forward отрежет и их. Добавьте для них отдельное правило accept.
Как сохранить правила iptables после перезагрузки
Команды iptables меняют правила в памяти, и после перезагрузки их не будет. На сервере без ufw правила сохраняет пакет iptables-persistent:
sudo apt install -y iptables-persistent
sudo netfilter-persistent saveПравила записываются в /etc/iptables/rules.v4 и загружаются при старте системы. На Ubuntu этот пакет конфликтует с ufw, и apt предложит удалить ufw. Если ufw вам нужен, используйте способ из следующего раздела.
Как сделать проброс портов через ufw
ufw по умолчанию отбрасывает пересылаемый трафик: в /etc/default/ufw стоит DEFAULT_FORWARD_POLICY="DROP". Можно сменить политику на ACCEPT, но точнее разрешить только нужный поток:
sudo ufw route allow in on ens3 out on wg0 to 10.8.0.2 port 80 proto tcpПравило снова говорит об адресе 10.8.0.2 и порте 80, потому что forward видит пакет после DNAT. Отдельной команды для DNAT у ufw нет. Правила NAT пишут вручную в начало /etc/ufw/before.rules, перед секцией *filter:
*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A PREROUTING -i ens3 -p tcp --dport 8080 -j DNAT --to-destination 10.8.0.2:80
-A POSTROUTING -o wg0 -p tcp -d 10.8.0.2 --dport 80 -j MASQUERADE
COMMITЗатем выполните sudo ufw reload. Строка COMMIT обязательна: без неё ufw не сможет загрузить файл. Если после правки ufw не запускается, пошаговый разбор есть в статье как восстановить сломанный ufw. Основы самого ufw описаны в руководстве по ufw для VPS.
Почему проброс не работает: Docker и политика DROP
Docker с бэкендом iptables, когда сам включает ip_forward, ставит политику цепочки FORWARD в DROP. В документации Docker о фильтрации пакетов прямо сказано, что такая политика не даёт хосту работать маршрутизатором. Симптом: счётчик DNAT растёт, а до бэкенда ничего не доходит.
Выходов два. Первый: явные правила ACCEPT в FORWARD из шага 3. Они пропускают ваш поток при любой политике цепочки. Второй, по той же документации (на октябрь 2026 года): строка "ip-forward-no-drop": true в /etc/docker/daemon.json, после которой Docker не меняет политику. С экспериментальным бэкендом nftables Docker, по словам документации, такую политику не создаёт вовсе. Отдельная ловушка Docker в том, что опубликованные им порты обходят ufw. Она разобрана в статье почему порты Docker обходят ufw.
Почему проброс не работает при проверке с самого VPS
curl http://203.0.113.10:8080 на самом VPS ничего не доказывает. Пакеты, созданные локально, проходят цепочку output, а не prerouting, поэтому правило DNAT их не видит. Запрос уйдёт на порт 8080 самого VPS, где никто не слушает. Вы получите отказ в соединении, хотя проброс исправен.
Проверяйте снаружи: с другого сервера или с телефона через мобильный интернет.
curl -v http://203.0.113.10:8080/Другие способы проверить порт снаружи собраны в статье как проверить, открыт ли порт в Linux. Помните и про сетевой фаервол в панели провайдера. У многих хостеров это отдельный фильтр, который режет порт ещё до того, как пакет попадёт на VPS.
Как найти шаг, на котором теряется пакет
Идите по пути пакета и смотрите на счётчики правил. Запустите проверку снаружи и в это время выполните на VPS:
sudo iptables -t nat -L PREROUTING -n -v
sudo iptables -L FORWARD -n -v
sudo nft list rulesetПервые две команды для схемы на iptables, третья для nftables. Как читать полный список правил, разобрано в статье как посмотреть активные правила фаервола в Ubuntu.
- Счётчик DNAT стоит на нуле. Пакет не доходит до VPS или приходит на другой интерфейс. Проверьте фаервол в панели провайдера. Затем сверьте имя интерфейса в правиле с выводом
ip route show default. - Счётчик DNAT растёт, а счётчик правила в forward нет. Пакет отбрасывается в forward. Проверьте
sysctl net.ipv4.ip_forward, затем политику цепочкиFORWARD, особенно если на сервере есть Docker. - Оба счётчика растут, а клиент ждёт до таймаута. Пакет ушёл к бэкенду, но ответа нет. Запустите на VPS
sudo tcpdump -ni wg0 tcp port 80. Если видны только исходящие пакеты с флагом SYN и ни одного ответа, проблема в обратном пути из шага 4 или в фаерволе самого бэкенда. - Клиент сразу получает отказ в соединении. Пакет дошёл, но на бэкенде порт 80 никто не слушает. Проверьте
sudo ss -tlnpна бэкенде. Сервис, который слушает только127.0.0.1, недоступен снаружи даже через проброс.
Проброс UDP и диапазона портов
UDP пробрасывается так же. Замените -p tcp на -p udp, а tcp dport на udp dport. Диапазон портов в iptables пишут через двоеточие, в nftables через дефис:
sudo iptables -t nat -A PREROUTING -i ens3 -p udp --dport 27015:27020 \
-j DNAT --to-destination 10.8.0.2iifname "ens3" udp dport 27015-27020 dnat to 10.8.0.2Если в адресе назначения нет номера порта, порт в пакете остаётся прежним. Не забудьте разрешить тот же диапазон в forward. Для варианта А добавьте его и в postrouting.
FAQ
Почему правило DNAT есть, а проброс порта не работает?
Проверьте четыре условия по порядку. sysctl net.ipv4.ip_forward должен показывать 1. Правило DNAT должно стоять в prerouting на внешнем интерфейсе. Цепочка forward должна пропускать пакет с адресом и портом бэкенда, а не с публичным портом, потому что forward видит пакет уже после DNAT. У ответа должен быть путь назад через VPS: маскарадинг на VPS или маршрутизация по источнику на бэкенде. Счётчики правил показывают, на каком шаге теряется пакет.
Почему проброс не работает, если проверять с самого VPS?
Пакеты, созданные на самом VPS, проходят цепочку output, а не prerouting, поэтому правило DNAT к ним не применяется. Запрос уходит на порт самого VPS, где обычно никто не слушает, и вы получаете отказ в соединении. Проверяйте проброс с другой машины, например с телефона через мобильный интернет.
Нужен ли маскарадинг для проброса портов?
Нужен, если маршрут по умолчанию у бэкенда ведёт не через VPS. Без маскарадинга ответ уходит мимо VPS, и соединение не устанавливается. Цена маскарадинга: бэкенд видит адрес VPS вместо адреса клиента, поэтому журналы и fail2ban на бэкенде теряют смысл. Если реальный адрес важен, настройте на бэкенде маршрутизацию по источнику, чтобы ответы шли обратно через туннель.
Как пробросить порт на домашний сервер с серым IP?
Поднимите туннель с домашней машины до VPS, например WireGuard. Соединение начинает домашняя сторона, поэтому серый IP ему не мешает. Затем на VPS включите ip_forward и добавьте DNAT с публичного порта на туннельный адрес домашнего сервера. Разрешите этот поток в forward и обеспечьте обратный путь. Пройдёт ли туннель через вашего провайдера, зависит от того, что именно провайдер блокирует.
Почему после установки Docker проброс портов перестал работать?
Docker с бэкендом iptables, когда включает пересылку пакетов, ставит политику цепочки FORWARD в DROP. После этого пересылаемый трафик без явного разрешения отбрасывается. Добавьте правило ACCEPT для своего потока в FORWARD или задайте "ip-forward-no-drop": true в /etc/docker/daemon.json и перезапустите Docker.