Devin.KR
로그인

리눅스 tcpdump 사용법 - 패킷 캡처 필터 문법과 실전 트러블슈팅

개발자 조회 1

이 명령어를 언제 쓰는가

진단의 마지막 단계다. DNS는 정상이고(dig), 경로도 살아 있고(traceroute), 서버는 포트를 열고 있는데(ss -tlnp), 클라이언트에서는 여전히 안 붙는다. 이제 남은 질문은 하나다. 패킷이 실제로 도착하고 있는가.

tcpdump는 그 답을 준다. SYN이 서버에 들어오는지, 서버가 SYN-ACK을 보내는데 그게 돌아가지 못하는지, 아니면 RST가 즉시 오는지에 따라 원인이 완전히 달라진다.

  • SYN이 아예 안 보인다 → 중간 방화벽이나 보안그룹이 버렸다. 서버 문제가 아니다.
  • SYN은 오는데 응답이 없다 → 서버의 로컬 방화벽(iptables/nftables/firewalld)이 DROP했거나, 그 포트에 리스닝이 없다.
  • SYN에 RST가 즉시 온다 → 포트가 닫혀 있거나 방화벽이 REJECT로 응답한 것이다.
  • SYN-ACK까지 갔는데 클라이언트가 못 받는다 → 비대칭 라우팅이나 돌아오는 경로의 필터링이다.

이 네 가지를 구분하는 것만으로 대부분의 "접속이 안 된다" 티켓이 정리된다. Ubuntu/Debian·RHEL 모두 tcpdump 패키지로 설치하고, 실행에는 root 권한이 필요하다.

기본 형식

sudo tcpdump [옵션] [필터식]
  • 옵션 — 어디서(-i), 어떻게 보여줄지(-n, -A, -v), 어디에 저장할지(-w).
  • 필터식 — BPF 문법. 이걸 제대로 쓰는 것이 tcpdump의 전부다. 필터 없이 돌리면 초당 수천 줄이 흘러가 아무것도 못 읽는다.

필터 문법

필터는 무엇을(type) + 어느 방향(dir) + 어떤 프로토콜(proto) 세 축의 조합이다.

키워드의미예시
host특정 IP가 출발지 또는 목적지host 192.168.0.22
net대역 단위net 10.0.0.0/8
port특정 포트port 3306
portrange포트 범위portrange 8000-8100
src / dst방향 한정src host 10.0.0.5
tcp udp icmp프로토콜 한정udp port 53
and or not논리 결합(&&, ||, !도 됨)host 10.0.0.5 and port 443
tcp[tcpflags]TCP 플래그 비트 검사tcp[tcpflags] & tcp-syn != 0

괄호와 &, !는 셸이 먼저 해석하므로 필터식 전체를 작은따옴표로 감싸는 습관을 들인다.

자주 쓰는 옵션

옵션의미예시
-i캡처할 인터페이스. any는 전체-i eth0, -i any
-nIP를 이름으로 바꾸지 않는다-n
-nn포트 번호도 서비스명으로 안 바꾼다. 사실상 필수-nn
-c지정 개수만 잡고 종료-c 50
-wpcap 파일로 저장. 화면에는 안 나온다-w /tmp/cap.pcap
-r저장된 pcap 파일을 읽어 분석-r /tmp/cap.pcap
-A페이로드를 ASCII로 출력(평문 프로토콜용)-A
-X16진수와 ASCII 병기-X
-e이더넷 헤더(MAC 주소)까지 표시-e
-v -vvTTL, IP ID, 체크섬 등 상세 표시-vv
-tttt날짜까지 포함한 절대 시각-tttt
-s패킷당 캡처 바이트(snaplen). -s 0은 전체-s 0
-C / -W파일 크기(MB) 분할 / 보관 개수. 링버퍼-C 100 -W 10
-l줄 단위 버퍼링. 파이프로 넘길 때 필요-l | grep GET

실전 예제

1. 클라이언트의 접속 시도가 서버에 도달하는지 본다. 가장 중요한 첫 확인이다. 서버에서 이걸 띄워두고 클라이언트에서 접속을 시도한다.

sudo tcpdump -i any -nn 'host 203.0.113.50 and port 8080'
22:30:01.123456 IP 203.0.113.50.51234 > 10.0.0.10.8080: Flags [S], seq 1234567890, win 64240, length 0
22:30:01.123512 IP 10.0.0.10.8080 > 203.0.113.50.51234: Flags [S.], seq 987654321, ack 1234567891, win 65160, length 0
22:30:01.131200 IP 203.0.113.50.51234 > 10.0.0.10.8080: Flags [.], ack 1, win 502, length 0

플래그를 읽는 법이다. [S]는 SYN, [S.]는 SYN-ACK, [.]은 ACK, [P.]는 데이터 포함(PSH-ACK), [F.]는 정상 종료(FIN-ACK), [R]은 강제 끊김(RST)이다. 위처럼 세 줄이 다 나오면 3-way 핸드셰이크 성공이고, 네트워크는 무죄다. 첫 줄만 반복되면 서버가 응답을 못 하는 것이다.

2. 새로 들어오는 연결 시도만 골라 본다. 기존 트래픽이 많은 서버에서 SYN만 걸러 보면 화면이 조용해진다.

sudo tcpdump -i any -nn 'tcp[tcpflags] & (tcp-syn|tcp-rst) != 0 and port 3306'

SYN과 RST만 남으므로 "누가 붙으려 했고 누가 거절당했는지"가 바로 보인다.

3. 파일로 저장해 나중에 분석한다. 간헐적으로만 재현되는 문제에는 화면을 지켜보는 것이 답이 아니다. 파일로 받아 Wireshark로 연다.

sudo tcpdump -i eth0 -nn -s 0 -w /var/tmp/issue.pcap 'host 10.0.0.22 and port 5432'

재현 후 Ctrl+C로 멈추고, 서버에서 바로 훑어보려면 이렇게 읽는다.

tcpdump -nn -r /var/tmp/issue.pcap | head -50
tcpdump -nn -r /var/tmp/issue.pcap 'tcp[tcpflags] & tcp-rst != 0'

필터는 저장할 때와 읽을 때 각각 걸 수 있다. 저장은 넉넉하게 하고, 읽을 때 좁히는 편이 안전하다. 저장 시점에 너무 좁히면 정작 필요한 패킷을 놓친다.

4. 며칠짜리 간헐 장애를 링버퍼로 잡는다. 디스크를 채우지 않으면서 계속 돌린다. 100MB 파일 10개를 돌려쓰므로 최대 1GB에서 멈춘다.

sudo tcpdump -i eth0 -nn -s 0 -C 100 -W 10 -w /var/tmp/ring.pcap 'port 443'

장시간 백그라운드로 돌릴 때는 nohup과 함께 쓰고, 반드시 종료 계획을 세워둔다.

5. 평문 HTTP 요청 내용을 본다. 리버스 프록시가 헤더를 제대로 넘기는지 확인할 때 유용하다.

sudo tcpdump -i any -nn -A -s 0 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

뒤의 긴 조건은 페이로드가 있는 패킷만 고르는 관용구다. 빈 ACK을 걸러내 화면이 훨씬 읽기 쉬워진다. 443 포트에는 의미가 없다. TLS로 암호화돼 있어 -A로 봐도 알아볼 수 없다.

6. DNS 질의가 실제로 나가는지 확인한다. 애플리케이션의 이름 해석이 느릴 때.

sudo tcpdump -i any -nn 'udp port 53'

질의는 나가는데 응답이 없으면 DNS 서버나 방화벽 문제이고, 질의 자체가 안 나가면 캐시에서 처리되고 있거나 애플리케이션 쪽 문제다.

7. 같은 IP가 두 개 보이는지 확인한다. IP 충돌이 의심될 때 ARP를 본다.

sudo tcpdump -i eth0 -nn -e arp

-e로 MAC을 함께 보면, 하나의 IP를 서로 다른 MAC이 주장하는 상황이 드러난다.

함정과 주의점

SSH로 접속한 서버에서 필터 없이 tcpdump를 돌리면 폭주한다. 화면에 출력하는 행위 자체가 SSH 트래픽을 만들고, 그 트래픽이 다시 캡처돼 출력되고, 그게 또 트래픽을 만든다. 화면이 미친 듯이 흐르고 세션이 사실상 멈춘다. 반드시 자기 SSH 세션을 제외한다.

sudo tcpdump -i any -nn 'not port 22'

더 안전한 방법은 필요한 대상만 명시하는 것이다. not으로 빼기보다 host X and port Y로 좁혀 들어가는 습관이 낫다.

-n을 빼면 캡처가 스스로를 오염시킨다. tcpdump가 IP를 이름으로 바꾸려고 DNS 질의를 보내는데, 그 질의가 다시 캡처 대상이 된다. 게다가 DNS가 느리면 출력이 몇 초씩 멈춘다. -nn은 조건 반사로 붙인다.

잘못된 인터페이스를 보고 있을 수 있다. 도커나 쿠버네티스 환경에서는 트래픽이 eth0이 아니라 브리지(docker0)나 veth를 지난다. VPN이면 tun0, 본딩이면 bond0다. 아무것도 안 잡히면 필터가 아니라 인터페이스를 의심한다. ip -br addr로 후보를 먼저 확인하고, 모르겠으면 -i any로 시작한다.

-w로 저장한 파일은 순식간에 디스크를 채운다. 트래픽이 많은 서버에서 필터 없이 몇 분만 돌려도 수 GB가 쌓인다. 루트 파티션이 차면 서비스 전체가 죽는다. 반드시 -C/-W로 상한을 두거나 -c로 개수를 제한하고, 저장 위치는 루트가 아닌 별도 파티션(/var/tmp 등)으로 지정한 뒤 df -h로 여유를 먼저 확인한다.

파이프로 넘길 때는 -l이 필요하다. tcpdump는 기본적으로 출력을 버퍼링하므로 tcpdump ... | grep 패턴이 한참 아무것도 안 뱉는다. 실시간으로 보려면 -l을 붙인다.

snaplen 때문에 페이로드가 잘릴 수 있다. 요즘 tcpdump는 기본값이 충분히 크지만, 오래된 버전이나 일부 환경에서는 68바이트만 잡아 헤더 뒤가 날아간다. 내용까지 봐야 하면 -s 0을 명시한다. 반대로 트래픽만 세면 되는 상황에서는 -s 96 정도로 줄여 부하와 용량을 아낀다.

운영 서버에서 캡처는 부하를 만든다. 필터가 넓을수록 커널이 복사하는 패킷이 늘어난다. 고부하 서버에서는 반드시 좁은 필터와 -c 제한을 걸고, 다 쓴 프로세스가 남아 있지 않은지 ps로 확인한다.

함께 보면 좋은 명령어

  • ss — 캡처 전에 그 포트가 실제로 LISTEN 중인지 먼저 확인한다.
  • nc — 캡처를 띄워둔 상태에서 반대편에서 접속을 유발할 때 쓴다.
  • ip — 캡처할 인터페이스 이름과 라우팅 경로를 확인한다.
  • traceroute — 패킷이 아예 안 보일 때 경로 어디서 끊겼는지 좁힌다.

필터 문법의 정식 정의는 pcap-filter 문서에 정리돼 있다.