dev notes

DNS, TCP, TLS 통신에 대한 WireShark를 이용한 패킷 분석

2024-06-3011 min read
공유

개요#

처음에는 네트워크 프로그래밍을 코드로만 봤습니다. 소켓을 열고, 연결을 받고, 요청을 읽는 흐름은 익숙했는데 그 아래에서 실제 패킷이 어떻게 오가는지는 한 번도 눈으로 본 적이 없었습니다.

Vanilla Java로 HTTP Server를 직접 구현할 때 ServerSocket으로 클라이언트 연결을 받았는데, 그 아래에서 실제로 어떤 패킷이 오가는지 눈으로 본 적은 없었습니다. 코드상으로는 accept() 한 줄인데, 실제로는 DNS 쿼리 → TCP 핸드셰이크 → TLS 핸드셰이크라는 과정이 먼저 일어나야 합니다.

그래서 Wireshark로 www.naver.com 접속 흐름을 직접 캡처해서, 그 전 과정을 패킷 단위로 뜯어봤습니다.

  • DNS 쿼리와 응답
  • TCP 3way handshake (open)
  • TLS client hello & handshake
  • TCP 4way handshake (close)

DNS 쿼리#

DNS 쿼리 패킷 캡처 — www.naver.com A 레코드 요청

DNS 쿼리 패킷 캡처 — www.naver.com A 레코드 요청

DNS 쿼리는 RFC 1035 기반으로 동작합니다.

  • 프레임 541: 클라이언트가 DNS 서버에 www.naver.com의 A 레코드를 요청. IPv4 주소를 얻기 위함.
  • 프레임 542: 클라이언트가 DNS 서버에 www.naver.com의 HTTPS 레코드를 요청.
  • 프레임 543: DNS 서버가 HTTPS 레코드 요청에 응답. www.naver.com이 www.naver.com.nheos.com의 CNAME임을 알려줌.
  • 프레임 544: DNS 서버가 A 레코드 요청에 응답. 여러 A 레코드가 포함됨.

추가적으로 dns로의 접속 목적지는 현재 사용하고 있는 인터넷제공자인 LG 사의 네임서버였고 출발지는 내 pc의 ip가 아닌 홈라우터(wifi 공유기)의 ip가 할당되어있었습니다.

DNS 응답 — LG 네임서버 경유, 공유기 IP 확인

DNS 응답 — LG 네임서버 경유, 공유기 IP 확인

CNAME과 CDN

프레임 543에서 www.naver.com이 www.naver.com.nheos.com의 CNAME이라고 나왔습니다. 이건 네이버가 CDN을 사용하고 있기 때문입니다. nheos.com은 네이버의 CDN 도메인인데, CNAME으로 연결해두면 네이버 본 서버의 IP를 직접 노출하지 않으면서 CDN 엣지 서버로 트래픽을 분산시킬 수 있습니다.

A 레코드에 여러 IP가 포함된 것도 같은 이유입니다. 클라이언트의 지리적 위치에 따라 가까운 엣지 서버의 IP가 반환됩니다.

출발지 IP가 내 PC가 아닌 이유

출발지가 공유기 IP인 건 NAT(Network Address Translation) 때문입니다. 집에서 쓰는 사설 IP(192.168.x.x)는 인터넷으로 나갈 수 없고, 공유기가 자기 공인 IP로 바꿔서 보냅니다. 돌아오는 응답도 공유기가 받아서 내 PC로 포워딩해줍니다.

TCP 3-way handshake (open)#

TCP 3-way handshake (SYN → SYN-ACK → ACK)

TCP 3-way handshake (SYN → SYN-ACK → ACK)

  • 프레임 545 (SYN): 클라이언트가 서버에 TCP SYN 패킷을 보냄. 목적지 포트 443 (HTTPS), MSS: 1460.
  • 프레임 546 (SYN-ACK): 서버가 클라이언트에 TCP SYN-ACK 패킷을 보냄.
  • 프레임 547 (ACK): 클라이언트가 서버에 TCP ACK 패킷을 보내 3-way 핸드셰이크 완료.

MSS 1460의 의미

SYN 패킷에서 MSS(Maximum Segment Size)가 1460으로 잡혀 있습니다. (RFC 9293 - TCP) 이건 Ethernet의 MTU(Maximum Transmission Unit)인 1500바이트에서 IP 헤더(20바이트)와 TCP 헤더(20바이트)를 빼면 1460이 나오기 때문입니다. 한 번에 보낼 수 있는 데이터의 최대 크기를 협상하는 과정입니다.

Ethernet MTU: 1500 bytes
- IP Header:    20 bytes
- TCP Header:   20 bytes
= MSS:        1460 bytes (순수 데이터)

MSS보다 큰 데이터를 보내야 하면 TCP가 여러 세그먼트로 나눠서 보내고, 받는 쪽에서 재조립합니다.

TLS handshake (open)#

TLS 핸드셰이크 시작 (Client Hello)

TLS 핸드셰이크 시작 (Client Hello)

TLS 1.3 핸드셰이크 — SNI=www.naver.com

TLS 1.3 핸드셰이크 — SNI=www.naver.com

  • 프레임 549 (Client Hello): TLSv1.3, SNI=www.naver.com
  • 프레임 551 (Server Hello): Server Hello, Change Cipher Spec, Application Data
  • 프레임 556: 클라이언트가 Change Cipher Spec, Application Data 전송

TLS 1.3이 빠른 이유

캡처에서 TLSv1.3이 찍혔습니다. (RFC 8446 - TLS 1.3) TLS 1.2와 비교하면 핸드셰이크 라운드트립이 줄어든 게 보입니다.

TLS 1.2 (2-RTT):
  Client → ServerHello Request
  Server → ServerHello, Certificate, KeyExchange, Done
  Client → KeyExchange, ChangeCipherSpec, Finished
  Server → ChangeCipherSpec, Finished
  → 이제야 암호화된 데이터 전송 가능

TLS 1.3 (1-RTT):
  Client → ClientHello + KeyShare
  Server → ServerHello + KeyShare + EncryptedExtensions + Finished
  Client → Finished
  → 이제 암호화된 데이터 전송 가능

TLS 1.3은 Client Hello에 키 교환 정보(KeyShare)를 같이 보내기 때문에 서버가 첫 응답에서 바로 암호화 세팅을 완료할 수 있습니다. 1.2는 키 교환을 별도 라운드트립으로 해야 해서 한 번 더 왕복이 필요합니다.

실제로 캡처에서도 프레임 549(ClientHello) → 551(ServerHello + Finished)로 서버가 한 번에 응답을 끝내는 걸 볼 수 있습니다.

SNI가 필요한 이유

Client Hello에 SNI(Server Name Indication)로 www.naver.com이 찍혀 있는데, 이건 하나의 IP에 여러 도메인이 호스팅되는 경우가 많기 때문입니다. 서버가 어떤 도메인의 인증서를 보여줘야 하는지 알려면 클라이언트가 먼저 도메인명을 알려줘야 합니다.

CDN에서 특히 중요합니다. 네이버의 CDN 엣지 서버 하나가 수십 개 도메인을 처리하고 있을 수 있으니까요. SNI 없이는 어떤 인증서를 보여줄지 모릅니다.

TCP 4-way handshake (close)#

TCP 4-way handshake (FIN-ACK) 연결 종료

TCP 4-way handshake (FIN-ACK) 연결 종료

  • 프레임 12121 (FIN-ACK): 클라이언트가 서버에 TCP FIN-ACK 패킷을 보냄
  • 프레임 12123 (ACK): 서버의 FIN-ACK에 대한 응답
  • 프레임 12126 (FIN-ACK): 클라이언트 연결 종료 시도
  • 프레임 12127 (ACK): 서버가 연결 종료 완료

프레임 번호가 545(연결)에서 12121(종료)로 점프한 게 보입니다. 그 사이에 1만 개 이상의 프레임이 오갔다는 뜻인데, 이게 실제 HTML, CSS, JS, 이미지 등을 주고받은 데이터입니다. 연결 맺고 끊는 건 수 프레임이면 끝나지만, 실제 데이터 전송이 훨씬 많습니다.

ISN과 시퀀스 번호#

전체 통신 과정 요약 — 0.01초 이내 완료

전체 통신 과정 요약 — 0.01초 이내 완료

ISN 값 +1 ACK 응답 확인

ISN 값 +1 ACK 응답 확인

실제로 ISN으로 보낸 값에 +1을 해서 이를 ACK로 돌려보내는 과정도 확인할 수 있었습니다.

ISN(Initial Sequence Number)이 0이 아니라 랜덤한 큰 수인 이유는 보안 때문입니다. ISN이 예측 가능하면 공격자가 TCP 세션을 하이재킹할 수 있습니다. 랜덤한 ISN을 쓰면 제3자가 시퀀스 번호를 추측하기 어려워집니다.

ACK에서 +1을 하는 건 "이 번호까지 잘 받았으니 다음 번호를 보내라"는 뜻입니다. 시퀀스 번호 덕분에 패킷이 순서 없이 도착해도 원래 순서대로 재조립할 수 있고, 빠진 패킷이 있으면 재전송을 요청할 수 있습니다.

패킷을 직접 보니까 대부분의 과정이 0.01초 안에 지나간다는 점도 새삼 실감났습니다. 정보가 프레임으로 캡슐화되어 여러 라우터를 거쳐 다시 돌아오는 속도가 생각보다 훨씬 빨랐습니다.

코드로는 new ServerSocket(8080) 한 줄, accept() 한 줄처럼 보이는데 그 사이에 DNS 쿼리 4개, TCP 핸드셰이크 3개, TLS 핸드셰이크 3개가 오가고 있었습니다. 평소에는 프레임워크가 다 감춰주니까 잘 안 보이지만, 연결 지연이나 통신 실패를 디버깅할 때는 결국 이 레이어를 알아야 어디서 막혔는지 판단이 빨라진다는 걸 다시 느꼈습니다.

Connected Notes