📚 네트워크 · 패킷 분석 › 01. TCP/IP 구조 이해 — 46편
이전 글: 45. TCP 연결 종료 — FIN과 RST의 차이 · 다음 글: 47. DNS 이름 해석 흐름 — 도메인이 IP가 되기까지
참고(리눅스 시스템 기초): IP / TCP / UDP / Port — TCP·UDP 기본 개념과 포트
앞선 글에서 TCP 연결이 3-Way Handshake로 성립하고, FIN·RST로 끝나며, 그 사이에 여러 상태를 거친다는 것을 확인했습니다. 그런데 연결이 "성립했다"는 것과 "데이터가 실제로 얼마나, 제대로 오갔다"는 것은 다른 이야기입니다.
TCP가 순서 보장과 손실 복구를 하는 근거는 헤더의 세 값, 시퀀스 번호(Sequence Number), 확인 응답 번호(Acknowledgment Number), 윈도우(Window) 입니다. 이 값을 읽을 수 있으면 다음 질문에 패킷 근거로 답할 수 있습니다.
관제 업무에서 "연결은 됐는데 데이터가 안 갔다", "업로드가 중간에 멈췄다", "IPS 적용 후 특정 서비스가 느려졌다" 같은 문의가 들어오면, 결국 이 세 값의 흐름을 확인하게 됩니다.
시퀀스 번호는 이 세그먼트에 담긴 첫 번째 데이터 바이트의 번호입니다. TCP는 데이터를 패킷 단위가 아니라 바이트 스트림으로 보고 바이트마다 번호를 매깁니다.
ACK 번호는 "여기까지 잘 받았으니, 다음엔 이 번호부터 보내 달라" 는 뜻입니다. 즉 마지막으로 받은 바이트 번호 + 1입니다.
윈도우 필드는 수신 측이 ACK 번호 이후로 추가로 받을 수 있는 바이트 수(수신 윈도우, rwnd)를 광고합니다. 송신 측은 ACK를 받지 못한 데이터가 이 크기를 넘지 않도록 전송량을 조절합니다. 이것이 흐름 제어(Flow Control) 입니다.
| 구분 | 수신 윈도우 (rwnd) | 혼잡 윈도우 (cwnd) |
|---|---|---|
| 누가 정하나 | 수신 측 | 송신 측 (내부 계산) |
| 무엇을 보호하나 | 수신 측 버퍼 | 네트워크 경로 |
| 패킷에 보이나 | 보임 (Window 필드) | 보이지 않음 (ss -ti 로 호스트에서 확인) |
| 줄어드는 원인 | 애플리케이션이 데이터를 늦게 읽음 | 손실·지연 감지 |
송신 측이 한 번에 보낼 수 있는 양은 대략 min(rwnd, cwnd) 입니다. 패킷에서 윈도우가 넉넉한데도 전송이 느리다면 혼잡 제어나 경로 문제를 의심할 수 있습니다.
Wireshark와 tcpdump는 보기 쉽게 ISN을 0으로 놓은 상대 시퀀스 번호를 기본으로 보여 줍니다. 아래 흐름도도 상대 번호 기준입니다.

그림 1. 중간 세그먼트가 유실되면 수신 측은 같은 ACK를 반복하고, 송신 측은 빠진 부분을 재전송합니다
Client (192.168.10.20) Server (192.168.10.10:8080)
| SYN seq=0 win=64240 (WS=128, SACK_PERM, MSS=1460)
|------------------------------------------------------------->|
| SYN,ACK seq=0 ack=1 win=65160 (WS=128, SACK_PERM, MSS=1460)
|<-------------------------------------------------------------|
| ACK seq=1 ack=1 |
|------------------------------------------------------------->|
| PSH,ACK seq=1 ack=1 len=100 (HTTP 요청 100바이트) |
|------------------------------------------------------------->|
| ↓ 서버: 1~100번 바이트 수신 → 다음은 101 |
| ACK seq=1 ack=101 |
|<-------------------------------------------------------------|
| ACK seq=1 ack=101 len=1448 (응답 1번째 조각: 1~1448) |
|<-------------------------------------------------------------|
| ACK seq=1449 ack=101 len=1448 (응답 2번째 조각) ✕ 손실 |
|<- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
| ACK seq=2897 ack=101 len=1448 (3번째 조각 도착) |
|<-------------------------------------------------------------|
| ↓ 클라이언트: 1449부터 비어 있음 |
| ACK seq=101 ack=1449 SACK=2897-4345 (Dup ACK) |
|------------------------------------------------------------->|
| ACK seq=1449 ack=101 len=1448 (재전송) |
|<-------------------------------------------------------------|
| ACK seq=101 ack=4345 (빈틈 복구 → 누적 ACK 한 번에 전진) |
|------------------------------------------------------------->|
핵심 규칙은 세 가지입니다.
| 방식 | 계기 | 패킷에서 보이는 모습 |
|---|---|---|
| 타임아웃 재전송 (RTO) | 일정 시간 ACK가 오지 않음 | 같은 seq 세그먼트가 시간 간격을 두고 다시 나옴, 간격이 점점 늘어남 |
| 빠른 재전송 (Fast Retransmit) | 중복 ACK 3개 수신 | Dup ACK 여러 개 직후 같은 seq 재전송 |
RTO 재전송은 간격이 지수적으로 늘어나기 때문에(백오프), 응답이 전혀 없는 상대에게 보낸 SYN이나 데이터가 1초, 2초, 4초… 간격으로 반복되는 모습이 보입니다. 이 값은 OS와 설정마다 다르므로 정확한 초 단위보다는 "간격이 늘어나는 패턴"으로 기억하는 것이 좋습니다.
Server(송신) ── data ──→ Client(수신) win 점점 감소 (애플리케이션이 읽지 않음)
↓
Client ── ACK win=0 ──→ Server [TCP ZeroWindow]
↓
Server ── Window Probe ──→ Client (주기적으로 1바이트 또는 빈 세그먼트)
↓
Client ── ACK win=29200 ──→ Server [TCP Window Update]
↓
Server ── data 재개
Zero Window는 네트워크 문제가 아니라 수신 측 애플리케이션이 버퍼를 비우지 못하는 상황입니다. 원인을 네트워크 장비에서 찾으면 헛수고가 됩니다.
실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu), 인터페이스 이름은 ens33 예시입니다. VM(192.168.10.10)을 서버로, 같은 대역의 다른 VM 또는 호스트 PC(192.168.10.20)를 클라이언트로 사용합니다.
# Rocky Linux
sudo dnf install -y tcpdump iproute python3 curl
# Ubuntu
sudo apt update && sudo apt install -y tcpdump iproute2 python3 curl
# 서버 VM: 20MB 테스트 파일 생성 후 간이 웹 서버 실행
mkdir -p ~/seqlab && cd ~/seqlab
dd if=/dev/zero of=test.bin bs=1M count=20
python3 -m http.server 8080
# 방화벽 임시 허용 (실습 후 반드시 제거)
# Rocky: sudo firewall-cmd --add-port=8080/tcp (--permanent 없이 → 재부팅/리로드 시 사라짐)
# Ubuntu: sudo ufw allow 8080/tcp → 실습 후 sudo ufw delete allow 8080/tcp
# 서버 VM, 다른 터미널: 캡처 시작 (Handshake부터 반드시 포함)
sudo tcpdump -i ens33 -nn port 8080 -w seq_lab.pcap
# 클라이언트: 파일 다운로드
curl -o /dev/null http://192.168.10.10:8080/test.bin
# 상대 시퀀스 번호 (기본)
sudo tcpdump -nn -r seq_lab.pcap -c 8
# 절대 시퀀스 번호 (-S): 실제 헤더에 들어 있는 값
sudo tcpdump -nn -S -r seq_lab.pcap -c 8
📷 [실습 화면 삽입] 같은 pcap을
-S없이/있이 읽은 두 결과를 나란히 — seq 값이 0·1 기준 상대 번호와 큰 절대 번호로 다르게 보이는 부분
# 다운로드가 진행 중일 때 서버 VM에서 실행
ss -tin '( sport = :8080 )'
# 커널의 TCP 관련 설정
sysctl net.ipv4.tcp_window_scaling net.ipv4.tcp_sack net.ipv4.tcp_rmem net.ipv4.tcp_wmem
# 시스템 전체 재전송 카운터 (누적값)
nstat -az TcpRetransSegs TcpOutSegs
본인 VM에서만, SSH 세션도 영향을 받을 수 있으니 콘솔 접속 상태에서 진행합니다.
# Rocky는 tc가 iproute-tc 패키지, netem 모듈은 kernel-modules-extra 에 있을 수 있음
# sudo dnf install -y iproute-tc kernel-modules-extra
# Ubuntu는 iproute2 에 tc 포함
sudo tc qdisc add dev ens33 root netem loss 3% # 송신 패킷 3% 손실
# ... 클라이언트에서 curl 다운로드 재실행, tcpdump로 캡처 ...
sudo tc qdisc del dev ens33 root # 반드시 원복
📷 [실습 화면 삽입] netem 적용 후 Wireshark에서
tcp.analysis.flags필터를 건 화면 — Dup ACK와 Retransmission 표시 줄
ss -tin 출력 형식 예시(값은 환경마다 다름):
ESTAB 0 2896000 192.168.10.10:8080 192.168.10.20:51544
cubic wscale:7,7 rto:204 rtt:0.61/0.12 ato:40 mss:1448 pmtu:1500
rcvmss:536 advmss:1448 cwnd:10 bytes_sent:8452096 bytes_acked:5556096
retrans:0/3 ...
| 필드 | 의미 | 볼 점 |
|---|---|---|
wscale:7,7 | 송신/수신 Window Scale 값 | Handshake에서 협상된 결과 |
rto | 현재 재전송 타임아웃(ms) | 손실이 이어지면 커짐 |
rtt | 평균 왕복 시간/편차(ms) | 경로 지연 판단 |
mss | 세그먼트 최대 데이터 크기 | 1460에서 TCP 옵션(타임스탬프 등)만큼 줄어든 값일 수 있음 |
cwnd | 혼잡 윈도우(세그먼트 수) | 패킷에는 없는, 송신 측 내부 값 |
retrans | 현재 미해결/누적 재전송 수 | 0이 아니면 손실 경험 |
tcpdump 상대 번호 출력 형식 예시(값은 환경마다 다름):
192.168.10.20.51544 > 192.168.10.10.8080: Flags [P.], seq 1:95, ack 1, win 502, length 94
192.168.10.10.8080 > 192.168.10.20.51544: Flags [.], ack 95, win 510, length 0
seq 1:95는 1번부터 94번 바이트까지(끝 번호 95는 다음 seq)라는 뜻이고, 서버는 ack 95로 그대로 응답했습니다. win 502는 헤더 필드 원값이며, 실제 윈도우는 502 × 2^7 = 64,256바이트입니다.
다음 seq = seq + len 규칙을 3개 이상 패킷에서 검산했다win 원값과 scale을 곱해 실제 윈도우를 계산했다| 필터 | 의미 |
|---|---|
tcp.analysis.flags | Wireshark가 표시한 모든 TCP 이상 표시 |
tcp.analysis.retransmission | 재전송으로 판단된 세그먼트 |
tcp.analysis.fast_retransmission | 중복 ACK 이후 빠른 재전송 |
tcp.analysis.duplicate_ack | 중복 ACK |
tcp.analysis.lost_segment | 캡처상 앞 구간이 비어 있음 (캡처 누락일 수도 있음) |
tcp.analysis.out_of_order | 순서가 뒤바뀐 도착 |
tcp.analysis.zero_window | 윈도우 0 광고 |
tcp.analysis.window_full | 송신 측이 상대 윈도우를 꽉 채움 |
Wireshark의 tcp.window_size_scalefactor 가 -1(unknown)이면 Handshake가 캡처에 없어 스케일을 모르는 상태, -2면 스케일을 사용하지 않는 연결입니다. 이 상태에서 계산된 윈도우 값은 믿으면 안 됩니다.
| 관찰 | 정상일 수 있는 경우 | 의심해야 하는 경우 |
|---|---|---|
| 재전송 소량 | 무선망·장거리 구간, 순간 혼잡 | 특정 목적지·특정 시간대에만 반복 → 장비 드롭(IPS 인라인 차단 포함) 가능성 |
| Zero Window | 수신 애플리케이션 일시 지연 | 클라이언트가 의도적으로 작은/0 윈도우를 유지하며 연결만 오래 붙잡음(느린 읽기형 자원 고갈 원리) |
lost_segment | 캡처 장비가 패킷을 놓침(미러링 과부하) | 실제 손실과 구분하려면 호스트 nstat 재전송 카운터와 비교 |
| 세션 중간 RST | 서버 타임아웃, 애플리케이션 종료 | seq가 현재 흐름과 동떨어진 RST, TTL이 앞뒤 패킷과 다른 RST → 경로상 장비나 제3자가 끼워 넣은 패킷일 가능성 |
| ACK가 보낸 적 없는 데이터를 확인 | 캡처 시작 전 데이터(중간부터 캡처) | 캡처가 완전한데도 발생 → 세션 위조·비대칭 경로 확인 |
# 재전송·이상 표시가 있는 패킷만 요약 (tshark는 Wireshark CLI, 패키지: Rocky wireshark-cli / Ubuntu tshark)
tshark -r seq_lab.pcap -Y 'tcp.analysis.flags' -T fields \
-e frame.number -e ip.src -e ip.dst -e tcp.seq -e tcp.ack -e tcp.len -e _ws.col.Info
# 세션 중간 RST의 seq와 TTL 확인
tshark -r seq_lab.pcap -Y 'tcp.flags.reset == 1' -T fields -e frame.number -e ip.src -e ip.ttl -e tcp.seq
📷 [실습 화면 삽입] Wireshark 패킷 상세에서
[SEQ/ACK analysis]항목을 펼친 화면 — Next Sequence Number와 iRTT 표시
흔적이 남는 곳
ss -ti, nstat의 재전송 카운터는 해당 서버에서 실제로 무슨 일이 있었는지 알려 주는 보조 증거입니다.관제에서의 활용
한계와 오탐 주의
lost_segment를 곧바로 "네트워크 손실"로 보고하면 안 됩니다.