Ctrl + Shift + ESC

와이어샤크로 TCP Handshake 확인하기 본문

Network

와이어샤크로 TCP Handshake 확인하기

단축키실행해보세요 2025. 11. 12. 16:21

실습 동기

https://github.com/esc-beep/TIL/blob/main/Network/4.%EC%A0%84%EC%86%A1-%EA%B3%84%EC%B8%B5.md

 

TIL/Network/4.전송-계층.md at main · esc-beep/TIL

Today I Learned. Contribute to esc-beep/TIL development by creating an account on GitHub.

github.com

전송 계층의 내용 중 TCP Handshake는 매우 중요하게 다뤄지는 부분이기 때문에 꼭 패킷을 직접 확인해보겠다는 생각을 했다.

 

사전 준비

import socket
import datetime

HOST = '127.0.0.1'
PORT = 13

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen(1)
    print(f"Daytime server is running on {HOST}:{PORT} ...")

    while True:
        conn, addr = s.accept()
        with conn:
            print(f"Connected by {addr}")
            current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            conn.sendall(current_time.encode())

 

다음과 같은 코드를 sudo python3 daytime_server.py 로 실행한다.

포트 13은 0번부터 1023번 사이의 잘 알려진 포트 범위에 속하므로, 해당 포트를 사용하기 위해 루트 권한이 필요하다.

 

이러한 루트 권한을 en0(외부 네트워크 장치)에 sudo 권한으로 열어두면 위험할 수 있을 것 같아 host를 127.0.0.1, 루프백으로 열어두었다. 루프백으로 실행했기 때문에 wireshark 캡처는 이전처럼 en0이 아닌 lo0에서 캡처해야 한다.

 

python 파일을 실행한 뒤 wireshark 터미널을 하나 더 열어서 nc 127.0.0.1 13 명령어를 실행하면 위와 같이 DAYTIME 프로토콜을 확인할 수 있다.

여기서 127.0.0.1의 13번 포트와 62681번 포트가 루프백 통신을 한 것을 확인할 수 있다. (파이썬 코드를 실행한 터미널에서도 확인할 수 있다.)

 

이제부터 전체 TCP 통신 과정을 하나씩 살펴보자.

 

TCP 3-way handshake

TCP 연결 설정은 3-way handshake의 세 단계를 거친다.

1단계: 클라이언트 → 서버: SYN

1단계: 클라이언트 → 서버: SYN

 

먼저, 클라이언트가 서버로 TCP 연결을 요청(SYN)한 것을 확인할 수 있다.

클라이언트가 서버로 보낸 세그먼트 시작 번호인 시퀀스 넘버의 경우 보안상의 이유로 OS가 랜덤하게 생성한 799646334가 배정된 것을 확인할 수 있다.

Wireshark는 사람이 보기 편하도록 상대 시퀀스 번호를 자동 보정해서 보여준다. 따라서 시퀀스 번호는 이해하기 쉽도록 0으로 생각한다.

데이터의 경우 전송 데이터가 없으므로 0바이트이다.

 

2단계: 서버 → 클라이언트: SYN+ACK

2단계: 서버 → 클라이언트: SYN+ACK

 

서버도 클라이언트에 연결 요청(SYN)을 하고, 그와 동시에 클라이언트로부터 받은 연결 요청을 수락(ACK)한다.

서버가 클라이언트로 보낸 세그먼트 시작 번호 또한 랜덤하게 생성된 2662166153이지만 이해하기 쉽도록 상대 시퀀스 번호인 0으로 생각한다.

클라이언트로부터 받은 연결 요청(SYN) 세그먼트는 시퀀스 번호 하나를 소비하므로 ACK 번호는 1을 더한 1이 된다.

전송 데이터가 없으므로 0바이트이다.

 

3단계: 클라이언트 → 서버: ACK

3단계: 클라이언트 → 서버: ACK

 

클라이언트가 서버의 연결 요청을 수락(ACK)했다.

클라이언트는 1단계에서 SYN 세그먼트를 보내며 시퀀스 번호 하나를 소비했다. 따라서 다음 세그먼트의 시퀀스 번호를 1 증가한다.

서버로부터 받은 SYN+ACK 세그먼트 역시 시퀀스 번호 하나를 소비하므로 다음에 받기를 기대하는 ACK 번호는 이전 시퀀스 번호에 1을 더한 1이 된다.

여전히 전송 데이터는 0바이트이다.

 

이렇게 3-handshake 과정을 통해 양쪽 모두 통신할 준비가 완료되었다.

 

데이터 전송 및 확인

서버에서 클라이언트로 데이터 전송을 하기 전에 Window Update가 되었다.

실제로 확인해보니 Window size는 동일했고, 버퍼 상태를 맞췄다 / 타이머를 초기화했다는 의미의 상태 동기화용 제어 ACK에 가까웠다.

4단계: 서버 → 클라이언트: 데이터 전송

4단계: 서버 → 클라이언트: 데이터 전송

서버가 클라이언트로 데이터를 전송하는 Daytime 프로토콜이다.

TCP는 신뢰성 있는 전송을 보장하기 위해 모든 데이터에 대해 ACK를 보낸다. 따라서 ACK 프로토콜과 데이터를 즉시 애플리케이션에 전달하라는 PSH 플래그가 설정된 것을 확인할 수 있다.

2단계의 SYN+ACK에서 시퀀스 번호를 하나 소비했기 때문에 시퀀스 번호는 1이다.

데이터가 없는 순수 ACK 세그먼트는 응답만 하기 때문에 시퀀스 번호를 소비하지 않는다. 따라서 서버는 클라이언트에 다음에 받길 원하는 ACK 번호를 1로 유지한다.

20바이트 크기의 날짜와 시간 데이터를 전송한 것을 확인할 수 있다.

 

5단계: 클라이언트 → 서버: ACK

5단계: 클라이언트 → 서버: ACK

 

클라이언트는 Daytime 데이터를 잘 받았다고 응답(ACK)한다.

3단계에서 클라이언트가 보낸 ACK 세그먼트는 데이터가 없는 순수 ACK 세그먼트이므로 4단계와 동일하게 시퀀스 번호는 증가하지 않고 1로 유지한다.

ACK 번호는 4단계에서 서버가 보낸 시퀀스 번호에 데이터의 크기를 더한다. 이는 20바이트까지 잘 받았고, 다음 시퀀스 번호로 21을 기대한다는 의미이다.

전송 데이터는 없으므로 0바이트이다.

TCP 4-way handshake

TCP 연결 종료는 4-way handshake의 네 단계를 거친다.

6단계: 서버 → 클라이언트: FIN

6단계: 서버 → 클라이언트: FIN

 

서버는 더 이상 보낼 데이터가 없으므로 클라이언트에 연결 종료 요청(FIN)을 한다.

4단계에서 서버는 20바이트 데이터를 전송했기 때문에 다음 세그먼트의 시퀀스 번호는 이전 시퀀스 번호에 20을 더한 21이다.

5단계에서 클라이언트가 보낸 ACK 세그먼트는 데이터가 없는 순수 ACK 세그먼트이므로 시퀀스 번호가 증가하지 않는다.

전송 데이터 또한 없으므로 0바이트이다.

 

눈썰미가 좋은 사람이라면 5단계의 패킷 프레임이 115이고, 6단계의 패킷 프레임이 114인 것을 보았을 수도 있다.

이는 Daytime 프로토콜은 매우 짧은 단방향 전송이라, 데이터 전송과 연결 종료가 거의 동시에 일어났기 때문이다.

서버가 데이터를 보내고 클라이언트가 ACK를 보내기 전에 서버가 FIN을 보내 프레임 순서가 살짝 섞였다.

하지만 데이터가 FIN보다 먼저 도착했고, 처리되었기 때문에 재전송은 이루어지지 않았다.

7단계: 클라이언트 → 서버: ACK

7단계: 클라이언트 → 서버: ACK

 

서버의 연결 종료 요청을 클라이언트가 수락(ACK)한다.

5단계에서 클라이언트가 순수 ACK 세그먼트를 보냈기 때문에 시퀀스 번호는 증가하지 않고 1을 유지한다.

6단계에서 서버롵부터 받은 FIN 세그먼트는 시퀀스 번호 하나를 소비한다. 따라서 다음에 받기를 기다하는 ACK 번호는 이전 시퀀스 번호인 21에 1을 더한 22이다.

이는 21번 바이트까지 잘 받았고, 다음 시퀀스 번호로 22을 기대한다는 의미이다.

전송 데이터가 없으므로 0바이트이다.

 

8단계: 클라이언트 → 서버: FIN

8단계: 서버 → 클라이언트: FIN

 

클라이언트도 더 이상 보낼 데이터가 없다면 서버로 연결 종료 요청(FIN)을 한다.

7단계에서 클라이언트는 데이터가 없는 순수 ACK 세그먼트를 보냈으므로 시퀀스 번호는 증가하지 않고 1을 유지한다.

마찬가지로 ACK 번호도 22를 유지한다.

전송 데이터 또한 없으므로 0바이트이다.

 

9단계: 서버 → 클라이언트: ACK

9단계: 서버 → 클라이언트: ACK

 

서버는 클라이언트의 연결 종료 요청을 수락(ACK)한다.

6단계에서 서버는 FIN 세그먼트를 보냈기 때문에 시퀀스 번호 하나를 소비한다. 따라서 다음 세그먼트의 시퀀스 번호는 이전 시퀀스 번호에 1을 더한다.

8단계에서 클라이언트가 보낸 FIN 세그먼트는 시퀀스 번호를 하나 소비하므로 ACK 번호는 1 증가해 2가 된다.

전송 데이터가 없으므로 0바이트이다.

 

이렇게 클라이언트와 서버 간 TCP 연결이 종료되었다.