Порт и процесс
Чтение и практика ~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),
порядок выбираете сами.
До любого наблюдения, кроме повтора симптома, запишите:
- Не меньше двух гипотез, каждая из которых объясняет этот симптом. Одна — слова Петра; вторая — ваша.
- Для каждой — что вы увидели бы в наблюдении, которое её подтверждает, и в том, которое её исключает.
- Одно наблюдение, которое различает ваши гипотезы. Выполните его и запишите вывод — одна-две строки — и то, какие гипотезы он исключил.
Затем одним предложением назовите причину в форме «клиент идёт на …, а слушает …» и приложите две строки вывода: одну со стороны клиента, одну со стороны сервера.
Разбор мини-задачи
Откройте, когда записали свои гипотезы и наблюдение
Верных путей два; оба приводят к одной паре строк.
Путь с клиента. 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 слушателя — и какой вывод
из этого следует о фразе «раз тайм-аут при подключении, значит файрвол». Ничего в системе при этом не менялось: файрвол
не участвует.
Источники
- Руководство
curl8.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: клиент видит тайм-аут).
Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.