netstat, ss, lsof — смотрим текущие соединения и открытые порты
Когда «порт занят» или «сервис не отвечает на 8080», вам нужен инструмент, который показывает текущее состояние сетевого стека: какие сокеты открыты, какие процессы их слушают, какие соединения установлены. В Linux/macOS таких инструментов три: netstat (классика, deprecated на Linux), ss (современная замена), lsof (универсальный list open files).
В этом уроке разберём, как ими пользоваться, какой когда, и как читать их output. Это базовый инструментарий sysadmin/DevOps.
netstat — классика, но deprecated
netstat — из эпохи UNIX 80-х. До сих пор повсеместен (macOS, Windows, BSD), но в современном Linux считается deprecated — его заменили на ss (из пакета iproute2). На большинстве Linux-дистрибутивов 2020-х netstat не предустановлен, нужно ставить отдельно.
Базовое использование:
# Все TCP-соединения и слушающие порты:
netstat -an
# tcp4 0 0 127.0.0.1.5432 *.* LISTEN
# tcp4 0 0 10.0.0.42.443 203.0.113.5.45678 ESTABLISHED
# tcp4 0 0 10.0.0.42.443 198.51.100.8.32145 ESTABLISHED
# udp4 0 0 *.53 *.*
# Только TCP (без UDP):
netstat -an -p tcp
# С процессами (Linux):
sudo netstat -anp
# С процессами (macOS):
sudo netstat -anv # macOS немного другие флаги
Поля:
- Proto — tcp4 / tcp6 / udp4 / udp6.
- Recv-Q / Send-Q — размер receive и send очередей. >0 в Recv-Q может означать, что процесс не успевает читать.
- Local Address — IP:port на нашей стороне.
- Foreign Address — IP:port удалённой стороны.
- State — состояние TCP (LISTEN, ESTABLISHED, TIME_WAIT, CLOSE_WAIT, …).
# Только слушающие порты:
netstat -anlt -p tcp # Linux
netstat -anlv | grep LISTEN # macOS
# Найти, кто слушает порт 8080:
netstat -anv | grep ':8080.*LISTEN'
# Открытые UDP-сокеты:
netstat -an -p udp
ss — современный, быстрый
ss (Socket Statistics) — стандарт в Linux с 2010-х. Получает информацию напрямую из ядра через netlink, поэтому быстрее netstat (особенно при тысячах соединений).
Базовое:
# Все слушающие TCP:
ss -tln
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# LISTEN 0 128 127.0.0.1:5432 0.0.0.0:*
# LISTEN 0 128 0.0.0.0:80 0.0.0.0:*
# LISTEN 0 128 [::]:443 [::]:*
# Все TCP-соединения (включая ESTABLISHED):
ss -tn
# С процессами (нужен sudo):
sudo ss -tlnp
# Только UDP:
ss -uln
# IPv4 / IPv6:
ss -4tln # только IPv4
ss -6tln # только IPv6
# Подробная статистика по каждому соединению:
ss -tni
Флаги:
-t— TCP-u— UDP-l— listening only-n— numeric (не резолвить имена)-p— show processes-i— internal info (RTT, retransmits, cwnd)-a— all (listening + established)-4 / -6— IPv4 / IPv6
Особенно полезный -i — показывает TCP internal state:
ss -tni
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 10.0.0.42:443 203.0.113.5:45678
cubic wscale:7,7 rto:204 rtt:1.234/0.678 ato:40 mss:1448
cwnd:10 ssthresh:7 bytes_sent:12345 bytes_acked:12345 segs_out:18 segs_in:20
data_segs_out:5 send 93.7Mbps lastsnd:12 lastrcv:14 pacing_rate 187.4Mbps
delivery_rate 47.8Mbps app_limited rcvmss:536 advmss:1448 rcvspace:14600
Это глубокая TCP-диагностика:
- rtt: 1.234/0.678 — средний RTT и его jitter. Полезно для понимания качества соединения.
- cwnd: 10 — congestion window. Маленький cwnd = TCP считает сеть перегруженной.
- retrans: N — сколько retransmits. Большой — потеря пакетов.
- bytes_sent / bytes_acked — если разница большая, есть unacked данные.
Состояния TCP — что они значат
При просмотре соединений вы видите разные states. Каждое имеет конкретный смысл:
| State | Значение |
|---|---|
| LISTEN | Слушает входящие соединения |
| SYN_SENT | Послали SYN, ждём SYN-ACK (клиент в начале handshake) |
| SYN_RECV | Получили SYN, послали SYN-ACK, ждём ACK (сервер) |
| ESTABLISHED | Handshake завершён, передача данных |
| FIN_WAIT_1 | Послали FIN, ждём подтверждения |
| FIN_WAIT_2 | Получили ACK на наш FIN, ждём FIN от другой стороны |
| TIME_WAIT | Соединение закрыто, ждём кратное 2*MSL (~60 сек) для предотвращения duplicate packets |
| CLOSE_WAIT | Получили FIN от другой стороны, ждём app закрыть свою сторону |
| LAST_ACK | Послали FIN после CLOSE_WAIT, ждём ACK |
| CLOSING | Обе стороны послали FIN одновременно |
| CLOSED | Соединение полностью закрыто |
Самые «подозрительные» состояния в production:
- Много TIME_WAIT. Нормально для активного сервера, но если >10k — может быть проблема (исчерпание local ports). Решение:
net.ipv4.tcp_tw_reuse=1. - Много CLOSE_WAIT. Означает: удалённая сторона послала FIN, но наш app не закрыл свою сторону. Это утечка соединений в коде — забыли
close()илиConnection.close(). - Много SYN_SENT висящих. Не доходим до сервера, скорее всего firewall.
- Много SYN_RECV висящих. Возможно, под SYN flood (см. урок про DDoS).
# Сколько соединений в каждом состоянии:
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 234 ESTABLISHED
# 89 TIME_WAIT
# 12 CLOSE_WAIT
# 5 LISTEN
# 1 SYN_SENT
lsof — list open files (и сокеты)
В UNIX «всё файл», включая сетевые сокеты. lsof показывает, какие файлы открыты каждым процессом. С нужными флагами это эквивалентно netstat + ps.
# Все открытые TCP/UDP-соединения:
sudo lsof -i
# Только TCP:
sudo lsof -iTCP
# Только UDP:
sudo lsof -iUDP
# Только LISTEN (слушающие порты):
sudo lsof -iTCP -sTCP:LISTEN
# Кто слушает конкретный порт:
sudo lsof -iTCP:8080
# Кто слушает 8080 на состояние LISTEN:
sudo lsof -iTCP:8080 -sTCP:LISTEN
# Все сетевые соединения процесса по PID:
sudo lsof -i -p 12345
# Все сетевые соединения по имени процесса:
sudo lsof -i -c nginx
# Без номеров (резолвить имена):
sudo lsof -i
# С номерами:
sudo lsof -i -n -P
Пример output:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1234 root 6u IPv4 0x123456 0t0 TCP *:80 (LISTEN)
nginx 1234 root 7u IPv4 0x123457 0t0 TCP *:443 (LISTEN)
python3 5678 www 12u IPv4 0x123458 0t0 TCP 127.0.0.1:8000 (LISTEN)
postgres 9012 postg 4u IPv4 0x123459 0t0 TCP 127.0.0.1:5432 (LISTEN)
Главная фишка lsof — видно PROCESS+PID на каждом сокете. Это сразу отвечает на вопрос «кто занимает порт».
Кто слушает порт — стандартные сценарии
«Сервер не запускается — Address already in use»
python3 -m http.server 8000
# OSError: [Errno 48] Address already in use
Кто занял порт?
# Linux ss:
sudo ss -tlnp | grep ':8000'
# LISTEN 0 5 0.0.0.0:8000 0.0.0.0:* users:(("python3",pid=12345,fd=3))
# Linux netstat:
sudo netstat -anp | grep ':8000.*LISTEN'
# macOS lsof:
sudo lsof -iTCP:8000 -sTCP:LISTEN
# python3 12345 user 3u IPv4 ... TCP *:8000 (LISTEN)
# Универсальный:
sudo lsof -i:8000
Видим: процесс python3 с PID 12345 уже слушает. Если хотим освободить порт:
sudo kill 12345
# или принудительно:
sudo kill -9 12345
«На каком порту мой сервер?»
# Запустили сервер с random port:
python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
server = HTTPServer(('', 0), BaseHTTPRequestHandler) # 0 = random free port
print(f'Listening on {server.server_port}')
server.serve_forever()
"
# Listening on 49823
# Подтвердить через ss:
ss -tlnp | grep python
«Какие соединения активны?»
# Все ESTABLISHED:
ss -tn state established
# По конкретному порту:
ss -tn '( sport = :80 or dport = :80 )' state established
# С процессом:
sudo ss -tnp state established
Performance debugging через ss
ss -tni показывает TCP internal state — это бесценно для performance debug:
sudo ss -tni
ESTAB 0 0 10.0.0.42:443 203.0.113.5:45678
cubic wscale:7,7 rto:204 rtt:1.234/0.678 ato:40 mss:1448
cwnd:10 ssthresh:7 retrans:3
bytes_sent:12345678 bytes_acked:12345600 segs_out:850 segs_in:870
data_segs_out:850 data_segs_in:1
send 12.5Mbps lastsnd:12 lastrcv:14 pacing_rate 25.0Mbps
delivery_rate 8.4Mbps
Ключевые метрики:
- rtt: avg/mdev — средний RTT и его медианное отклонение. Большой rtt — сеть тормозит.
- cwnd — congestion window. Маленький при наличии данных = TCP замедлен из-за потерь.
- retrans: N — ретрансмиты. Большой = packet loss.
- send rate — текущая скорость. Если меньше ожидаемой — bottleneck.
Пример: ваш API клиент жалуется на медленные ответы. Запросите ss:
sudo ss -tnpi state established
ESTAB 0 0 10.0.0.42:443 203.0.113.5:45678 users:(("nginx",pid=1234,fd=10))
cubic wscale:7,7 rto:1000 rtt:850.234/200.678 mss:1448
cwnd:2 ssthresh:2 retrans:24
RTT 850ms — очень медленно для local DC. retrans:24 — много ретрансмитов. cwnd:2 — TCP сжался почти до минимума. Скорее всего проблема в качестве сети до клиента (Wi-Fi, плохой ISP).
Сравнение netstat / ss / lsof
| Свойство | netstat | ss | lsof |
|---|---|---|---|
| Стандарт сегодня | Deprecated (Linux) | Yes (Linux) | Yes (cross-platform) |
| Скорость | Медленный (читает /proc) | Быстрый (netlink) | Средний |
| Process info | -p (Linux) или -v (macOS) | -p | По умолчанию |
| Internal TCP info | Нет | -i (rtt, cwnd, retrans) | Нет |
| Cross-platform | Да (Linux, macOS, Windows) | Только Linux | Yes |
| Filter syntax | Простой grep | Богатый ('( sport = :80 )') | Простой |
| Network namespaces | Нет | Да (-N) | Нет |
Когда что использовать:
- Linux production — ss. Современный, быстрый, мощный.
- macOS — lsof или netstat. ss не входит в macOS.
- Кто слушает порт быстро — lsof. Лаконично.
- TCP performance debug — ss -i. Internal state — только в ss.
Network namespaces (Docker, K8s)
В контейнерах ваши обычные netstat/ss/lsof показывают только хостовую namespace, не контейнерную. Чтобы заглянуть в контейнер:
# Список network namespaces:
ip netns list
# Войти в namespace конкретного контейнера и запустить ss:
sudo nsenter -t $(docker inspect -f '{{.State.Pid}}' my-container) -n ss -tlnp
# Или через docker exec:
docker exec my-container ss -tln
# (если в контейнере есть ss -- alpine может не иметь)
В Kubernetes:
# В пое:
kubectl exec my-pod -- ss -tln
# Или через kubectl debug (если включено):
kubectl debug -it my-pod --image=busybox -- netstat -an
Полезные one-liners
# Сколько соединений с конкретного IP:
ss -tn | awk -v ip="203.0.113.5" '$5 ~ ip {n++} END {print n}'
# Топ-10 IP по числу соединений:
ss -tn | awk 'NR>1 {split($5, a, ":"); print a[1]}' | sort | uniq -c | sort -rn | head
# Распределение по состояниям:
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# Соединения с конкретного порта (например, к нашему API):
ss -tnp dport = :443
# Watch соединений в реальном времени:
watch -n 1 'ss -tn state established | wc -l' # счётчик ESTABLISHED
# Найти процессы с большим числом сокетов:
sudo lsof -i | awk '{print $1}' | sort | uniq -c | sort -rn | head
TIME_WAIT и почему его много
TIME_WAIT — это normal state, в котором соединение «висит» 60 секунд (2*MSL = Maximum Segment Lifetime) после закрытия. Это нужно, чтобы поздние duplicate packets не повлияли на новое соединение с тем же 4-tuple.
В production-сервере с большим RPS это нормально иметь много TIME_WAIT:
ss -tan | grep TIME-WAIT | wc -l
# 5230
Но если очень много — может исчерпать local source ports (limit ~30000) и новые исходящие соединения начнут failing с «port in use».
Решения:
# Включить TCP TIME_WAIT reuse (безопасно):
sysctl -w net.ipv4.tcp_tw_reuse=1
# Увеличить диапазон local ports:
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# Connection pooling в приложении -- лучшее решение, чем тюнить kernel:
# requests.Session() в Python, sql.DB connection pool в Go, etc
‘tcp_tw_recycle’ — старая опция, удалена в Linux 4.12 из-за проблем с NAT. Не используйте ‘tcp_tw_recycle’, даже если найдёте в старых tutorials.
Попробуй сам
Серия упражнений для практики:
# 1. Что слушает на вашей машине прямо сейчас:
sudo lsof -iTCP -sTCP:LISTEN -n -P
# или Linux:
sudo ss -tlnp
# 2. Запустите простой сервер на 8000:
python3 -m http.server 8000 &
SERVER_PID=$!
# 3. Подтвердите, что слушает:
sudo ss -tlnp | grep 8000
sudo lsof -iTCP:8000
# 4. Сделайте к нему запросы и посмотрите ESTABLISHED:
curl http://localhost:8000/ &
ss -tn state established | grep 8000
# 5. Остановите сервер:
kill $SERVER_PID
# 6. Через несколько секунд TIME_WAIT:
ss -tan | grep 8000
# 7. Подождите ~60 секунд -- TIME_WAIT должен исчезнуть:
sleep 65
ss -tan | grep 8000 # больше нет
Анализ существующих соединений:
# Сколько соединений сейчас на вашем сервере к разным удалённым серверам:
ss -tn state established | awk 'NR>1 {split($5,a,":"); print a[1]}' | sort | uniq -c | sort -rn
# Какие процессы держат больше всего сокетов:
sudo lsof -i 2>/dev/null | awk '{print $1}' | sort | uniq -c | sort -rn | head
# Распределение по TCP-state:
ss -tan | awk 'NR>1 {state[$1]++} END {for(s in state) print state[s], s}' | sort -rn
# Activity на конкретный port:
sudo ss -tnp dport = :443 state established | head
ss, netstat и ip: полный арсенал диагностики соединений на Linux