Сеть: ip, curl, DNS и диагностика
Сервер не отвечает. Что делать? Звонить девопсу? Нет - сначала проверить сеть. 80% проблем на проде - это сеть, DNS или файрвол.
Проверка сетевых интерфейсов
ip addr # все интерфейсы и IP-адреса
ip addr show eth0 # конкретный интерфейс
hostname -I # только IP-адреса (коротко)
ss -tlnp: читаем вывод по колонкам
ss - первая команда при любом «не подключается». Флаги расшифровываются так: -t только TCP, -l только слушающие сокеты, -n не превращать номера портов в имена, -p показать процесс.
sudo ss -tlnp
# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("backend",pid=1234,fd=3))
# LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=567,fd=6))
# LISTEN 0 244 [::1]:5432 [::]:* users:(("postgres",pid=890,fd=5))
Ключевая колонка - Local Address, и именно её читают неправильно:
| Адрес | Кто может подключиться |
|---|---|
0.0.0.0:80 | кто угодно, со всех интерфейсов |
127.0.0.1:8080 | только процессы на этой же машине |
[::]:80 | все интерфейсы IPv6 (обычно плюс IPv4) |
10.0.0.5:5432 | только через конкретный интерфейс, в этой подсети |
Recv-Q у слушающего сокета - это число соединений, принятых ядром, но ещё не забранных приложением. Ненулевое значение, которое не уменьшается, означает: приложение не успевает делать accept, очередь копится, клиенты ждут. Send-Q у слушающего сокета показывает размер backlog (лимит очереди). Без sudo колонка Process будет пустой - это не баг, а права.
ss -tan state established # активные соединения, а не только слушающие
ss -tanp | grep :5432 # кто держит коннекты к базе
ss -s # сводка: сколько сокетов какого типа
Слушает 127.0.0.1 - снаружи не доступно
Самая частая причина «локально работает, с моей машины нет». Приложение привязалось к loopback-интерфейсу, и никакой файрвол тут не виноват:
sudo ss -tlnp | grep 8080
# LISTEN 0 4096 127.0.0.1:8080 ... <- вот причина
curl http://localhost:8080/health # с сервера отвечает
curl http://SERVER_IP:8080/health # снаружи: Connection refused
В коде это буквально одна строка. localhost:8080 слушает только loopback, пустой хост - все интерфейсы:
http.ListenAndServe("localhost:8080", mux) // только с этой машины
http.ListenAndServe(":8080", mux) // со всех интерфейсов
http.ListenAndServe("0.0.0.0:8080", mux) // то же самое, явно
<?php
declare(strict_types=1);
// Встроенный сервер: адрес привязки задаётся аргументом -S
// php -S 127.0.0.1:8080 - только с этой машины
// php -S 0.0.0.0:8080 - со всех интерфейсов
// В PHP-FPM то же решение принимает директива listen в пуле:
// listen = 127.0.0.1:9000 или listen = 0.0.0.0:9000
$onlyLocal = stream_socket_server('tcp://127.0.0.1:8080', $errno, $errstr);
$everyone = stream_socket_server('tcp://0.0.0.0:8080', $errno, $errstr);
Обратная сторона: слушать 0.0.0.0 базой данных на публичном VPS - плохая идея. Правильная схема для PostgreSQL и Redis - 127.0.0.1 плюс подключение через приватную сеть или SSH-туннель:
ssh -L 5432:127.0.0.1:5432 user@server # порт сервера появится локально
curl - главный инструмент для HTTP
curl http://localhost:8080/health # GET запрос
curl -v http://localhost:8080/health # подробно (заголовки, TLS)
curl -X POST -H "Content-Type: application/json" \
-d '{"email":"test@test.com"}' \
http://localhost:8080/api/v1/users # POST с JSON
curl -o file.tar.gz https://example.com/file.tar.gz # скачать файл
curl -w "%{http_code}" -s -o /dev/null http://localhost:8080/health # только код ответа
Флаги, которые нужны постоянно:
| Флаг | Значение |
|---|---|
-v | verbose (показать заголовки) |
-s | silent (без прогресс-бара) |
-o | output в файл |
-H | добавить заголовок |
-d | тело запроса |
-X | метод (POST, PUT, DELETE) |
-k | игнорировать ошибки SSL |
Читаем curl -v по фазам
curl -v показывает четыре разные фазы, и падать может любая. Понимать, на какой именно оборвалось, важнее, чем видеть итоговую ошибку:
* Host api.example.com:443 was resolved. <- 1. DNS
* IPv4: 203.0.113.10
* Trying 203.0.113.10:443...
* Connected to api.example.com (203.0.113.10) port 443 <- 2. TCP
* ALPN: server accepted h2
* SSL certificate verify ok. <- 3. TLS
> GET /health HTTP/2 <- 4. HTTP-запрос (мы пишем)
> host: api.example.com
>
< HTTP/2 200 <- ответ (сервер пишет)
< content-type: application/json
Правило чтения: * - это комментарии самого curl о транспорте, > - что мы отправили, < - что пришло. Значит:
- оборвалось до
Trying- виноват DNS; - дошло до
Tryingи повисло - файрвол дропает пакеты (не отвергает, а молчит); Connection refusedсразу - порт закрыт, приложение не слушает;- упало на
SSL certificate problem- сертификат просрочен, самоподписан или не совпало имя; - дошло до
>и висит - принимающее приложение зависло на обработке, транспорт в порядке.
Чтобы получить числа вместо ощущений, у curl есть тайминги по фазам:
curl -s -o /dev/null -w "dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
https://api.example.com/health
# dns=0.004 tcp=0.041 tls=0.128 ttfb=0.512 total=0.513
Разница ttfb - tls - это время, которое сервер думал над ответом. Если она большая, а остальное быстрое, проблема в приложении или базе, а не в сети. Обратная картина (большой dns) - симптом медленного или недоступного резолвера.
Полезные флаги для диагностики, которых нет в базовой таблице:
curl -sf http://localhost:8080/health # -f: ненулевой exit code на 4xx/5xx, нужен в скриптах
curl -I https://example.com # только заголовки (HEAD)
curl -L http://example.com # следовать редиректам
curl --resolve api.example.com:443:127.0.0.1 https://api.example.com/health # проверить свой сервер до смены DNS
curl --max-time 5 --connect-timeout 2 ... # не висеть вечно в скриптах
Флаг --resolve стоит запомнить отдельно: он подменяет DNS только для этого запроса, но оставляет правильный заголовок Host и SNI. Так проверяют новый сервер, ещё не переключив домен.
DNS - проверка доменов
dig example.com # DNS запрос (подробно)
dig +short example.com # только IP
nslookup example.com # альтернатива
host example.com # ещё альтернатива
cat /etc/resolv.conf # какие DNS-серверы используются
dig показывает одно, приложение видит другое
Ловушка, на которую попадаются все: dig не учитывает /etc/hosts. Он отправляет запрос прямо DNS-серверу, минуя системный резолвер. А приложение (и curl, и Go-сервис) идёт через resolver, который сначала смотрит в файл. Отсюда классика: dig +short api.internal возвращает продовый IP, а сервис стучится на localhost, потому что кто-то полгода назад оставил строку в /etc/hosts.
cat /etc/hosts # локальные переопределения имён
getent hosts api.internal # разрешить имя так, как это сделает приложение
dig +short api.internal # разрешить имя в обход /etc/hosts
cat /etc/nsswitch.conf | grep hosts
# hosts: files dns <- files (то есть /etc/hosts) идёт ПЕРВЫМ
Правило диагностики: расхождение между getent hosts и dig +short почти всегда означает запись в /etc/hosts. Проверять всегда обеими командами.
Тот же файл - легальный инструмент. Локальная разработка с «настоящими» доменами делается строкой 127.0.0.1 api.local, а проверка нового сервера до переключения DNS - через curl --resolve из раздела выше (это чище, чем правка /etc/hosts, потому что не нужно потом откатывать).
Кеш DNS живёт как минимум в двух местах, и оба сбрасываются вручную:
resolvectl statistics # статистика кеша systemd-resolved
sudo resolvectl flush-caches # сбросить кеш systemd-resolved
resolvectl query api.example.com # что вернёт резолвер с учётом кеша и /etc/hosts
Проверка доступности
ping example.com # ICMP ping (Ctrl+C для остановки)
ping -c 4 example.com # 4 пакета и стоп
# Проверка TCP-порта (без ping)
nc -zv example.com 443 # можно ли подключиться к порту 443
nc -zv localhost 5432 # доступен ли PostgreSQL
# Или через curl
curl -v telnet://localhost:5432 # проверка TCP-соединения
nc -z отвечает не на тот вопрос
nc -z открывает TCP-соединение и сразу закрывает. Он отвечает ровно на один вопрос: «принял ли кто-то соединение на этом порту». И этого мало.
| Проверка | Что подтверждает | Чего не заметит |
|---|---|---|
ping | хост отвечает на ICMP | что приложение вообще запущено; ICMP часто заблокирован отдельно |
nc -z host 8080 | порт принимает соединения | зависшее приложение, ошибки 500, просроченный TLS, неверный роутинг |
curl -sf host/health | приложение отвечает 2xx | глубину проверки: живы ли база и брокер |
curl -sf host/health с проверкой тела | сервис проверил свои зависимости | ничего важного, это верхний уровень |
Практическое следствие: nc -z годится для быстрой проверки в bash-скрипте, но health-check сервиса всегда делают HTTP-запросом с -f. Причина в том, что accept выполняет ядро: приложение может висеть в дедлоке или ждать базу, а соединения всё равно принимаются - nc -z покажет «всё хорошо» на мёртвом сервисе.
Три ошибки подключения и что каждая означает
Connection refused порт никто не слушает: процесс упал, слушает другой адрес
или ты стучишься не туда. Пакет дошёл, ответ RST пришёл.
Connection timed out ответа нет вообще: файрвол дропает пакеты молча,
security group закрыт, маршрут ведёт в никуда.
No route to host маршрутизации до адреса нет: неверная подсеть,
упал шлюз, VPN отключился.
Разница между первой и второй строкой - главная развилка диагностики. refused означает, что сеть работает и дело в приложении (или адресе привязки, см. раздел про 127.0.0.1). timed out означает, что дело в фильтрации трафика, и лезть в логи приложения бессмысленно.
MTU: «пинг идёт, а запрос не проходит»
Отдельный класс инцидентов, который выглядит как магия: ping работает, curl подключается и печатает заголовки запроса, а дальше висит. Или SSH заходит, но обрывается, как только выводит много текста.
Причина - MTU, максимальный размер пакета на пути. Мелкие пакеты (пинг, запрос) проходят, крупные (ответ с телом, вывод команды) - нет, потому что где-то по маршруту MTU меньше, а сообщение «пакет слишком большой» блокируется файрволом. Классические места: VPN, туннели, сети внутри Docker и оверлеи, мобильные операторы.
ip link show eth0 | grep mtu # какой MTU у интерфейса (обычно 1500)
ping -M do -s 1472 example.com # 1472 + 28 байт заголовков = 1500
# Frag needed and DF set (mtu = 1400) <- значит реальный предел 1400
tracepath example.com # покажет MTU по шагам маршрута
Проверка простая: если ping -c 3 без флагов проходит, а ping -M do -s 1472 возвращает Frag needed, у тебя проблема с MTU, а не с приложением. Лечение на стороне сервера - понизить MTU интерфейса или включить MSS clamping на шлюзе; в одиночку бэкендер это чинит редко, но правильно назвать причину сетевикам - половина решения.
Диагностика: маршрут до сервера
traceroute example.com # через какие узлы идёт трафик
mtr example.com # traceroute + ping в реальном времени
Файрвол
sudo ufw status # статус файрвола (Ubuntu)
sudo ufw allow 8080/tcp # открыть порт
sudo ufw deny 3306/tcp # закрыть порт
sudo iptables -L -n # правила iptables (низкоуровнево)
Скачивание файлов
wget https://example.com/file.tar.gz # скачать файл
wget -q -O - https://example.com/script.sh # вывести в stdout
curl -fsSL https://example.com/install.sh # аналог через curl
Чек-лист диагностики «сервер не отвечает»
ping server- сервер вообще жив?ss -tlnp | grep 8080- приложение слушает порт?curl -v http://localhost:8080/health- отвечает локально?sudo ufw status- файрвол не блокирует?dig +short yourdomain.com- DNS указывает на правильный IP?curl -v https://yourdomain.com/health- отвечает снаружи?
Мини-задание
- Узнай свой IP:
hostname -Iилиip addr - Проверь DNS:
dig +short google.com - Сделай HTTP-запрос:
curl -v http://httpbin.org/get - Проверь порт:
nc -zv localhost 22 - Выполни
sudo ss -tlnpи найди сервис, который слушает только127.0.0.1 - Замерь тайминги:
curl -s -o /dev/null -w "dns=%{time_namelookup} ttfb=%{time_starttransfer}\n" https://example.com - Сравни
getent hosts localhostиdig +short localhost- объясни разницу - Проверь MTU своего интерфейса:
ip link show | grep mtu - Постучись на закрытый порт (
curl -v http://localhost:9999) и на дропающий адрес - сравниrefusedиtimed out
Итог
curl- основной инструмент HTTP-диагностики. Выучи флаги-v,-s,-H,-d.ss -tlnp- порты.dig- DNS.ping/nc- доступность.- Чек-лист из 6 шагов покрывает 90% ситуаций «почему не работает».
Типичная ошибка
Паниковать и перезагружать сервер, когда приложение не отвечает. Перезагрузка - это не диагностика. Сначала пройди чек-лист, найди причину, потом действуй.
Мини-практика
Подними любой HTTP-сервер локально (например, встроенный сервер PHP - php -S localhost:9090). Проверь через curl -v http://localhost:9090. Посмотри заголовки ответа. Потом останови сервер и повтори curl - увидишь Connection refused. Запомни, как выглядит разница между «сервис не отвечает» и «сервис не запущен».