기본 콘텐츠로 건너뛰기

TCP 3-Way Handshake는 어떻게 연결을 만드는가

TCP 3-Way Handshake는 어떻게 연결을 만드는가

빠른 답

  • 3-way handshake는 양쪽이 서로 송수신 가능하다는 사실과 초기 순서 번호를 확인하는 연결 설정 절차입니다.
  • SYN은 연결 요청, SYN-ACK은 요청 수락과 서버의 초기 번호 전달, ACK는 클라이언트의 최종 확인입니다.
  • 연결이 안 될 때는 애플리케이션 코드보다 먼저 포트 리슨 상태, 방화벽, 라우팅, SYN 재전송 여부를 확인해야 합니다.
  • tcpdump나 Wireshark 출력에서 SYN만 반복되면 서버 응답이 없거나 중간 네트워크에서 막힌 상황일 가능성이 큽니다.

시간 흐름으로 이해하기

연결 요청 전
DNS 조회나 설정을 통해 대상 IP와 포트가 정해집니다.
첫 번째 왕복 전
클라이언트가 SYN 으로 연결 의사와 초기 순서 번호를 보냅니다.
서버 응답 시점
서버가 포트를 열고 있다면 SYN-ACK 로 요청 수락과 서버의 초기 순서 번호를 돌려줍니다.
최종 확인 시점
클라이언트가 ACK 를 보내면 양쪽 TCP 상태가 ESTABLISHED 로 바뀝니다.
데이터 전송 이후
HTTP, 데이터베이스, gRPC 같은 애플리케이션 프로토콜 데이터가 TCP 연결 위에서 오갑니다.

흐름으로 보기

흐름 다이어그램
TCP 3-Way Handshake는 어떻게 연결을 만드는가 흐름 다이어그램

이 흐름은 브라우저, API 클라이언트, 백엔드 서비스, 데이터베이스 클라이언트 모두에서 같은 구조로 나타납니다. 차이가 있다면 80, 443, 5432, 6379처럼 대상 포트와 그 위에서 동작하는 애플리케이션 프로토콜이 달라진다는 점입니다.

ESTABLISHED 상태가 되었다는 말은 TCP 연결이 만들어졌다는 뜻입니다. TLS 인증서 검증, HTTP 라우팅, 데이터베이스 인증, 애플리케이션 권한 검사는 그 이후의 단계입니다. 그래서 장애를 볼 때는 “TCP 연결이 안 되는가”와 “연결 이후 요청 처리가 실패하는가”를 분리해서 보는 것이 좋습니다.

TCP 연결 설정이 필요한 이유

TCP는 UDP처럼 데이터를 바로 보내는 방식이 아니라 연결을 먼저 만들고 그 위에서 바이트 스트림을 주고받습니다. 이 연결은 단순히 서버가 살아 있다는 신호가 아닙니다. 양쪽이 서로 패킷을 주고받을 수 있는지, 이후 데이터의 순서를 어떤 번호부터 계산할지, 수신 버퍼는 어느 정도인지 같은 조건을 맞추는 과정입니다.

TCP는 데이터 순서 보장, 손실 복구, 중복 제거, 흐름 제어를 제공합니다. 이런 기능을 위해서는 “상대가 내 첫 패킷을 받았는지”와 “상대가 보낸 첫 번호를 내가 확인했는지”가 필요합니다. 3-way handshake의 세 단계는 이 확인을 최소한의 메시지 교환으로 수행합니다.

운영 관점에서는 handshake가 애플리케이션 앞단의 경계선 역할을 합니다. 예를 들어 HTTP 500은 애플리케이션 내부 예외일 가능성이 있지만, SYN이 반복되고 SYN-ACK이 돌아오지 않는다면 서버 프로세스, 포트 바인딩, 보안 그룹, OS 방화벽, 라우팅 같은 네트워크 구간을 먼저 살펴봐야 합니다.

Sequence Number와 ACK Number

Sequence Number는 내가 보내는 바이트 스트림이 어느 번호에서 시작하는지를 나타냅니다. 클라이언트와 서버는 연결마다 각자의 초기 순서 번호를 고릅니다. 이 번호는 이후 데이터 재전송, 순서 보정, 중복 감지의 기준이 됩니다.

ACK Number는 마지막으로 받은 번호가 아니라 다음에 받을 것으로 기대하는 번호입니다. SYN은 실제 데이터가 없어도 순서 번호 공간을 1만큼 사용합니다. 그래서 서버는 클라이언트의 SYN을 받으면 ACK = 클라이언트 초기 순서 번호 + 1로 응답합니다. 클라이언트도 서버의 SYN에 대해 같은 방식으로 ACK = 서버 초기 순서 번호 + 1을 보냅니다.

Client -> Server: SYN, seq=1000
Server -> Client: SYN-ACK, seq=7000, ack=1001
Client -> Server: ACK, seq=1001, ack=7001

이 예시에서 클라이언트의 초기 번호가 1000이면 서버는 1001을 기대한다고 응답합니다. 서버의 초기 번호가 7000이면 클라이언트는 7001을 기대한다고 확인합니다. 패킷 캡처에서 숫자가 +1로 움직이는 이유가 여기에 있습니다.

3-way handshake가 끝났다고 애플리케이션 요청까지 성공한 것은 아닙니다. TCP 연결은 만들어졌지만 TLS handshake에서 인증서 오류가 날 수 있고, HTTP 요청은 404나 500을 받을 수 있습니다. 반대로 애플리케이션 로그가 전혀 남지 않는다면 요청이 애플리케이션 레이어까지 오기 전에 끊겼을 가능성을 확인해 볼 만합니다.

서버 포트와 배포 구성

TCP 연결이 만들어지려면 서버 프로세스가 대상 IP와 포트에서 리슨하고 있어야 합니다. 0.0.0.0:8080에 바인딩하면 외부 인터페이스로 들어오는 8080 포트 연결을 받을 수 있습니다. 반면 127.0.0.1:8080에만 바인딩하면 같은 서버 내부에서는 접속되지만 외부 호스트에서는 접속할 수 없습니다.

리버스 프록시나 로드 밸런서를 앞에 두면 TCP 연결 지점이 하나 더 생깁니다. 클라이언트는 로드 밸런서의 443 포트에 연결하고, 로드 밸런서는 내부 애플리케이션 서버의 8080 포트로 다시 연결할 수 있습니다. 이때 외부 방화벽은 443을 허용해야 하고, 내부 보안 그룹이나 OS 방화벽은 로드 밸런서에서 애플리케이션 서버로 가는 8080을 허용해야 합니다.

아래 구성은 외부 HTTP 요청을 내부 애플리케이션 포트로 넘기는 간단한 예입니다.

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

이 구성에서는 클라이언트와 Nginx 사이에 한 번, Nginx와 애플리케이션 서버 사이에 또 한 번 TCP handshake가 일어납니다. 외부에서는 접속되는데 애플리케이션 로그가 비어 있다면 프록시 이후 구간의 upstream 주소, 포트, 로컬 바인딩, 내부 방화벽을 따로 확인해야 합니다.

연결 확인을 위한 최소 예시

가장 작은 단위로 TCP 연결을 확인하려면 특정 포트에서 리슨하는 서버를 띄운 뒤 클라이언트에서 접속해 볼 수 있습니다. 아래 Python 코드는 8080 포트에서 TCP 연결을 받고 짧은 응답을 보냅니다.

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 8080))
server.listen(5)

print("listening on 0.0.0.0:8080")

while True:
    conn, addr = server.accept()
    print("accepted", addr)
    conn.sendall(b"hello\n")
    conn.close()

서버를 실행한 뒤 다른 터미널에서 접속하면 TCP 연결 가능 여부를 확인할 수 있습니다. nc는 애플리케이션 프로토콜을 깊게 타지 않고 포트 접근성을 확인할 때 유용합니다.

$ ss -lntp | grep ':8080'
LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("python",pid=2418,fd=3))

$ nc -vz 127.0.0.1 8080
Connection to 127.0.0.1 8080 port [tcp/http-alt] succeeded!

$ nc 127.0.0.1 8080
hello

이 출력은 해당 IP와 포트까지 TCP 연결이 가능하다는 뜻입니다. 다만 HTTP API가 정상이라는 의미는 아닙니다. TCP 포트 확인과 애플리케이션 응답 확인은 서로 다른 검사입니다.

패킷 캡처로 보는 정상 handshake

패킷 캡처는 TCP handshake를 직접 확인할 수 있는 방법입니다. 서버나 클라이언트에서 tcpdump를 실행한 뒤 접속을 시도하면 SYN, SYN-ACK, ACK 순서가 보입니다.

$ sudo tcpdump -nn -i eth0 'tcp port 8080'
10:15:01.120001 IP 10.0.1.25.51544 > 10.0.2.10.8080: Flags [S], seq 1000, win 64240
10:15:01.120420 IP 10.0.2.10.8080 > 10.0.1.25.51544: Flags [S.], seq 7000, ack 1001, win 65160
10:15:01.120731 IP 10.0.1.25.51544 > 10.0.2.10.8080: Flags [.], ack 7001, win 64240

첫 줄은 클라이언트가 서버 8080 포트로 SYN을 보낸 장면입니다. 둘째 줄은 서버가 SYN-ACK로 응답한 장면입니다. 셋째 줄은 클라이언트가 최종 ACK를 보낸 장면입니다. 세 줄이 짧은 시간 안에 이어진다면 TCP 연결 설정 자체는 성공한 것으로 볼 수 있습니다.

애플리케이션 로그에는 요청이 없는데 패킷 캡처에서 handshake가 보인다면 연결 이후 단계를 확인해야 합니다. 예를 들어 TLS를 기대하는 포트에 평문 HTTP를 보내고 있거나, 프록시의 upstream 경로가 잘못되었거나, 애플리케이션이 연결 직후 요청을 닫고 있을 수 있습니다.

장애 징후와 점검 순서

연결 실패 메시지는 비슷해 보여도 의미가 다릅니다. Connection refused는 대상 호스트까지 도달했지만 해당 포트에서 받아 주는 프로세스가 없거나 중간 장비가 거절 응답을 보낸 경우에 자주 나타납니다.

Connection timed outSYN을 보냈지만 응답을 받지 못했을 때 많이 보입니다. 방화벽, 보안 그룹, 네트워크 ACL, 라우팅, 서버 다운 상태처럼 응답이 돌아오지 않는 구간을 확인해야 합니다.

No route to host는 목적지로 가는 경로가 없거나 네트워크 계층에서 도달할 수 없다는 신호에 가깝습니다. 이 경우에는 애플리케이션 설정보다 라우팅 테이블, 서브넷, VPN, 피어링, 보안 정책을 먼저 보는 편이 원인에 더 가깝습니다.

$ nc -vz 10.0.2.10 8080
nc: connect to 10.0.2.10 port 8080 (tcp) failed: Connection refused

$ nc -vz -w 3 10.0.2.10 8080
nc: connect to 10.0.2.10 port 8080 (tcp) timed out: Operation now in progress

$ sudo tcpdump -nn 'host 10.0.2.10 and tcp port 8080'
10:21:33.001 IP 10.0.1.25.51610 > 10.0.2.10.8080: Flags [S], seq 392812, win 64240
10:21:34.026 IP 10.0.1.25.51610 > 10.0.2.10.8080: Flags [S], seq 392812, win 64240
10:21:36.074 IP 10.0.1.25.51610 > 10.0.2.10.8080: Flags [S], seq 392812, win 64240

마지막 캡처처럼 같은 seqSYN이 반복되고 SYN-ACK가 없다면 클라이언트의 요청이 서버에 도달하지 못했거나, 서버 응답이 클라이언트로 돌아오지 못하는 상황을 의심할 수 있습니다.

운영 점검은 다음 순서로 나누면 범위를 줄이기 쉽습니다.

  • DNS가 기대한 IP로 해석되는지 확인합니다.
  • 서버 프로세스가 실행 중인지 확인합니다.
  • 서버가 올바른 IP와 포트에 리슨하는지 확인합니다.
  • 서버 로컬에서 127.0.0.1 또는 내부 IP로 접속해 봅니다.
  • 같은 내부망의 다른 호스트에서 접속해 봅니다.
  • 클라이언트와 서버 양쪽에서 패킷을 캡처해 SYNSYN-ACK의 위치를 비교합니다.
  • 로드 밸런서, 보안 그룹, OS 방화벽, 라우팅 테이블을 연결 경로 순서대로 확인합니다.
  • TCP 연결은 성공하지만 요청이 실패한다면 TLS, Host 헤더, 프록시 upstream, 애플리케이션 로그를 확인합니다.

흔한 오해

ping이 된다고 TCP 연결까지 된다고 볼 수는 없습니다. ping은 ICMP를 사용하고, TCP 포트 리슨 여부와 직접 연결되지 않습니다. 어떤 환경은 ICMP를 막아 ping은 실패하지만 TCP 443은 열려 있을 수 있습니다. 반대로 ping은 되지만 TCP 8080은 방화벽에 막혀 있을 수도 있습니다.

TCP handshake 성공도 서비스 정상 동작을 보장하지 않습니다. 연결은 만들어졌지만 TLS 인증서가 만료되었을 수 있고, HTTP 라우팅이 맞지 않을 수 있으며, 데이터베이스 인증이나 애플리케이션 권한 검사에서 실패할 수 있습니다.

문제를 나눠 볼 때는 계층을 분리하는 편이 도움이 됩니다. 먼저 목적지 IP까지 경로가 있는지 확인하고, 다음으로 대상 TCP 포트가 열려 있는지 봅니다. 그다음 SYN, SYN-ACK, ACK가 오가는지 확인합니다. 이후 TLS handshake, HTTP 응답, 데이터베이스 프로토콜 응답, 애플리케이션 로그를 차례로 이어 보면 “서버가 안 된다”는 넓은 문제를 더 작은 단서로 바꿀 수 있습니다.

원문 참고

https://www.maeil-mail.kr/question/76

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...