우리가 인터넷을 통해 웹사이트를 방문하거나, 이메일을 보내거나, 파일을 다운로드할 때마다 보이지 않는 곳에서는 수많은 통신 규약들이 작동하고 있습니다. 그중에서도 데이터 전송의 신뢰성을 보장하는 가장 핵심적인 프로토콜이 바로 TCP(Transmission Control Protocol)입니다. TCP가 어떻게 두 컴퓨터 간에 데이터가 유실되거나 순서가 뒤바뀌지 않도록 보장할 수 있는 것일까요? 그 비밀의 첫 단추는 바로 TCP 3-Way Handshake라고 불리는 연결 수립 과정에 있습니다. 이 과정은 단순히 통신을 시작하겠다는 신호를 보내는 것을 넘어, 양측이 데이터를 주고받을 준비가 되었음을 서로 확인하고, 통신에 필요한 초기 설정값을 동기화하는 매우 정교한 '의식'과도 같습니다.
이 과정을 이해하기 위해 전화 통화에 비유해 보겠습니다. A가 B에게 전화를 겁니다. B가 전화를 받으면 "여보세요?"라고 말하며 자신이 전화를 받았음을 알립니다(첫 번째 단계). A는 B의 목소리를 듣고 "아, B씨 맞으시죠? 저 A입니다. 제 말 들리시나요?"라고 말하며 상대방을 확인하고 자신의 목소리가 들리는지 묻습니다(두 번째 단계). B는 A의 목소리를 듣고 "네, A씨. 잘 들립니다."라고 대답함으로써 양측 모두 서로의 목소리가 들리는 것을 확인하고 비로소 본 통화를 시작할 수 있게 됩니다(세 번째 단계). 만약 이 과정 중 하나라도 빠진다면, 한쪽만 일방적으로 이야기하거나 상대방이 듣고 있는지 확신할 수 없는 불안정한 통화가 될 것입니다. TCP 3-Way Handshake는 이와 같이 컴퓨터 세계에서 신뢰할 수 있는 '통화 연결'을 수립하는 과정입니다.
TCP 3-Way Handshake의 세 단계 심층 분석
TCP 통신은 클라이언트(Client)와 서버(Server) 모델을 기반으로 합니다. 일반적으로 연결을 요청하는 쪽이 클라이언트, 요청을 받아들이는 쪽이 서버가 됩니다. 웹 브라우저가 웹 서버에 접속하는 경우가 대표적인 예입니다. 이 과정은 세 개의 TCP 패킷(Packet)을 교환하는 형태로 이루어집니다. 각 패킷에는 TCP 헤더(Header)라는 중요한 정보가 포함되어 있으며, Handshake 과정에서는 이 헤더의 특정 플래그(Flag) 비트와 순서 번호(Sequence Number)가 핵심적인 역할을 합니다.
주요 플래그는 다음과 같습니다:
- SYN (Synchronize Sequence Numbers): 연결 요청을 시작할 때 사용됩니다. "지금부터 통신을 시작하고 싶으니, 내 순서 번호를 여기에 맞춰줘"라는 의미를 가집니다.
- ACK (Acknowledgement): 상대방으로부터 받은 패킷을 잘 받았다는 확인 응답입니다. "네가 보낸 메시지는 여기까지 잘 받았어"라는 의미를 전달합니다.
- FIN (Finish): 연결을 종료하고자 할 때 사용됩니다.
- RST (Reset): 비정상적인 연결을 강제로 끊을 때 사용됩니다.
이제 이 플래그들이 실제 Handshake 과정에서 어떻게 사용되는지 단계별로 자세히 살펴보겠습니다.
1단계: 클라이언트의 연결 요청 (SYN)
모든 것은 클라이언트가 서버에게 연결을 요청하는 것으로 시작됩니다. 이때 클라이언트는 SYN 플래그가 1로 설정된 특별한 TCP 패킷을 서버로 전송합니다. 이 패킷은 단순한 요청 신호가 아닙니다. 여기에는 매우 중요한 정보인 초기 순서 번호(Initial Sequence Number, ISN)가 포함되어 있습니다.
순서 번호는 TCP가 데이터를 조각내어 보낼 때, 각 조각(세그먼트)이 올바른 순서로 재조립될 수 있도록 붙이는 고유한 번호입니다. 통신을 시작할 때 양측은 이 순서 번호를 무엇부터 시작할지 서로 알려주어야 합니다. 클라이언트는 이 첫 SYN 패킷에 자신의 ISN을 실어 보냅니다. 예를 들어, 클라이언트가 생성한 ISN이 x라고 가정해 봅시다.
Client -> Server TCP Packet Flags: SYN=1, ACK=0 Sequence Number: x
이 패킷을 보낸 클라이언트는 서버로부터 응답이 오기를 기다리는 SYN_SENT 상태로 전환됩니다. 만약 서버가 응답하지 않으면, 클라이언트는 일정 시간 후에 다시 SYN 패킷을 보내는 재전송 메커니즘을 사용합니다.
여기서 중요한 점은 ISN이 예측 가능하지 않은 임의의 숫자로 생성된다는 것입니다. 만약 ISN이 0, 1, 2처럼 순차적이거나 예측 가능하다면, 공격자가 TCP 연결을 가로채는 'TCP 세션 하이재킹(TCP Session Hijacking)' 공격에 취약해질 수 있습니다. 따라서 현대 운영체제는 보안을 위해 매우 복잡한 알고리즘을 통해 무작위적인 ISN을 생성합니다.
2단계: 서버의 응답 및 연결 요청 (SYN-ACK)
서버는 LISTEN 상태에서 클라이언트의 연결 요청을 기다리다가 SYN 패킷을 수신합니다. 패킷을 받으면 서버는 연결을 수락할 준비를 합니다. 이 준비 과정에는 클라이언트와의 통신을 위한 TCB(Transmission Control Block)라는 데이터 구조를 커널 메모리에 할당하는 작업이 포함됩니다. TCB에는 클라이언트의 IP 주소, 포트 번호, 순서 번호 등 연결에 필요한 모든 정보가 저장됩니다.
준비를 마친 서버는 클라이언트에게 응답 패킷을 보냅니다. 이 패킷은 두 가지 중요한 역할을 동시에 수행합니다.
- 클라이언트의 요청 수락(ACK): 서버는 클라이언트가 보낸 SYN 패킷을 잘 받았다는 것을 알려주기 위해 ACK 플래그를 1로 설정합니다. 이때 확인 번호(Acknowledgement Number) 필드에는 클라이언트가 보낸 순서 번호(
x)에 1을 더한 값(x+1)을 담아 보냅니다. 이는 "나는 당신이 보낸x번 패킷까지 잘 받았고, 다음에는x+1번 패킷을 보내주세요"라는 의미입니다. - 서버 자신의 동기화 요청(SYN): 서버 또한 자신의 데이터 전송을 위해 고유한 초기 순서 번호(ISN)를 생성해야 합니다. 서버는 SYN 플래그를 1로 설정하고, 자신의 ISN(예:
y)을 순서 번호 필드에 담아 클라이언트에게 보냅니다.
이처럼 두 가지 역할을 한 번에 수행하기 때문에 이 패킷을 SYN-ACK 패킷이라고 부릅니다.
Server -> Client TCP Packet Flags: SYN=1, ACK=1 Sequence Number: y Acknowledgement Number: x + 1
이 패킷을 보낸 후, 서버는 클라이언트의 마지막 확인 응답을 기다리는 SYN_RECEIVED 상태가 됩니다. 이 상태는 잠재적인 보안 위협에 노출될 수 있는 구간이며, 이에 대해서는 나중에 더 자세히 다루겠습니다.
3단계: 클라이언트의 최종 확인 (ACK)
클라이언트는 서버로부터 SYN-ACK 패킷을 받습니다. 이 패킷을 통해 클라이언트는 두 가지 사실을 확인할 수 있습니다. 첫째, 자신이 보낸 연결 요청이 서버에 성공적으로 도달했다는 것(Acknowledgement Number x+1을 통해 확인). 둘째, 서버도 통신을 시작할 준비가 되었으며, 서버의 초기 순서 번호가 y라는 것. 이제 클라이언트는 마지막으로 서버의 연결 요청을 수락했다는 확인 메시지를 보내야 합니다.
클라이언트는 ACK 플래그가 1로 설정된 패킷을 서버에 전송합니다. 이 패킷의 순서 번호는 자신이 이전에 보냈던 번호에 1을 더한 x+1이 됩니다. 그리고 확인 번호 필드에는 서버가 보낸 순서 번호(y)에 1을 더한 값(y+1)을 담아 보냅니다. 이는 "당신이 보낸 y번 패킷까지 잘 받았고, 다음에는 y+1번 패킷을 보내주세요"라는 의미입니다.
Client -> Server TCP Packet Flags: SYN=0, ACK=1 Sequence Number: x + 1 Acknowledgement Number: y + 1
이 마지막 ACK 패킷을 보낸 클라이언트는 즉시 ESTABLISHED 상태로 전환되어 서버와 데이터를 주고받을 수 있는 준비를 마칩니다. 서버 역시 이 ACK 패킷을 수신하면 ESTABLISHED 상태로 전환됩니다. 이로써 양측 모두 통신 준비가 완료되었음을 상호 확인했으며, 비로소 신뢰성 있는 양방향 데이터 전송이 시작될 수 있습니다.
전체 과정을 도식화하면 다음과 같습니다.
Client Server
(CLOSED) (LISTEN)
| |
| -------- SYN (seq=x) -------------------> |
| |
(SYN_SENT) (SYN_RECEIVED)
| |
| <------- SYN/ACK (seq=y, ack=x+1) -------- |
| |
| -------- ACK (seq=x+1, ack=y+1) -------> |
| |
(ESTABLISHED) (ESTABLISHED)
왜 3-Way Handshake가 필요한가? 2-Way의 한계
여기서 한 가지 근본적인 질문을 던질 수 있습니다. "왜 굳이 세 단계를 거쳐야 할까? 두 단계, 즉 클라이언트가 SYN을 보내고 서버가 ACK으로 응답하면 충분하지 않을까?" 이는 네트워크 프로토콜 설계의 핵심을 꿰뚫는 매우 중요한 질문입니다.
만약 2-Way Handshake (SYN -> ACK) 방식을 사용한다고 가정해 봅시다. 클라이언트가 서버에 SYN 패킷을 보냈는데, 이 패킷이 네트워크 혼잡 등의 이유로 서버에 늦게 도착하는 '지연'이 발생할 수 있습니다. 클라이언트는 응답이 오지 않자 연결 요청이 실패했다고 판단하고, 새로운 SYN 패킷을 다시 보냅니다. 이 두 번째 요청은 정상적으로 처리되어 서버와 연결이 수립되고 데이터 통신 후 정상적으로 종료되었습니다.
그런데 여기서 문제가 발생합니다. 네트워크 어딘가에 떠돌던 첫 번째의 '오래된' SYN 패킷이 뒤늦게 서버에 도착하는 상황입니다. 2-Way Handshake 방식에서는 서버가 SYN 패킷을 받으면 무조건 연결을 수립하고 ACK으로 응답하게 됩니다. 서버는 이것이 오래된 요청인지 알 방법이 없습니다. 따라서 서버는 새로운 연결이 요청된 것으로 착각하고, 해당 연결을 위한 자원(메모리, 소켓 등)을 할당합니다. 하지만 정작 클라이언트는 이미 통신을 마쳤기 때문에 이 연결에 대해 아무것도 모르고, 서버가 보낸 ACK은 무시됩니다. 결국 서버는 사용되지도 않는 연결을 위해 불필요한 자원을 낭비하게 되며, 이런 '반쪽 짜리 연결(Half-Open Connection)'이 많이 쌓이면 서버의 자원이 고갈되어 정상적인 서비스를 제공할 수 없게 됩니다.
3-Way Handshake는 바로 이 문제를 해결합니다. 서버가 SYN-ACK를 보냈을 때, 클라이언트로부터 마지막 ACK 응답이 와야만 연결이 최종적으로 확립됩니다. 만약 오래된 SYN 패킷이 서버에 도착하여 서버가 SYN-ACK를 보내더라도, 현재 클라이언트는 연결을 요청한 적이 없으므로 마지막 ACK를 보내지 않습니다. 서버는 일정 시간 동안 ACK를 기다리다가 응답이 없으면 해당 연결 요청이 비정상적이라고 판단하고 할당했던 자원을 회수합니다. 이처럼 클라이언트의 최종 확인 절차를 통해 '유령' 연결이 생성되는 것을 방지하고, 양측의 연결 의사를 명확하게 확인할 수 있는 것입니다.
3-Way Handshake와 보안: SYN Flooding 공격
3-Way Handshake의 설계는 신뢰성을 확보하는 데 매우 효과적이지만, 역설적으로 이 구조를 악용한 서비스 거부(Denial of Service, DoS) 공격이 가능합니다. 가장 대표적인 공격이 바로 SYN Flooding입니다.
공격 원리는 Handshake의 2단계와 3단계 사이의 허점을 이용하는 것입니다. 서버는 2단계에서 SYN-ACK 패킷을 보낸 후 SYN_RECEIVED 상태로 전환되어 클라이언트의 마지막 ACK를 기다린다고 했습니다. 이때 서버는 해당 연결 요청 정보를 'Backlog Queue'라는 임시 저장 공간에 보관합니다. 만약 악의적인 공격자가 이 Backlog Queue를 가득 채워버리면 어떻게 될까요?
SYN Flooding 공격의 순서는 다음과 같습니다.
- 공격자는 출발지 IP 주소를 위조하여 수많은 SYN 패킷을 공격 대상 서버로 전송합니다. 출발지 IP를 존재하지 않는 주소나 공격과 무관한 제3의 주소로 위조하기 때문에 서버가 보내는 SYN-ACK 응답은 절대로 공격자에게 도달하지 않습니다.
- 서버는 각 SYN 요청에 대해 SYN-ACK 패킷으로 응답하고, Backlog Queue에 해당 연결 정보를 저장한 뒤
SYN_RECEIVED상태로 대기합니다. - 하지만 위조된 IP 주소 때문에 서버는 영원히 마지막 ACK 패킷을 받을 수 없습니다.
- 공격자가 이 과정을 반복하여 대량의 SYN 패킷을 보내면, 서버의 Backlog Queue는 응답을 기다리는 '반쪽 짜리 연결'들로 가득 차게 됩니다.
- 결국 Backlog Queue가 꽉 차면, 서버는 더 이상 새로운 연결 요청을 받아들일 수 없게 됩니다. 이때부터 정상적인 사용자가 서버에 접속을 시도해도 서버가 응답하지 못하는 서비스 거부 상태에 빠지게 됩니다.
이것은 마치 레스토랑에서 허위로 예약 전화만 계속 걸어 모든 테이블을 예약 상태로 만들어 놓고, 정작 실제 손님들은 자리가 없어 들어오지 못하게 만드는 것과 같습니다. 이러한 공격에 대응하기 위해 다양한 방어 기법들이 개발되었습니다.
- Backlog Queue 크기 늘리기: 가장 단순한 방법이지만, 공격의 규모가 크면 근본적인 해결책이 될 수 없습니다. _
- SYN_RECEIVED 상태의 Timeout 시간 줄이기: 서버가 ACK를 기다리는 시간을 줄여 Backlog Queue가 더 빨리 비워지도록 하는 방법입니다. 하지만 너무 짧게 설정하면 정상적인 사용자의 연결까지 끊어질 수 있습니다. _
- SYN Cookies: 가장 효과적인 방어 기법 중 하나입니다. SYN Cookies는 서버가
SYN_RECEIVED상태에서 연결 정보를 Backlog Queue에 저장하는 대신, 연결에 필요한 중요 정보(클라이언트 IP/Port, 서버 IP/Port 등)를 암호화하여 서버 자신의 ISN(순서 번호)에 숨겨서 클라이언트에게 SYN-ACK로 보내는 방식입니다. 정상적인 클라이언트라면 이 정보를 담은 ACK(Acknowledgement Number: 서버가 보낸 ISN+1)를 다시 보내올 것이고, 서버는 이 ACK 패킷의 확인 번호를 복호화하여 연결 정보를 복원할 수 있습니다. 즉, 마지막 ACK가 도착하기 전까지는 서버가 어떠한 상태 정보도 저장하지 않으므로 Backlog Queue가 고갈될 일이 없습니다.
실제 시스템에서 연결 상태 확인하기
이러한 TCP 연결 상태는 우리 컴퓨터에서도 직접 확인할 수 있습니다. 리눅스, macOS, 윈도우 등 대부분의 운영체제는 netstat이라는 네트워크 상태 확인 명령어를 제공합니다.
터미널이나 명령 프롬프트에서 다음 명령어를 입력하면 현재 시스템의 모든 TCP 연결 상태를 확인할 수 있습니다.
netstat -nat
-n은 주소를 이름 대신 숫자로 표시하는 옵션이고, -a는 모든 소켓을 표시, -t는 TCP 연결만 표시하라는 의미입니다. 이 명령어를 실행하면 다음과 유사한 출력을 볼 수 있습니다.
| Proto | Local Address | Foreign Address | State |
|---|---|---|---|
| tcp | 0.0.0.0:22 | 0.0.0.0:* | LISTEN |
| tcp | 192.168.1.10:54321 | 172.217.25.142:443 | ESTABLISHED |
| tcp | 192.168.1.10:12345 | 203.0.113.10:80 | SYN_SENT |
| tcp | 127.0.0.1:8080 | 127.0.0.1:56789 | TIME_WAIT |
위 표에서 `State` 열을 보면 LISTEN, ESTABLISHED, SYN_SENT 등 우리가 살펴본 TCP 상태들을 직접 눈으로 확인할 수 있습니다. 웹 브라우저로 새로운 사이트에 접속하는 순간 이 명령어를 실행하면, 아주 짧은 순간이지만 `SYN_SENT` 상태의 연결을 포착할 수도 있습니다. 서버 관리자라면 SYN_RECEIVED 상태의 연결이 비정상적으로 많은지 확인함으로써 SYN Flooding 공격의 징후를 파악할 수도 있습니다.
더욱 정밀한 분석을 원한다면 와이어샤크(Wireshark)와 같은 패킷 분석 도구를 사용하여 네트워크를 오가는 실제 SYN, SYN-ACK, ACK 패킷과 그 내용을 직접 들여다볼 수도 있습니다. 이를 통해 각 패킷의 순서 번호와 확인 번호가 어떻게 변화하는지 명확하게 이해할 수 있습니다.
연결의 시작과 끝: 4-Way Handshake와의 비교
연결을 수립하는 과정이 3-Way Handshake라면, 연결을 종료하는 과정은 4-Way Handshake를 통해 이루어집니다. 이 둘을 비교하면 TCP 설계의 정교함을 더욱 깊이 이해할 수 있습니다.
연결 종료 과정이 네 단계인 이유는, 한쪽이 "이제 보낼 데이터가 없다"(FIN)고 선언하더라도, 다른 쪽은 아직 보낼 데이터가 남아있을 수 있기 때문입니다. TCP는 양방향 통신이므로, 각 방향의 데이터 전송이 독립적으로 종료되어야 합니다.
간략한 4-Way Handshake 과정은 다음과 같습니다.
- (FIN from Client) 클라이언트가 데이터 전송을 마치고 서버에게 FIN 패킷을 보냅니다. "나는 더 이상 보낼 데이터가 없어."
- (ACK from Server) 서버는 클라이언트의 FIN을 잘 받았다는 의미로 ACK 패킷을 보냅니다. 하지만 서버는 아직 클라이언트에게 보낼 데이터가 남아있을 수 있으므로, 바로 연결을 닫지 않고 데이터를 마저 보냅니다.
- (FIN from Server) 서버도 모든 데이터 전송을 마치면, 이제 자신도 연결을 종료하겠다는 의미로 FIN 패킷을 클라이언트에게 보냅니다.
- (ACK from Client) 클라이언트는 서버의 FIN을 잘 받았다는 마지막 ACK 패킷을 보냅니다. 이 ACK를 받은 서버는 연결을 완전히 닫습니다. 클라이언트는 혹시 모를 네트워크 지연을 고려해 일정 시간(
TIME_WAIT상태)을 기다린 후 연결을 닫습니다.
이처럼 연결 시작 시에는 서버의 응답(ACK)과 동기화 요청(SYN)이 하나의 패킷으로 결합될 수 있지만, 종료 시에는 상대방의 데이터 전송이 끝날 때까지 기다려야 하므로 ACK와 FIN이 별도의 패킷으로 분리되어 네 단계가 되는 것입니다. 이는 데이터의 유실 없는 안전한 종료를 보장하기 위한 TCP의 배려입니다.
결론: 신뢰의 첫걸음
TCP 3-Way Handshake는 단순히 연결을 시작하는 신호 교환을 넘어, 불확실한 네트워크 환경 위에서 신뢰성 있는 통신을 구축하기 위한 정교하고 논리적인 약속입니다. 각 단계는 양측의 통신 의사와 준비 상태를 명확히 확인하고, 데이터 전송에 필수적인 초기 순서 번호를 동기화하며, 잠재적인 오류(오래된 패킷으로 인한 연결)를 방지하는 핵심적인 역할을 수행합니다.
SYN Flooding과 같은 보안 취약점이 존재하기도 하지만, 이는 그만큼 이 과정이 현대 인터넷 통신의 중추적인 역할을 담당하고 있다는 반증이기도 합니다. 우리가 매일같이 사용하는 웹 브라우징, 이메일, 온라인 게임 등 수많은 서비스의 안정성은 바로 이 보이지 않는 세 번의 '악수'로부터 시작됩니다. 3-Way Handshake에 대한 이해는 네트워크의 동작 원리를 파악하고, 문제 발생 시 원인을 진단하는 데 있어 개발자와 엔지니어에게 필수적인 기초 지식이라 할 수 있습니다.
Post a Comment