TCP 3-Way Handshake
TCP 연결이 수립되고 종료되는 과정. SYN·ACK·FIN 플래그의 의미와 각 단계에서 상태 기계(State Machine)가 어떻게 전이되는지 살펴본다.
왜 Handshake가 필요한가?
TCP는 신뢰성 있는 전송을 보장해야 한다. 이를 위해서는 데이터를 보내기 전에 먼저 상대방과 연결을 수립(Establish)하고 양측이 통신 준비가 됐음을 확인해야 한다.
3-Way Handshake는 이 과정에서 세 가지를 달성한다: ① 상대방이 존재하고 응답 가능함을 확인, ② 시퀀스 번호(Sequence Number) 동기화, ③ 연결 파라미터(MSS, Window Size, SACK 지원 여부 등) 협상.
연결 수립: 3-Way Handshake
seq=x 생성
seq=y 생성, x 기억
seq=x+1 기억
각 단계 상세 설명
SYN — 클라이언트 → 서버
클라이언트가 서버에 연결을 요청한다. SYN 플래그를 세우고, 임의로 생성한 초기 시퀀스 번호(ISN, Initial Sequence Number) seq=x를 함께 보낸다. ISN을 임의로 생성하는 이유는 이전 연결의 패킷과 혼동되지 않도록 하기 위해서다. 클라이언트 상태: SYN_SENT
SYN-ACK — 서버 → 클라이언트
서버가 클라이언트의 SYN을 받고 연결을 수락한다. SYN 플래그와 ACK 플래그를 동시에 세운다. 서버의 ISN seq=y를 보내고, 클라이언트의 ISN에 1을 더한 ack=x+1을 보내 "x까지 받았으니 x+1번째 바이트부터 보내라"고 알린다. 서버 상태: SYN_RECEIVED
ACK — 클라이언트 → 서버
클라이언트가 서버의 SYN-ACK를 받고 ACK를 보낸다. ack=y+1로 서버의 SYN을 확인했음을 알린다. 이 패킷이 도착하면 양측 모두 ESTABLISHED 상태가 되고 데이터 전송을 시작할 수 있다. 이 세 번째 ACK에는 이미 HTTP 요청 데이터가 포함될 수 있다(TCP Fast Open).
연결 종료: 4-Way Handshake
TCP 연결 종료는 수립보다 복잡하다. 양방향 연결을 각각 독립적으로 종료해야 하기 때문이다. 한쪽이 전송을 끝내도 반대쪽은 여전히 데이터를 보낼 수 있는 반이중(Half-Close) 상태를 허용한다.
2MSL 대기 후 CLOSED
TIME_WAIT 상태
연결을 먼저 닫는 쪽(Active Close)은 마지막 ACK를 보낸 후 바로 CLOSED가 되지 않고 TIME_WAIT 상태로 2MSL(Maximum Segment Lifetime, 보통 60~120초)을 기다린다.
이유는 두 가지다: ① 마지막 ACK가 유실됐을 때 서버가 FIN을 재전송하면 이를 받아 다시 ACK를 보낼 수 있어야 한다. ② 네트워크에 떠돌아다니는 이전 연결의 지연 패킷이 새 연결에 영향을 주지 않도록 한다.
SO_REUSEADDR 옵션이나 tcp_tw_reuse 커널 파라미터로 완화할 수 있다.
TCP 상태 전이표
| 상태 | 설명 | 발생 시점 |
|---|---|---|
| CLOSED | 연결 없음 | 초기 상태 또는 연결 종료 후 |
| SYN_SENT | SYN 전송, 응답 대기 | 클라이언트: connect() 호출 후 |
| SYN_RECEIVED | SYN 수신, SYN-ACK 전송 | 서버: SYN 수신 후 |
| ESTABLISHED | 연결 수립, 데이터 전송 가능 | 3-Way Handshake 완료 후 |
| FIN_WAIT_1 | FIN 전송, ACK 대기 | Active Close 측: close() 호출 후 |
| FIN_WAIT_2 | ACK 수신, 상대 FIN 대기 | Active Close 측 |
| CLOSE_WAIT | FIN 수신, 앱이 close() 호출 전 | Passive Close 측 |
| TIME_WAIT | 2MSL 대기 중 | Active Close 측: 마지막 ACK 전송 후 |
| CLOSED | 완전 종료 | TIME_WAIT 만료 후 |
시퀀스 번호와 ACK 메커니즘
TCP는 신뢰성을 위해 모든 바이트에 시퀀스 번호(Sequence Number)를 부여한다. 수신 측은 올바르게 받은 마지막 바이트 번호+1을 ACK 번호로 돌려보낸다. 이를 통해 송신 측은 어디까지 전달됐는지 알 수 있다.
# Wireshark로 3-Way Handshake 캡처 예시 # (실제 캡처 시 wireshark 또는 tcpdump 사용) $ tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack|tcp-fin) != 0' -n # 출력 예시: # 14:23:01 192.168.1.10.52341 > 93.184.216.34.443: Flags [S], seq 123456789 # 14:23:01 93.184.216.34.443 > 192.168.1.10.52341: Flags [S.], seq 987654321, ack 123456790 # 14:23:01 192.168.1.10.52341 > 93.184.216.34.443: Flags [.], ack 987654322 # Flags: S=SYN, .=ACK, F=FIN, R=RST, P=PSH
SYN Flooding 공격
3-Way Handshake의 설계를 악용한 DoS 공격이다. 공격자가 위조된 출발지 IP로 대량의 SYN 패킷을 보내면, 서버는 SYN-ACK를 보내고 SYN_RECEIVED 상태의 연결을 Backlog 큐에 쌓는다. ACK는 영영 오지 않으므로 큐가 가득 차 정상 연결을 받지 못하게 된다.
net.ipv4.tcp_syncookies=1로 활성화할 수 있다.
TCP Fast Open (TFO)
일반 3-Way Handshake는 데이터를 보내기 전에 1 RTT(Round Trip Time)를 소비한다. TCP Fast Open은 최초 연결 시 쿠키를 발급받고, 이후 연결에서는 SYN 패킷에 데이터를 실어 보내 핸드셰이크 지연을 줄이는 기법이다. HTTP/1.1에서 HTTPS 연결 시 체감되는 지연 개선에 활용된다.
# Linux에서 TCP Fast Open 활성화
$ sysctl net.ipv4.tcp_fastopen=3
# 1: 클라이언트만, 2: 서버만, 3: 양쪽 모두
# Python 소켓으로 TFO 사용
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_FASTOPEN, 5)
s.sendto(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
socket.MSG_FASTOPEN, ("example.com", 80))
댓글