포스트

TCP는 어떻게 신뢰를 만드나 — 3-way handshake, 상태 머신, 그리고 TIME_WAIT

TCP 3-way handshake가 왜 3번인지, SYN·ACK와 시퀀스 번호, 4-way 종료와 TIME_WAIT(2MSL), TCP 상태 머신, 순서번호·재전송으로 신뢰성을 만드는 방법, SYN flood 대응까지 다룹니다.

TCP는 어떻게 신뢰를 만드나 — 3-way handshake, 상태 머신, 그리고 TIME_WAIT

best-effort 위에 신뢰를 얹는 일

패킷 교환에서 봤듯 인터넷은 “최선을 다하지만 책임은 안 진다”는 best-effort 망입니다. 패킷은 버려질 수도, 순서가 뒤집힐 수도, 중복될 수도 있습니다. 그런데도 우리는 파일을 한 바이트도 안 틀리고 받습니다. 그 신뢰를 끝단(end-to-end) 에서 만들어내는 게 TCP입니다.

이 글은 “3-way handshake = SYN, SYN-ACK, ACK”를 외우는 데서 멈추지 않습니다. 왜 2번이 아니라 3번인지, 상태가 어떻게 전이하는지, 그리고 실무에서 가장 많이 터지는 TIME_WAIT/SYN flood까지 — 헤더 필드와 커널 파라미터 레벨로 내려갑니다.

TCP 세그먼트 헤더 — 신뢰성의 도구상자

TCP가 하는 모든 일은 이 20바이트(옵션 제외) 헤더 안에 들어 있습니다.

필드크기역할
출발지/목적지 포트각 16비트어느 프로세스로 (IP는 호스트까지, 포트가 프로세스까지)
순서 번호(seq)32비트이 세그먼트 첫 바이트의 스트림 내 위치
확인 번호(ack)32비트“여기까지 잘 받았고, 다음엔 이 번호를 기대한다” (누적 ACK)
데이터 오프셋4비트헤더 길이(옵션 가변)
플래그9비트SYN·ACK·FIN·RST·PSH·URG
윈도우 크기16비트수신 버퍼 여유 = 흐름 제어(다음 글)
체크섬16비트손상 검출

핵심은 seq/ack가 “패킷 번호”가 아니라 “바이트 번호” 라는 점입니다. TCP는 패킷의 모임이 아니라 바이트 스트림입니다. 이 추상화가 재조립·중복제거·재전송을 가능하게 합니다.

연결 수립: 왜 악수를 3번 하나

연결을 열 때 양쪽은 서로의 초기 순서 번호(ISN) 를 교환하고 확인합니다. 아래 흐름을 움직임으로 보세요 — ①SYN ②SYN-ACK ③ACK 가 순서대로 오갑니다.

클라이언트 서버 ① SYN seq=x ② SYN+ACK seq=y, ack=x+1 ③ ACK ack=y+1

왜 3번인가? 2번이면 한쪽만 상대의 ISN을 확인받습니다. 클라이언트는 ②로 서버의 seq를 확인했지만, 서버는 자기 SYN이 도달했는지 모릅니다. ③ACK가 그걸 닫아, 양방향 ISN이 모두 합의됩니다. 또한 2-way라면 지연됐던 옛 SYN이 살아 돌아와 유령 연결이 생길 수 있습니다(ISN 합의가 이를 막습니다).

ISN을 0이 아니라 시간 기반 + 난수로 잡는 이유도 같은 맥락입니다 — 이전 연결의 잔존 세그먼트와 섞이는 것, 그리고 공격자의 시퀀스 추측(세션 하이재킹)을 막기 위해서입니다.

연결 종료: 왜 4번이고, TIME_WAIT는 왜 있나

종료는 4-way입니다. TCP는 양방향 독립 스트림이라, 각 방향을 따로 닫습니다(FIN → ACK를 양쪽이 한 번씩).

sequenceDiagram
    participant A as 능동 닫기(Active)
    participant B as 수동 닫기(Passive)
    A->>B: FIN (더 보낼 것 없음)
    B->>A: ACK
    Note over B: 남은 데이터 전송 가능 (half-close)
    B->>A: FIN
    A->>B: ACK
    Note over A: TIME_WAIT (2·MSL 대기 후 CLOSED)

여기서 TIME_WAIT가 핵심입니다. 마지막 ACK를 보낸 쪽은 곧장 닫지 않고 2·MSL(Max Segment Lifetime, 리눅스 기본 약 60초) 동안 기다립니다.

  • 이유 1: 내 마지막 ACK가 유실되면 상대가 FIN을 재전송합니다. 그때 응답하려면 소켓이 살아 있어야 합니다.
  • 이유 2: 같은 4-튜플(출발IP·포트, 목적IP·포트)의 새 연결에 이전 연결의 떠돌이 세그먼트가 섞이는 걸 막습니다.

상태 머신: 연결은 상태의 전이다

TCP 연결의 일생은 유한 상태 머신입니다. 아래는 능동 연결의 전형적 경로 — 상태가 순서대로 점등합니다.

CLOSED SYN_SENT ESTABLISHED FIN_WAIT TIME_WAIT CLOSED

서버 쪽은 LISTEN → SYN_RCVD → ESTABLISHED로 들어오고, 수동 종료는 CLOSE_WAIT → LAST_ACK를 거칩니다. 이 상태 이름들은 추상 개념이 아니라 ss/netstat 출력에 그대로 찍히는 글자입니다.

신뢰성은 어떻게 만들어지나

best-effort 망의 4가지 문제를 TCP는 이렇게 막습니다.

문제TCP의 해법
손실순서 번호 + 누적 ACK + 재전송(타임아웃 또는 3 dup ACK)
순서 뒤바뀜수신 측이 seq로 재정렬 후 애플리케이션에 전달
중복같은 seq 범위는 버림
손상체크섬 불일치 시 폐기 → 재전송 유발

손실 감지·재전송의 얼마나 빠르게·얼마나 많이는 혼잡 제어의 영역이고, 흐름 제어(수신자가 감당 못 할 만큼 보내지 않기)는 윈도우 필드로 합니다. 둘은 다른 문제입니다 — 다음 글에서 구분합니다.

프로덕션 함정 1: TIME_WAIT 고갈

부하 테스트나 짧은 연결을 대량으로 맺는 클라이언트(또는 keep-alive 안 쓰는 리버스 프록시 → 업스트림)에서 흔합니다. 능동 종료한 쪽에 TIME_WAIT 소켓이 수만 개 쌓여 로컬 포트(ephemeral port)가 고갈됩니다.

1
2
3
4
# TIME_WAIT 개수 확인
ss -tan state time-wait | wc -l
# ephemeral 포트 범위
cat /proc/sys/net/ipv4/ip_local_port_range   # 보통 32768 60999
  • 해법(클라이언트/아웃바운드): net.ipv4.tcp_tw_reuse=1 — TIME_WAIT 소켓을 새 아웃바운드 연결에 재사용(타임스탬프 옵션 필요, 안전). 포트 범위 확대.
  • ⚠️ 하지 말 것: tcp_tw_recycle. NAT 뒤 클라이언트들의 연결을 깨뜨려 리눅스 4.12에서 제거됐습니다. 옛날 블로그가 추천해도 쓰면 안 됩니다.
  • 근본 해법: keep-alive로 연결 재사용 → 그러면 종료 자체가 드물어집니다.

프로덕션 함정 2: SYN flood

3-way handshake에서 서버는 SYN을 받으면 SYN_RCVD 상태로 반쯤 열린 연결(half-open) 을 큐(backlog)에 쌓고 ACK를 기다립니다. 공격자가 출발지 IP를 위조해 SYN만 퍼부으면 이 큐가 차서 정상 연결을 못 받습니다.

1
2
3
# accept 큐 넘침 카운터
netstat -s | grep -i "SYNs to LISTEN"
ss -tl                                  # Recv-Q/Send-Q로 백로그 포화 확인
  • 해법: net.ipv4.tcp_syncookies=1. 큐가 차면 서버 상태를 저장하지 않고, 쿠키를 SYN-ACK의 seq에 인코딩해 보냅니다. 정상 클라이언트의 ACK엔 그 쿠키가 돌아오므로 검증만으로 연결을 복원합니다 — 상태 없이 SYN flood 무력화.

디버깅 도구

1
2
3
4
5
6
7
8
# 연결을 상태별로 집계 (운영 첫 진단)
ss -tan | awk 'NR>1{c[$1]++} END{for(s in c)print s,c[s]}'

# 특정 상태만
ss -tan state established '( dport = :443 )'

# 핸드셰이크를 눈으로 (절대 seq 표시)
sudo tcpdump -n -S 'tcp port 443 and (tcp[tcpflags] & (tcp-syn|tcp-fin) != 0)'

tcpdump에서 Flags [S](SYN) → [S.](SYN-ACK) → [.](ACK) 3줄이 보이면 정상 핸드셰이크입니다. [R](RST)가 보이면 상대가 연결을 거부/강제 종료한 것입니다(닫힌 포트, 방화벽, 앱 크래시).

면접/리뷰 단골 질문

  • Q. 왜 3-way인가, 2-way는 왜 안 되나? → 양방향 ISN을 모두 확인받기 위해. 2-way는 서버가 자기 SYN 도달을 모르고, 지연된 옛 SYN이 유령 연결을 만들 수 있다.
  • Q. TIME_WAIT는 왜 있고 누구에게 생기나? → 마지막 ACK 유실 대비 + 떠돌이 세그먼트 차단. 능동 종료(먼저 FIN 보낸) 쪽에 2·MSL 동안 생긴다.
  • Q. 서버에 CLOSE_WAIT가 잔뜩 쌓였다, 원인은? → 상대가 FIN을 보냈는데 내 애플리케이션이 close()를 안 호출해서. 소켓 누수 — 코드 버그다(커널 튜닝으로 못 고친다).
  • Q. SYN flood 방어는? → tcp_syncookies. 백로그가 차면 상태를 저장하지 않고 쿠키로 핸드셰이크를 무상태 처리한다.

정리

  • TCP는 best-effort 위에 바이트 스트림의 신뢰성을 끝단에서 만든다 — seq/ack는 바이트 번호다.
  • 3-way handshake는 양방향 ISN 합의가 목적(그래서 3번). 종료는 양방향 독립이라 4-way, 능동 쪽에 TIME_WAIT.
  • 실무 사고의 대부분은 상태 머신 이해로 풀린다: TIME_WAIT 고갈(→ keep-alive, tcp_tw_reuse), CLOSE_WAIT 누적(→ 앱이 close 안 함), SYN flood(→ syncookies).
  • 추측 말고 ss/tcpdump로 상태를 직접 보라.

다음 글: 얼마나 빠르게 보낼지를 정하는 TCP 흐름 제어와 혼잡 제어. 그리고 TCP의 한계를 넘으려는 시도가 UDP와 QUIC입니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

시리즈 · 네트워크 A-Z (20편)

  1. 네트워크란 무엇인가 — 회선 교환 vs 패킷 교환, 왜 인터넷은 패킷을 선택했나
  2. OSI 7계층과 TCP/IP — 왜 네트워크를 굳이 층으로 쪼갰나
  3. 이더넷·MAC·스위치 — 스위치는 어떻게 "어느 포트로 보낼지" 스스로 배우나
  4. IP 주소와 서브넷팅 — /24를 /26으로 쪼갠다는 게 비트 단위로 무슨 뜻인가
  5. 라우팅은 어떻게 길을 찾나 — 라우팅 테이블, Longest Prefix Match, 그리고 BGP
  6. ARP와 L2/L3 경계 — IP는 아는데 왜 MAC을 또 묻나
  7. TCP는 어떻게 신뢰를 만드나 — 3-way handshake, 상태 머신, 그리고 TIME_WAIT 지금 보는 글
  8. TCP는 얼마나 빨리 보낼까 — 흐름 제어 vs 혼잡 제어, 그리고 톱니파의 비밀
  9. UDP와 QUIC — TCP를 버려야 더 빨라지는 순간
  10. DNS는 어떻게 이름을 주소로 바꾸나 — 루트부터 권한 서버까지, 그리고 캐시의 마법
  11. HTTP의 진화 — 1.1의 줄 서기에서 HTTP/2 멀티플렉싱, HTTP/3까지
  12. TLS/HTTPS는 어떻게 도청 불가능한 채널을 만드나 — 핸드셰이크부터 인증서 체인까지
  13. NAT와 사설망 — 주소 하나로 수백 대가 인터넷을 쓰는 법, 그리고 P2P가 막히는 이유
  14. 로드 밸런싱 — L4와 L7은 무엇이 다르며, 왜 헬스체크가 서비스를 살리고 죽이나
  15. 프록시·리버스 프록시·CDN — 콘텐츠를 사용자 코앞으로 옮기는 기술
  16. 방화벽·보안그룹·네트워크 ACL — stateful과 stateless가 가르는 모든 것
  17. VPC & 클라우드 네트워킹 — 라우팅 테이블 한 줄이 퍼블릭과 프라이빗을 가른다
  18. 소켓과 커널 네트워크 스택 — accept 한 줄 뒤에서 벌어지는 일
  19. 네트워크 성능 — 대역폭은 왜 빠름이 아닌가 (RTT·BDP·처리량의 진실)
  20. 현대 네트워킹 — 사이드카, eBPF, 그리고 경계가 사라진 신뢰