MAATRIX / Блог / Порт открыт, а подключения нет: разбираем путь пакета от карты до сокета

Порт открыт, а подключения нет: разбираем путь пакета от карты до сокета

MAATRIX

Сканер снаружи бодро рапортует: 443/tcp open. А клиент всё равно упирается в таймаут, curl виснет на «Connected to…», или соединение рвётся сразу после установки. Первая реакция — «порт же открыт, значит дело не в сети» — и начинается охота на призраков в коде приложения, хотя проблема почти всегда в одном из пяти конкретных мест между сетевой картой сервера и вызовом accept() в вашем процессе. Разберём этот путь по шагам и в том порядке, в котором его реально стоит проверять, чтобы не гадать.

Сетевая карта и ядро хоста: пакет доехал, а дальше?

Кадр приходит на сетевой контроллер, тот через DMA кладёт его в кольцевой буфер памяти (RX ring), дальше срабатывает прерывание, ядро через механизм NAPI забирает пакет из буфера и передаёт выше по стеку — сначала на уровень Ethernet, затем IP. Механику этого этапа в деталях — прерывания, softirq, где физически теряются пакеты при переполнении кольца — мы разбирали отдельно в статье про путь пакета от сетевой карты до сокета внутри ядра. Здесь не повторяем эту часть — важно держать в уме, что первые потери случаются ещё до всякого firewall, просто из-за нехватки буферов или перегруженного прерываниями ядра CPU.

Для темы этой статьи важнее следующий шаг: как только пакет добрался до IP-уровня, ядро принимает решение маршрутизации. Для трафика, адресованного самому хосту, решение простое — «доставить локально», и ядро сверяется с локальной таблицей маршрутов:

ip route show table local

Тут кроется первая тонкая ловушка: если IP назначения пакета не входит в список адресов, которые ядро считает «своими» (например, публичный IP приходит через провайдерский NAT, а внутри хоста настроен только приватный адрес, и alias или floating IP не поднят так, как ожидает облако), пакет не считается локальным — ядро попробует его маршрутизировать дальше или отбросит. Внешне это выглядит ровно как «сервер не отвечает», хотя сетевая карта пакет честно приняла.

Если локальная доставка подтверждена, дальше пакет проходит через цепочки netfilter (PREROUTING → решение о маршрутизации → INPUT для локального трафика) и только после прохождения INPUT-цепочки ядро ищет сокет, слушающий нужный адрес и порт. Вот тут начинается следующий слой — и самый частый источник путаницы.

Firewall хоста: правило на порт — не единственное правило в игре

Главная ошибка в диагностике — думать про firewall как про бинарный переключатель «порт разрешён / порт запрещён». На деле и iptables, и nftables — это упорядоченный список правил, и решение по каждому пакету принимает первое совпавшее правило, а не самое очевидное для человека.

Классический случай: у вас есть широкое правило «разрешить 443/tcp», но выше по цепочке стоит более специфичное правило, которое блокирует конкретный источник — подсеть, забаненный fail2ban IP, интерфейс:

iptables -L INPUT -n -v --line-numbers

Флаг -v здесь не для красоты — он показывает счётчики пакетов и байт по каждому правилу. Если счётчик у правила «разрешить порт» растёт, а клиент всё равно не проходит, скорее всего, где-то раньше в цепочке уже сработало другое правило и до вашего разрешающего пакет просто не дошёл. Для nftables то же самое смотрите через:

nft list ruleset
nft -a list chain inet filter input

Второй момент, который часто путает: разница между DROP и REJECT. DROP молча съедает пакет — клиент не получает ответа и висит на таймауте TCP-ретрансмиссий (выглядит как «сеть где-то потеряла пакет», хотя пакет доехал и был осознанно отброшен). REJECT явно отвечает RST или ICMP unreachable — клиент получает быстрый и однозначный отказ. Мгновенный Connection refused на клиенте — почти наверняка REJECT (в firewall хоста или у провайдера) или просто отсутствие слушателя на порту. Долгий таймаут — вероятнее DROP или переполненная очередь ядра (см. дальше про backlog).

Третий частый источник: правила, завязанные не на порт, а на состояние соединения через conntrack. Стейтфулный firewall обычно требует явно разрешить возврат трафика (ESTABLISHED,RELATED) отдельно от новых соединений (NEW). В самописных цепочках nftables, где про обратный трафик для отдельного интерфейса забыли, рукопожатие может завершиться благополучно, а дальнейший обмен данными — уже нет. Конкретный случай, когда firewall формально настроен на запрет, а порт всё равно доступен из-за особенностей публикации портов в контейнерах, разобран в статье про то, как Docker пробивает firewall и почему порт всё равно открыт: правила, которые видит iptables -L, не всегда полная картина — Docker добавляет собственные цепочки, встающие раньше пользовательских правил в INPUT.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Security group и Network ACL провайдера: слой, который проверяют реже всего

Вот здесь чаще всего застревает диагностика «снаружи» — потому что этот слой физически не касается вашего сервера. Security group (SG) и Network ACL (NACL) — механизмы облачного провайдера, которые фильтруют трафик на уровне сети дата-центра, до того, как пакет вообще долетел до сетевой карты вашей виртуальной машины. Если правило SG или NACL блокирует пакет, вы не увидите его ни в iptables, ни в tcpdump на самом сервере — с точки зрения хоста этого пакета попросту никогда не существовало.

Отсюда две типичные ловушки:

  • Вы тестируете firewall хоста и не находите проблему, потому что смотрите не на том слое. iptables -L -v показывает нулевые счётчики по всем relevant-правилам просто потому, что до них дело не доходит — пакет отсекли раньше.
  • Сканирование снаружи проходит, а конкретный клиент — нет. Если правило SG разрешает порт с широкого диапазона адресов (например, 0.0.0.0/0), практически любой сканер увидит порт открытым. Но если параллельно есть более узкое правило NACL, которое режет конкретную подсеть, — клиенты из неё получат отказ, а сканер с произвольного IP пройдёт спокойно. Security group обычно стейтфул: оценивает только входящее правило, обратный трафик разрешается автоматически. Network ACL обычно стейтлес: требует явных правил в обе стороны и оценивается по номерам с явным deny. Отсюда частый источник путаницы — правило NACL обрезает именно возвратный трафик, хотя входящий SYN прошёл нормально.

Мы намеренно не приводим здесь точные пошаговые инструкции под конкретную панель конкретного облака — они разные у каждого провайдера и быстро устаревают. Смысл шага один: прежде чем копать iptables на сервере, откройте раздел сетевой безопасности в панели провайдера и убедитесь, что нужный порт, протокол и, если правило это поддерживает, конкретный source-адрес клиента разрешены именно там — отдельно от всего, что настроено внутри ОС.

Приложение слушает не там, где вы думаете

Если пакет прошёл security group, прошёл firewall хоста и добрался до ядра — последняя точка отказа перед вашим кодом это то, реально ли что-то слушает именно тот адрес и порт, куда он адресован.

Ключевая деталь — адрес привязки (bind address), а не только номер порта:

  • 0.0.0.0 (или :: для IPv6) — приложение принимает соединения на всех сетевых интерфейсах хоста, включая публичный IP.
  • 127.0.0.1 — только петля (loopback). Соединения снаружи, да и вообще с любого другого локального интерфейса, туда физически не долетают — ядро отвечает RST ещё на этапе поиска сокета, даже не касаясь firewall или приложения.
  • Конкретный IP (например, 10.0.0.5) — только этот интерфейс. Если у сервера несколько сетевых адресов, а публичный трафик приходит на другой — сокет его просто не увидит.

Проверяется это одной командой:

ss -tlnp | grep :443

В колонке Local Address:Port видно, на что реально забинжен процесс — 0.0.0.0:443, 127.0.0.1:443 или конкретный адрес. Частый сценарий: многие фреймворки и dev-серверы по умолчанию слушают localhost, и это прекрасно работает, когда вы проверяете curl localhost:PORT прямо на сервере по SSH — трафик не выходит на настоящую сетевую карту, а идёт по петле. Внешний же клиент бьёт в публичный IP, который приложение не слушает, — и получает отказ, никак не связанный ни с firewall, ни с security group, при том что оба слоя настроены абсолютно правильно. Подробнее про чтение вывода ss и netstat и состояния соединений — в статье про то, что ss и netstat реально показывают о ваших соединениях.

Второй сценарий — порт «отвечает», но не тем сервисом, который вы ожидаете: старый или совсем другой процесс остался висеть на том же порту после перезапуска, и сканер честно увидит open, потому что кто-то отвечает на SYN. Проверка та же — ss -tlnp покажет PID и имя процесса, который реально держит порт.

Backlog: сокет слушает, но очередь на входе переполнена

Даже когда всё выше в порядке — процесс жив, слушает нужный интерфейс, firewall и security group пропускают трафик, — остаётся ещё одна точка отказа, которая маскируется под «сервер работает, но иногда не пускает». У каждого слушающего сокета есть очередь подключений, уже прошедших сетевое рукопожатие, но ещё не забранных приложением через accept(). Если приложение не успевает вызывать accept() так же быстро, как приходят соединения, очередь переполняется, и новые клиенты получают либо тихий таймаут, либо явный RST — в зависимости от net.ipv4.tcp_abort_on_overflow.

Снаружи это выглядит особенно обманчиво: ping проходит, порт по-прежнему числится открытым при сканировании, процесс жив с точки зрения systemd — а часть клиентов всё равно не может достучаться. Мы разбирали эту механику подробно, включая разницу между очередью незавершённых SYN-соединений и очередью уже готовых, но не принятых, — в статье про backlog очереди соединений и то, почему клиент получает отказ на живом сервере. Там же — как читать колонки Recv-Q/Send-Q у ss -lnt для сокетов в состоянии LISTEN: там эти цифры означают текущую длину очереди и максимальный backlog, а не объём буферизованных данных, как для обычных установленных соединений.

Порядок диагностики: снаружи внутрь

Когда «порт открыт, а подключения нет» — есть смысл идти по слоям строго в одном порядке, от внешнего наблюдателя к самому процессу, а не нырять сразу в конфиг приложения, потому что интуиция обычно подсказывает неверный слой первым.

1. Подтвердите, что видно снаружи, и с какого источника. Сканируйте не только со своей рабочей машины, а по возможности с адреса, максимально близкого к реальному проблемному клиенту — из другого дата-центра, другой подсети, другого провайдера:

nmap -Pn -p 443 <ip-сервера>
nc -zv <ip-сервера> 443

Если результат отличается в зависимости от источника скана — почти наверняка это признак правила, завязанного на source-адрес: на уровне security group/NACL или firewall хоста (allowlist, fail2ban-бан конкретного IP).

2. Проверьте security group и network ACL в панели провайдера. Раньше, чем что-либо на сервере — блок на этом уровне не оставит следов ни в iptables, ни в tcpdump. Смотрите: разрешён ли нужный порт и протокол, разрешён ли source-адрес проблемного клиента, а для stateless ACL — разрешён ли ещё и обратный трафик.

3. Проверьте firewall самой ОС. iptables -L -n -v --line-numbers или nft list ruleset — смотрите не только на наличие разрешающего правила, но и на счётчики пакетов, и на то, что стоит выше него в цепочке. Отдельно проверьте, не форвардит ли что-то поверх ваших правил Docker или другой оверлей со своими цепочками.

4. Проверьте, что реально слушает порт. ss -tlnp | grep :<порт> — адрес привязки, PID и имя процесса. Если сокет в порядке — гляньте на ss -lnt для этого порта на предмет забитой очереди и в логи приложения на предмет ошибок вида address already in use или таймаутов accept().

Если тестируете связность между двумя своими серверами, а не с публичным клиентом, порядок и набор проверяемых слоёв практически такой же, но с парой дополнительных пунктов — маршрутизация внутри частной сети и состояние VPN-туннеля.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как понять, что блокировка произошла на уровне security group облака, а не firewall самой ОС?

Запустите tcpdump -ni <интерфейс> port <порт> на сервере в момент попытки подключения. Если SYN туда вообще не долетает — блокировка произошла раньше, на уровне провайдера. Если SYN виден в дампе, но ответа нет или пришёл RST, — проблема уже внутри хоста: firewall ОС либо ничего не слушает порт.

Почему один и тот же порт показывает open при сканировании с одного адреса и filtered/closed с другого?

Почти всегда это правила, завязанные на source-адрес: широкое разрешающее правило в security group плюс более узкое ограничение (по IP, подсети или бан от fail2ban) на другом уровне. Сканируйте с адреса, максимально похожего на реального клиента.

Что означает open|filtered в выводе nmap?

Nmap не получил однозначного ответа на пробу — ни подтверждения, ни явного отказа. Чаще всего так ведут себя пакеты, молча отброшенные (DROP), а не отклонённые явно (REJECT). Полноценный TCP-connect скан (nmap -sT или nc -zv) обычно даёт более однозначную картину, чем SYN-скан без прав root.

Приложение слушает 127.0.0.1, а curl прямо на сервере работает — как так?

Curl на localhost/127.0.0.1 идёт по петлевому интерфейсу и не выходит на настоящую сетевую карту — с точки зрения ядра это другой путь пакета, чем у внешнего клиента. Внешний запрос адресован публичному IP, а сокет, слушающий только loopback, для ядра просто не существует по этому адресу — отсюда RST ещё до firewall и до приложения.

Стоит ли менять DROP на REJECT, чтобы клиенты быстрее получали ответ?

Это компромисс. REJECT даёт быстрый и понятный отказ, что удобно для диагностики. DROP скрывает факт существования сервиса от случайных сканеров, но заставляет отклонённых клиентов ждать таймаута. Выбор зависит от того, что важнее — скорость обратной связи или маскировка присутствия.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →