📚 네트워크 · 패킷 분석 › 01. TCP/IP 구조 이해 — 41편
이전 글: 40. TCP와 UDP 헤더 비교 — 신뢰성과 속도의 차이 · 다음 글: 42. TCP 상태 전이 — LISTEN부터 TIME_WAIT까지
참고(리눅스 시스템 기초): Socket 이해 — listen·accept·connect 소켓 흐름
관제 보고서에서 가장 조심해야 하는 표현 중 하나가 "접속했다"입니다. 방화벽 로그에 허용(allow) 이벤트가 있다고 해서 실제로 연결이 성립했다는 뜻은 아닙니다. 방화벽이 첫 SYN 패킷을 통과시킨 시점에 로그를 남기는 장비도 많기 때문입니다.
이 네 가지는 영향도 판단에서 전혀 다른 결론으로 이어집니다. "외부 IP가 내부 서버 22번에 접속 시도"와 "외부 IP가 내부 서버 22번과 세션을 맺고 데이터를 주고받음"은 대응 우선순위가 다릅니다. 이번 글은 3-Way Handshake의 원리와, 연결이 성립했다는 증거를 어디서 어떻게 확인하는지를 다룹니다.
| 단계 | 방향 | 플래그 | 담긴 정보 | 의미 |
|---|---|---|---|---|
| 1 | 클라이언트 → 서버 | SYN | 클라이언트 초기 시퀀스 번호(ISN) = x, MSS·Window Scale 등 옵션 | "연결하고 싶다, 내 번호는 x부터 시작" |
| 2 | 서버 → 클라이언트 | SYN, ACK | 서버 ISN = y, ACK = x+1, 서버 옵션 | "받았다(x+1 기다림), 내 번호는 y부터" |
| 3 | 클라이언트 → 서버 | ACK | ACK = y+1 | "네 번호도 받았다" → 양쪽 모두 ESTABLISHED |
핵심은 양쪽이 서로의 시퀀스 번호를 확인했다는 것입니다. SYN 플래그 자체가 시퀀스 번호 1개를 소비하므로 응답 ACK는 ISN + 1이 됩니다. 시퀀스 번호와 ACK의 상세 동작은 11편에서 다룹니다.
ISN은 예측하기 어렵게 무작위에 가깝게 생성됩니다. 과거 ISN을 예측할 수 있던 시절에는 응답을 보지 않고도 위조 연결을 만들 수 있었기 때문입니다.
서버 커널은 연결 요청을 두 단계로 관리합니다.
| 대기열 | 들어가는 시점 | 상태 | 가득 차면 |
|---|---|---|---|
| SYN 대기열 (미완성 연결) | SYN 수신 후 SYN/ACK 전송 | SYN-RECEIVED (ss에서 SYN-RECV) | 새 SYN을 버리거나, SYN 쿠키 사용 |
| Accept 대기열 (완성 연결) | 마지막 ACK 수신 후 | ESTABLISHED, 애플리케이션의 accept() 대기 | 커널 카운터 ListenOverflows 증가, 연결 드롭 |
SYN Flood는 첫 번째 대기열을 미완성 연결로 채워 정상 사용자의 연결을 막는 공격입니다. Linux는 이 대기열이 넘칠 때 SYN 쿠키(net.ipv4.tcp_syncookies)로 상태를 저장하지 않고 SYN/ACK를 보내는 방식으로 대응합니다.
| 서버 측 상황 | 돌아오는 것 | 클라이언트 결과 |
|---|---|---|
| 포트 열림 (LISTEN) | SYN/ACK | 연결 성립 |
| 포트 닫힘 (호스트는 살아 있음) | RST/ACK | 즉시 Connection refused |
| 방화벽 drop | 없음 | SYN 재전송 반복 후 타임아웃 |
| 방화벽 reject | RST 또는 ICMP 3/13 등 (설정에 따라) | 즉시 실패 |
| 호스트 없음 (같은 대역) | 없음 (ARP 실패) | No route to host 또는 타임아웃 |
클라이언트 192.168.10.10이 서버 192.168.10.20:22에 접속하는 흐름입니다. 괄호는 각 시점의 TCP 상태입니다.

그림 1. SYN → SYN/ACK → ACK로 양쪽이 서로의 시퀀스 번호를 확인합니다

그림 2. 응답 유형에 따라 '성립·거부·무응답'이 구분됩니다
클라이언트 192.168.10.10:51514 서버 192.168.10.20:22
(CLOSED) (LISTEN)
│ │
│ ① SYN seq=x │
│ ─────────────────────────────────────────────→ │
(SYN-SENT) │ SYN 대기열에 등록
│ │ (SYN-RECEIVED)
│ ② SYN/ACK seq=y, ack=x+1 │
│ ←───────────────────────────────────────────── │
│ │
│ ③ ACK seq=x+1, ack=y+1 │
│ ─────────────────────────────────────────────→ │
(ESTABLISHED) │ Accept 대기열로 이동
│ │ (ESTABLISHED)
│ ↓
│ 애플리케이션 accept()
│ ④ 데이터 (PSH/ACK) ... 실제 통신 시작 │
↓ ↓
응답이 오지 않으면 클라이언트 커널은 SYN을 간격을 늘려 가며 재전송합니다. 재전송 횟수는 net.ipv4.tcp_syn_retries(일반적인 기본값 6)로 정해지며, 이 때문에 drop 정책 대상에 접속하면 수십 초에서 2분가량 기다린 뒤 실패합니다. 서버 쪽 SYN/ACK 재전송 횟수는 net.ipv4.tcp_synack_retries(일반적인 기본값 5)입니다.
관제 관점에서 기억할 점은, 같은 SYN이 짧은 간격으로 여러 번 보이는 것은 재전송일 수 있다는 것입니다. 출발지 포트와 시퀀스 번호가 같으면 새로운 시도가 아니라 같은 시도의 재전송입니다.
실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu) 2대(192.168.10.10 클라이언트, 192.168.10.20 서버 예시, 서버에 sshd 동작 중). 인터페이스 이름은 ens33을 예시로 사용합니다(환경에 따라 다름).
# 도구 설치
sudo dnf install -y tcpdump nmap-ncat # Rocky
sudo apt install -y tcpdump netcat-openbsd # Ubuntu
# [서버] SSH 포트의 처음 3개 패킷만 캡처 (-S: 시퀀스 번호를 절대값으로 표시)
sudo tcpdump -nn -S -i ens33 -c 3 'tcp port 22'
# [클라이언트] 연결만 확인하고 종료
nc -zv 192.168.10.20 22
# [서버] 8081 포트 캡처 (이 포트에는 서비스가 없다고 가정)
sudo tcpdump -nn -i ens33 'tcp port 8081'
# [클라이언트]
nc -zv 192.168.10.20 8081
# SYN만 켜진 패킷 = 연결 시작 요청
sudo tcpdump -nn -i ens33 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'
# SYN과 ACK가 모두 켜진 패킷 = 서버의 수락 응답
sudo tcpdump -nn -i ens33 'tcp[tcpflags] & (tcp-syn|tcp-ack) == (tcp-syn|tcp-ack)'
# RST가 켜진 패킷 = 거부·중단
sudo tcpdump -nn -i ens33 'tcp[tcpflags] & tcp-rst != 0'
첫 번째 필터에서 & (tcp-syn|tcp-ack)로 SYN·ACK 두 비트만 떼어 낸 뒤 == tcp-syn으로 비교하므로, SYN은 켜져 있고 ACK는 꺼진 패킷만 남습니다. tcp-syn != 0만 쓰면 SYN/ACK까지 함께 잡힙니다.
# 미완성 연결(SYN-RECV) 수
ss -tan state syn-recv
# 클라이언트에서 응답을 기다리는 연결(SYN-SENT)
ss -tan state syn-sent
# LISTEN 소켓의 대기열: Recv-Q = 현재 accept 대기 수, Send-Q = 최대 backlog
ss -ltn
# SYN 쿠키·대기열 관련 설정
sysctl net.ipv4.tcp_syncookies net.ipv4.tcp_max_syn_backlog net.core.somaxconn
sysctl net.ipv4.tcp_syn_retries net.ipv4.tcp_synack_retries
# 누적 카운터 (-a: 누적값, -z: 0인 항목도 표시)
nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtSyncookiesSent
nstat는 iproute2 패키지에 포함되어 Rocky·Ubuntu 모두 기본 설치되어 있는 경우가 많습니다.
📷 [실습 화면 삽입]
tcpdump -nn -S 'tcp port 22'결과 — SYN, SYN/ACK, ACK 3줄과 seq/ack 값의 +1 관계
tcpdump 출력 형식 예시(값은 환경마다 다름):
# 성공 (-S 절대 시퀀스)
IP 192.168.10.10.51514 > 192.168.10.20.22: Flags [S], seq 1180562114, win 64240, options [mss 1460,sackOK,TS val 1920311 ecr 0,nop,wscale 7], length 0
IP 192.168.10.20.22 > 192.168.10.10.51514: Flags [S.], seq 3021447785, ack 1180562115, win 65160, options [mss 1460,sackOK,TS val 88213 ecr 1920311,nop,wscale 7], length 0
IP 192.168.10.10.51514 > 192.168.10.20.22: Flags [.], ack 3021447786, win 502, options [nop,nop,TS val 1920312 ecr 88213], length 0
# 거부 (닫힌 포트)
IP 192.168.10.10.51520 > 192.168.10.20.8081: Flags [S], seq 2231907741, win 64240, options [...], length 0
IP 192.168.10.20.8081 > 192.168.10.10.51520: Flags [R.], seq 0, ack 2231907742, win 0, length 0
| 확인 포인트 | 성공 | 거부 |
|---|---|---|
| 두 번째 패킷 플래그 | [S.] (SYN/ACK) | [R.] (RST/ACK) |
| 두 번째 패킷의 ack | 클라이언트 seq + 1 | 클라이언트 seq + 1 (받은 것은 확인하되 거부) |
| 세 번째 패킷 | [.] ACK, ack = 서버 seq + 1 | 없음 |
nc -zv 결과 | succeeded / Connected 계열 메시지 | Connection refused |
-S를 빼면 tcpdump는 두 번째 패킷부터 시퀀스 번호를 상대값(ack 1)으로 표시합니다. 번호 관계를 확인할 때는 -S가 편합니다.
tcp_syncookies 설정과 ListenOverflows 카운터를 확인했다📷 [실습 화면 삽입] 닫힌 포트 캡처 —
Flags [S]다음Flags [R.]라인과nc의 Connection refused
| 관찰 | 정상일 수 있는 경우 | 의심해야 하는 경우 |
|---|---|---|
| SYN만 있고 응답 없음 | 대상 서버 중지, 방화벽 drop 정책 (설정 오류 포함) | 내부 단말이 외부 특정 IP로 일정 간격 SYN 반복 → 차단된 C2 접속 재시도 가능성 |
| SYN → RST 다수 | 서비스 재시작 중 일시적 거부 | 한 출발지가 여러 포트·여러 호스트로 SYN → 대부분 RST → 포트 탐색 (05. 네트워크 스캔 · 공격 징후 분석 시리즈에서 다룸) |
서버에 SYN-RECV 다수 | 순간 접속 폭주, 느린 클라이언트 네트워크 | 수백~수천 개 지속 + 출발지 IP가 매우 다양 + ACK가 오지 않음 → SYN Flood |
| 요청하지 않은 SYN/ACK 수신 | 비대칭 라우팅으로 SYN은 다른 경로를 탄 경우 | 여러 외부 서버에서 SYN/ACK 유입 → 누군가 내 IP를 출발지로 위조해 SYN을 보냄(backscatter) |
| Handshake 성립 후 데이터 없이 바로 종료 | 헬스체크, 포트 연결 확인 | 여러 포트에 대해 반복 → 연결 기반 탐색 |
# 출발지별 SYN 수 집계 (100개 캡처 후 상위 출발지)
sudo tcpdump -nn -i ens33 -c 100 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn' 2>/dev/null \
| awk '{print $3}' | cut -d. -f1-4 | sort | uniq -c | sort -rn | head
# SYN-RECV 수 추이 (1초마다 갱신, -H: 헤더 제외)
watch -n1 'ss -Htan state syn-recv | wc -l'
위 awk 예시는 tcpdump 기본 출력에서 세 번째 필드가 출발지IP.포트라는 점을 이용합니다. 옵션에 따라 출력 필드 위치가 달라질 수 있으므로 먼저 원본 출력을 확인한 뒤 사용합니다.
흔적이 남는 곳
| 위치 | 확인할 수 있는 것 |
|---|---|
| 방화벽 세션 로그 | 세션 생성 시점, 종료 사유, 송수신 바이트. 바이트 0 또는 패킷 1~2개인 세션은 성립하지 않았을 가능성 |
| NetFlow/세션 요약 | 세션의 누적 TCP 플래그 (SYN만 있는지, ACK까지 있는지) |
Zeek conn.log | conn_state 값으로 성립 여부 요약 (예: S0 시도만 있고 응답 없음, REJ 거부, SF 정상 성립·종료). 07. 네트워크 보안관제 통합 분석 시리즈에서 다룸 |
| 서버 애플리케이션 로그 | Handshake가 끝나고 애플리케이션이 accept()한 뒤에만 기록됨 (예: sshd 접속 로그) |
한계와 오탐 주의
SYN-RECV 상태가 쌓이지 않을 수 있어, 상태 수만으로 SYN Flood를 판단하면 놓칠 수 있습니다. TcpExtSyncookiesSent 카운터와 커널 로그의 SYN flooding on port 메시지를 함께 확인합니다.'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'은 연결 요청만, == (tcp-syn|tcp-ack)는 수락 응답만 걸러냅니다.SYN-RECV 누적, ListenOverflows·SyncookiesSent 카운터는 SYN Flood 판단 근거가 됩니다.