Перейти к содержимому
RootPoligon

Порт и процесс

Чтение и практика ~70 мин

вы сопоставляете адрес и порт, куда идёт клиент, с тем, что на самом деле слушает компьютер; различаете «не слушает», «слушает не там» и «слушает, но не отвечает»; не принимаете отказ или тайм-аут за готовый диагноз.

Лаборатория и подготовка — архив один для всех пяти глав. Перед новым сценарием остановите прежний через Ctrl+C.

Что нового

Соединение устанавливается не с «сервисом», а с парой адрес:порт. Принять его может только процесс, который слушает именно эту пару. В лаборатории вся сеть 127.0.0.0/8 — петля, и адреса 127.0.0.1, 127.0.0.2, 127.0.0.3 — разные: на каждом может слушать свой процесс (на другой системе то же самое бывает с разными сетевыми интерфейсами). Лаборатория не слушает «все адреса» (0.0.0.0) — в настоящих системах это отдельное решение по безопасности, и в этой главе мы его не принимаем.

Два сообщения клиента, которые легко принять за диагноз:

  • Отказ (curl код 7, в --verbose — Connection refused): клиент получил отказ от пары адрес:порт. Типичная причина — на этой паре никто не слушает. Бывает и правило, которое отвечает отказом нарочно; в этой лабе такого правила нет, файрвол компьютера мы не меняем и не проверяем.
  • Тайм-аут (код 28): ответ не пришёл вовремя. Где именно — показывает --verbose: если строки Connected to … нет, не установилось само соединение; если она есть, рукопожатие прошло, а HTTP-ответа нет — файрвол, не пропускающий новые соединения, тогда исключён. Сказать по одному тайм-ауту «это файрвол» нельзя: до Connected причин несколько, а после — файрвол этого вида ни при чём.

Что показывает ss -ltn: -l — только слушающие сокеты, -t — TCP, -n — числа вместо имён; колонка Local Address:Port — пара, которую слушают. Ключ -p добавляет процесс-владельца (по общему правилу Linux для чужих процессов нужны права администратора — в этой лабе не проверялось; процесс лаборатории ваш). Узнать, слушает ли пара, можно и «снаружи»: nc -z -w 2 АДРЕС ПОРТ пробует соединиться и сообщает результат кодом выхода. Запись заметки без посредников и имён делает python3 lab/netlab.py fetch --data '{"title": "текст"}' URL — это POST.

Подготовка

Из папки курса, в первом окне: python3 lab/netlab.py start ch3. В первом окне будет журнал приложения, остальное — во втором. В выводе ss будут и посторонние строки вашего компьютера — ищите нужный порт.

Ситуация

Суббота, 17 октября, 11:00. Ночью «Заметки» обновили; с утра пользователи видят «не удалось подключиться». Служба, как всегда, на http://notes.lab.test:18080/api/notes. Дежурный Пётр пишет в чат:

Процесс заметок жив, я его вижу в списке процессов. Перезапускал — не помогло. Скорее всего, порт 18080 закрыт файрволом. Давайте откроем его для всех адресов.

Воспроизведите симптом:

python3 lab/netlab.py fetch http://notes.lab.test:18080/api/notes
echo "код выхода: $?"

Мини-задача

Задача: выяснить, почему запрос не доходит до приложения, и вернуть доступ — не потеряв заметки и не открывая порт «для всех». Готовой цепочки команд нет: инструменты вы знаете (fetch с --verbose, dig, ss, nc -z, curl), порядок выбираете сами.

До любого наблюдения, кроме повтора симптома, запишите:

  1. Не меньше двух гипотез, каждая из которых объясняет этот симптом. Одна — слова Петра; вторая — ваша.
  2. Для каждой — что вы увидели бы в наблюдении, которое её подтверждает, и в том, которое её исключает.
  3. Одно наблюдение, которое различает ваши гипотезы. Выполните его и запишите вывод — одна-две строки — и то, какие гипотезы он исключил.

Затем одним предложением назовите причину в форме «клиент идёт на …, а слушает …» и приложите две строки вывода: одну со стороны клиента, одну со стороны сервера.

Разбор мини-задачи

Откройте, когда записали свои гипотезы и наблюдение

Верных путей два; оба приводят к одной паре строк.

Путь с клиента. fetch --verbose показывает, куда клиент пошёл:

*   Trying 127.0.0.1:18080...
* connect to 127.0.0.1 port 18080 from 127.0.0.1 port 48934 failed: Connection refused
curl: (7) Failed to connect to notes.lab.test port 18080 after 0 ms: Couldn't connect to server

Назначение — 127.0.0.1:18080. Теперь ss -ltn и поиск порта 18080:

LISTEN 0      5          127.0.0.3:18080      0.0.0.0:*

Порт слушают, но на 127.0.0.3, а не на 127.0.0.1.

Путь с сервера. ss -ltnp 'sport = :18080' показывает не только адрес, но и владельца:

LISTEN 0      5          127.0.0.3:18080      0.0.0.0:*    users:(("python3",pid=1901,fd=4))

Процесс жив и порт слушает — слова Петра про «жив» верны, но «не слушает» и «закрыт» не следуют. Дальше: что клиент берёт из DNS — dig notes.lab.test → 127.0.0.1, TTL 300. Имя ведёт на адрес, где слушателя нет.

Что это доказывает. Клиент идёт на 127.0.0.1:18080; на этой паре никто не слушает — отсюда отказ; процесс слушает другую пару, 127.0.0.3:18080. nc -z -w 2 127.0.0.1 18080 — отказ (код 1), nc -z -w 2 127.0.0.3 18080 — успех (код 0), curl -H 'Host: notes.lab.test' http://127.0.0.3:18080/api/notes — 200 и три заметки: приложение исправно.

Что это не доказывает. Отказ сам по себе не назвал бы причину: «никто не слушает», «слушают не там» и «правило отвечает отказом» выглядят у клиента одинаково. Причину назвало наблюдение ss — и подтвердит исправление: после него запрос пройдёт, а файрвол останется нетронутым. Гипотеза Петра «порт закрыт файрволом» не опровергнута навсегда, но она не нужна для объяснения симптома — и открывать порт для всех адресов ради неё значит менять защиту без доказательства.

Две разные цепочки. Если вы начали с ss, а не с fetch --verbose, это тоже верно: главное — сопоставить пару, куда идёт клиент, с парой, которую слушают.

Развилка: 11:20, что делать

Конфигурацию приложения ведёт Борис (разработчик, на связи), запись DNS — Ирина (инфраструктура, на связи). Решения (порядок случайный):

  • А. Перезапустить службу ещё раз с той же конфигурацией (в лабе: остановить окно с start и запустить заново).
  • Б. Привязать приложение к адресу, на который ведёт имя: python3 lab/netlab.py set app_bind=127.0.0.1 (лаба применяет настройку к работающему сценарию, как перезагрузку конфигурации). Предупредить, что адрес 127.0.0.3 перестанет отвечать.
  • В. Попросить Ирину поменять запись notes.lab.test на 127.0.0.3 (в лабе: set dns_notes=127.0.0.3), проверить dig.
  • Г. Открыть порт 18080 для всех адресов (в лабе: set app_bind=0.0.0.0 — лаба такую настройку отвергнет; это её защита, а не ответ на вопрос «стоит ли»).
  • Д. Передать разработчикам: «приложение не отвечает».

Для выбранного решения: что изменится для пользователя через пять минут, что сломается у тех, кто ходил по прежнему адресу, и как вернуть всё назад.

Перед ремонтом создайте контрольную заметку напрямую по адресу, где приложение сейчас отвечает, и запишите число заметок — оно понадобится в приёмке.

Последствия
  • А. Приложение поднимется с той же конфигурацией и слушает там же — ничего не изменится. В лабе перезапуск к тому же вернёт список заметок к исходным трём: данные хранятся в памяти лаборатории, как у контейнера без тома. Бесполезное и с риском потерять записи.
  • Б. Допустимо: исправляет расхождение со стороны приложения. Имя разрешается в 127.0.0.1 — клиенты подключаются сразу, ждать TTL не нужно. Цена: тот, кто ходил на 127.0.0.3 напрямую, получит отказ — нужно спросить Бориса, есть ли такие. Откат: set app_bind=127.0.0.3. Заметки на месте: настройка применяется без перезапуска процесса лаборатории.
  • В. Допустимо: исправляет расхождение со стороны имени. Цена: клиенты и резолверы с прежним ответом ходят на старый адрес до пяти минут (TTL 300); любой, кто прописал 127.0.0.1 в настройках, остаётся без службы. Откат: вернуть запись.
  • Г. Вредное: симптом объяснён и без файрвола; решение расширяет круг тех, кто может подключиться, без доказательства, что это нужно. В лабе отвергается.
  • Д. Бесполезное: приложение живо и исправно (curl на 127.0.0.3 — 200); разработчики ничего не найдут, пока простой идёт.

Приёмка

  • Гипотезы и предсказания записаны до наблюдений; одно из них — слова Петра.
  • Выполнено одно различающее наблюдение; записано, какие гипотезы оно исключило.
  • Причина названа в форме «клиент идёт на …, слушает …» с двумя строками вывода (клиент и ss).
  • Выбранное решение называет цену и откат.
  • Запрос пользователя fetch http://notes.lab.test:18080/api/notes возвращает 200.
  • Контрольная заметка на месте: число заметок после ремонта = число до него (с учётом вашей записи).
  • Не менялись файрвол и адрес «все интерфейсы».

Подсказки

Подсказка 1 — направление

Отказ говорит об одной конкретной паре адрес:порт — той, куда пришёл клиент. Куда именно он пришёл? И что слушает эта пара?

Подсказка 2 — нужное наблюдение

Сравните две пары: куда соединяется клиент (--verbose, строка Trying …) и какие адреса слушает порт 18080 (ss -ltn, колонка Local Address:Port). Если пары не совпадают — вы нашли расхождение.

Подсказка 3 — разбор

Клиент идёт на 127.0.0.1:18080 (так велит DNS), а процесс слушает 127.0.0.3:18080. На первой паре слушателя нет — отсюда отказ; процесс жив и исправен (ss -p, прямой запрос на 127.0.0.3). Допустимы Б (привязать приложение к 127.0.0.1) и В (поменять запись на 127.0.0.3) — с разной ценой: Б не ждёт TTL, но отрежет тех, кто ходил на 127.0.0.3; В ждёт до 5 минут и ломает тех, кто прописал 127.0.0.1. А ничего не меняет, Г меняет защиту без доказательств, Д передаёт исправное приложение. Опасность для данных — только в перезапуске лаборатории: заметки живут в её памяти.

Второй вариант

Та же суббота, 11:50. Вы (или Пётр) исправили адрес: приложение слушает 127.0.0.1:18080, DNS ведёт туда же. Но теперь пользователи пишут другое: «страница висит, ответа нет». Перезапустите лабу со сценарием ch3b (остановите прежний сценарий в первом окне за Ctrl+C и запустите python3 lab/netlab.py start ch3b). Опор меньше: повторите симптом python3 lab/netlab.py fetch --max-time 3 http://notes.lab.test:18080/api/notes и действуйте так же, как в основной части: гипотезы с предсказаниями, одно различающее наблюдение, причина одним предложением, решение с ценой и откатом.

Условие приёмки то же, плюс: объясните, почему правка из основной части — привязать приложение к 127.0.0.1 или перенести имя на 127.0.0.3 — здесь не помогает, и покажите это запуском, а не рассуждением.

Решения (порядок случайный; ключи настроек — для действий после диагностики):

  • А. Привязать приложение к 127.0.0.1: set app_bind=127.0.0.1.
  • Б. Перенести имя на 127.0.0.3: set dns_notes=127.0.0.3.
  • В. Открыть порт 18080 на файрволе для всех адресов.
  • Г. Заставить приложение снова принимать соединения (перезапуск его рабочего цикла): set app_mode=normal; собрать перед этим вывод ss и журнал для разработчиков.
  • Д. Не трогать приложение, собрать вывод ss, журнал и время начала и передать Борису; пока — предупредить поддержку.
  • Е. Дать клиенту больший лимит ожидания: fetch --max-time 60 ….
Подсказка 1 — направление

Тайм-аут и отказ — разные вещи. Что показывает --verbose до строки с тайм-аутом: есть ли Connected to?

Подсказка 2 — нужное наблюдение

Посмотрите ss -ltn для порта 18080 до запроса и после нескольких запросов: что делает число в колонке Recv-Q слушателя? И что говорит nc -z про этот порт?

Подсказка 3 — разбор

Соединение устанавливается (Connected to notes.lab.test (127.0.0.1) port 18080), nc -z успешен, но HTTP-ответа нет, а Recv-Q слушателя растёт с каждым запросом (0 → 2 после двух попыток): ядро принимает рукопожатия и ставит соединения в очередь, процесс их не берёт (accept). Адрес и порт верны, поэтому правка из основной части не меняет ничего (А) или превращает тайм-аут в отказ (Б: на 127.0.0.3 слушателя нет). Файрвол, не пускающий новые соединения, исключён: рукопожатие прошло (В); на петле нет потерь по пути — в настоящей сети это пришлось бы проверять отдельно. Допустимы Г (вернуть приём соединений, с записью улик для разработчиков) и Д (если простой терпим и Борис на связи). Е бесполезно — приложение не ответит и через 60 секунд.

Тайм-аут до «Connected»: тоже не файрвол

Для тех, кто выполнил второй вариант. Остановите сценарий и запустите его с очередью на одно соединение:

python3 lab/netlab.py start ch3b --set backlog=1

Сделайте два запроса fetch --max-time 1 …, затем третий с --verbose --max-time 2 и посмотрите ss -ltn для порта 18080. Запишите: есть ли теперь строка Connected to, что вывело curl, что в Recv-Q и Send-Q слушателя — и какой вывод из этого следует о фразе «раз тайм-аут при подключении, значит файрвол». Ничего в системе при этом не менялось: файрвол не участвует.

Источники

  • Руководство curl 8.5.0 (curl --manual): код выхода 7 — «Failed to connect to host», 28 — «Operation timeout».
  • ss(8): -l, -t, -n, -p — man7.org; значения Recv-Q и Send-Q у слушающего сокета в этой странице не определены — утверждение «Recv-Q слушателя — число соединений в очереди, ждущих accept» взято из наблюдения при запуске (лаборатория курса), а не из документации.
  • listen(2): параметр backlog — максимальная длина очереди ожидающих соединений; при переполнении клиент может получить ECONNREFUSED или запрос может быть проигнорирован и повторён — man7.org. Какой из двух исходов получится у вас, зависит от настроек ядра (в окружении автора net.ipv4.tcp_abort_on_overflow = 0: клиент видит тайм-аут).

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.