도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #4

자손킴 @jasonkim@hackers.pub

L4 전송

‘애플리케이션 식별’과 ‘요구사항에 따른 전송 제어’를 통해 네트워크와 애플리케이션을 연결하는 계층.

네트워크 계층은 여러 네크워크를 넘어 목적지까지 패킷을 전달하는 것만 담당한다. 전송 계층에서는 포트 번호를 사용해 패킷을 전달할 애플리케이션을 식별하고 처리한다.

이 계층에서는 애플리케이션의 요구사항과 통신 상태에 따라 패킷의 송수신량을 조절하거나 전송 도중에 사라진 패킷을 재전송 하기도 한다.

포트 번호

포트 번호는 애플리케이션을 식별하는 2바이트(16비트) 숫자이다.

L2 에서는 MAC 주소를 사용해 장비를 식별하고, L3에서는 IP 주소를 사용해 네트워크 경로와 최종 목적지 호스트를 식별한 것 처럼 L4 에서는 포트 번호를 사용해서 애플리케이션을 식별한다.

포트의 종류

포트 번호는 목적에 따라 3가지로 구분된다.

  • System Ports
    • 0~1023 까지를 사용한다.
    • ICANN의 인터넷 자원 관리 기능인 IANA에서 관리하고 있으며 주로 일반적인 서버 애플리케이션에 고유하게 매핑되어 있다.
  • User Ports
    • 1024~49151 을 사용한다.
    • Registered Ports 라고도 부른다.
    • 주로 각 업체가 개발한 자체 애플리케이션에 고유하게 매핑되어있다.
  • Dynamic Ports
    • 49152~65535 를 사용한다.
    • Private Ports 라고도 한다.
    • 주로 클라이언트 애플리케이션이 서버 애플리케이션에 접속할 때 발신자 포트 번호로 무작위 할당하는데 사용한다.
    • 운영체제와 버전별로 범위가 약간씩 다르다.

UDP(User Datagram Protocol)

안정성보다 실시간성이 요구되는 애플리케이션에 주로 사용되는 프로토콜이다. 그렇기 때문에 연결 확인 과정과 확인 응답 처리등을 생략하고 일방적으로 데이터를 계속 전달한다.

UDP 패킷 형식

실시간성을 중요시 하기 때문에 패킷 형식이 필드 4개로 매우 단순하다. 필드는 순서대로 발신자 포트 번호, 목적지 포트 번호, UDP 데이터그램 길이, 체크섬이다. 각 필드는 모두 2바이트로 구성되어 있다. 따라서 필드 전체 크기는 8바이트이다.

데이터를 수신한 쪽에서는 UDP 데이터그램 길이와 체크섬 검증만 성공하면 데이터를 받아들인다.

TCP(Transmission Control Protocol)

데이터 전송에 대한 신뢰성을 요구하는 애플리케이션에서 주로 사용한다. 데이터 전송에 앞서 ‘TCP 커넥션’이라는 논리적 파이프를 구성하여 통신 환경을 구축한다. TCP 커넥션은 Full-Duplex, 즉 보내는 파이프와 받는 파이프를 분리하여 데이터 전송 여부를 확인하면서 데이터를 전송하기 때문에 신뢰성이 향상된다.

TCP 패킷 형식

안정성을 위해 UDP에 비해 훨씬 더 많은 필드를 사용한다. 이 필드들을 사용해 어떤 데이터를 받았는지 확인하고 패킷의 송수신량을 조절한다.

TCP는 데이터를 수신하면 어디까지 데이터를 받았는지 보낸 쪽에 알려줘야 한다. 그런데 매번 데이터를 받을때 마다 응답 패킷을 돌려보내면 속도가 너무 느려지게 된다. (이러한 방식을 Stop-and-Wait라 부르며 인터넷 초창기에 사용했다고 한다.) 그렇기 때문에 패킷을 연속적으로 보내고 적당한 크기까지는 한 번에 처리 하고 응답하는 방식(Sliding-window)을 사용한다. 이를 위해서 윈도우 크기를 나타내는 필드를 사용한다.

  • 발신자 포트 번호
    • 클라이언트에서는 OS가 정해진 범위(주로 Dynamic Ports)에서 무작위로 할당한다.
  • 목적지 포트 번호
    • 어떤 애플리케이션이 사용하는 포트인지 식별하여 데이터를 전달한다.
  • 시퀀스 번호
    • TCP 세그먼트를 올바른 순서로 정렬하는 데 사용한다.
    • 데이터를 보내는 쪽에서는 초기 시퀀스 번호(ISN, Initial Sequence Number)부터 바이트 단위로 순차적으로 일련 번호를 부여한다.
    • 시퀀스 번호는 3-way handshake 과정에서 임의의 값으로 초기화 된다.
    • 시퀀스 번호 필드의 크기는 4바이트 이기 때문에, 시퀀스 번호가 2^32를 넘어가면 0부터 다시 시작한다.
    • 데이터를 받는 쪽은 수신한 TCP 세그먼트의 시퀀스 번호를 확인하여 번호 순서대로 정렬하여 애플리케이션에 전달한다.
  • 확인 응답 번호(ACK, Acknowledge)
    • 제어비트의 ACK 플래그가 1일때만 사용된다.
    • 어디까지 데이터를 전달 받았는지 알리기 위해서 사용된다.
    • 전달받은 마지막 바이트의 시퀀스 번호에 1을 더해서 전송한다. 즉, ACK에 기록된 시퀀스 번호부터 보내주면 된다는 것을 알리는 것이다.
  • 데이터 오프셋
    • TCP 헤더 길이를 나타낸다. 옵션 필드가 있기 때문에 TCP 헤더의 길이는 가변적이다.
  • 제어 비트
    • 8비트 플래그로 구성 되어 있으며 현재 연결이 어떤 상태인지 상호 전달한다.
    • 1번 비트: CWR(Congestion Window Reduced)
      • ECN-Echo에 따라 혼잡 윈도우를 감소 시켰음을 알린다.
    • 2번 비트: ECE(ECN-Echo)
      • 혼잡이 발생했음을 상대방에게 보고
    • 3번 비트: URG(Urgent Pointer field significant)
      • 비상사태를 발생하였음
    • 4번 비트: ACK(Acknowledgement field significant)
      • 확인 응답을 표시
    • 5번 비트: PSH(Push Function)
      • 애플리케이션에 데이터를 신속하게 전달할 수 있는 플래그
    • 6번 비트: RST(Reset the connection)
      • 연결을 강제로 끊는 플래그
    • 7번 비트: SYN(Synchronize sequence numbers)
      • 연결을 여는 플래그
    • 8번 비트: FIN(No more data from sender)
      • 연결을 닫는 플래그
  • 윈도우 크기
    • 수신 가능한 데이터 크기를 알려주는 필드
    • 2비트로 구성되어 있기 때문에 최대 65535바이트까지 통지 가능하며, 0은 더이상 수신 할 수 없음을 나타낸다. 발신측은 윈도우 사이즈가 0인 패킷을 받으면 일단 전송을 중단한다.
  • 체크섬
    • TCP 세그먼트가 손상되지 않았는지 무결성을 확인하는데 사용
  • 비상 포인터
    • 긴급 데이터가 있을 때 긴급 데이터를 나타내는 마지막 바이트의 시퀀스 번호가 설정된다.
    • 제어 비트의 URG 플래그가 1일 때만 유효.
  • 옵션
    • TCP 관련 확장 기능을 서로에게 알리기 위해 사용된다.
    • 4바이트 단위로 변화한다.
    • Kind에 따라 정의된 몇 가지 옵션을 ‘옵션 목록’으로 나열하는 형태로 구성된다. 옵션 목록의 조합은 OS와 그 버전에 따라 달라진다.
    • 특히 중요한 옵션은 다음 두 가지가 있다.
      • MSS(Maximum Segment Size)
        • MTU가 IP 패킷의 최대 크기를 나타내는 것 처럼, MSS는 TCP 페이로드 최대 크기를 나타낸다.
        • 기본 MTU가 1500이고 TCP 헤더가 40바이트라면, MSS는 1500-40=1460 바이트가 된다.
      • SACK(Selective Acknowledgement)
        • 사라진 TCP 세그먼트만 재전송 하는 기능. RFC2018에서 표준화되어 거의 모든 OS에서 지원된다.
        • RFC9293에 정의된 표준 TCP는 ACK 번호를 사용해 데이터를 어디까지 받았는지만 판단한다. 그렇기 때문에 부분적으로 TCP 세그먼트가 손실되더라도 손실된 데이터 이후의 모든 TCP 세그먼트를 재전송해야 하는 비효율성이 있다.
        • SACK를 지원하면 부분적으로 TCP 세그먼트가 손실된 경우, “어디서부터 어디까지 받았는지” 범위를 옵션 필드로 알려주어 손실된 데이터만 재전송 가능하다.

TCP의 상태 전이

제어 비트를 구성하는 8개의 플래그 설정으로 TCP 커넥션 상태를 제어한다. 연결시에는 3-way handshake를 사용해 서로가 지원하는 기능과 시퀀스 번호를 결정하고 연결을 수립한다.

연결이 완료되면 실제 애플리케이션 데이터 교환을 시작한다. TCP는 데이터 전송의 신뢰성을 유지하기 위해 ‘흐름 제어’, ‘혼잡 제어’, ‘재전송 제어’등의 제어를 조합하여 전송을 수행한다.

데이터 교환이 완료되면 커넥션 종료 처리를 진행한다. 클로즈 처리에는 몇가지 패턴이 있는데, 여기서는 4-way handshake와 3-way handshake를 소개한다.

커넥션 시작 단계

  1. 서버는 지정된 포트를 열어두고 데이터 수신을 기다리고 있으며, 이 상태를 LISTEN 이라고 한다.
  2. 클라이언트는 SYN 플래그는 1로, 시퀀스 번호는 임의의 값(여기서는 x 라고 칭한다)을 설정한 SYN 패킷을 전송한다.
  3. SYN 패킷을 전송한 클라이언트는 CLOSED->SYN_SENT 로 상태를 전환하고 SYN/ACK 패킷을 기다린다.
  4. 서버가 SYN 패킷을 수신하면 다음과 같은 SYN/ACK 패킷을 클라이언트로 전송한다.
    • SYN 플래그와 ACK 플래그는 모두 1
    • 확인 응답 번호는 SYN 패킷의 시퀀스 번호인 x에 1을 더한 x+1
    • 시퀀스 번호는 임의의 값(여기서는 y 라고 칭한다.)
  5. 책에서는 서버가 LISTENSYN-RCVD로 전환되는 것 처럼 표현해 두었는데 실제로는 TCB(Transmission Control Block)를 만들고 이것을 SYN-Queue에 등록한다고 한다. 즉, SYN-RCVD가 되는 주체는 TCB이다.
  6. 클라이언트가 다시 SYN/ACK 패킷을 받으면 ACK 플래그를 1로 설정하고 확인 응답 번호를 y+1 로 설정하여 다시 서버로 전송한다.
  7. ACK 패킷을 전송한 클라이언트는 SYN-SENTESTABLISHED 로 상태를 전환하고 데이터 교환을 시작한다.
  8. ACK 패킷을 받은 서버는 TCB를 큐에서 제거하고, 소켓을 생성하고, 소켓을 ESTABLISHED 상태로 만들고 데이터 교환을 시작한다.

커넥션 설정 단계

  • 흐름 제어
    • 수신 측 장비가 수행하는 유량 조정이다. 수신측 단말은 윈도우 크기 필드를 사용하여 자신이 받을 수 있는 데이터 양을 알린다. 데이터를 보내는 쪽에서는 윈도우 크기까지는 ACK를 기다리지 않고 세그먼트를 계속 보낸다. 이를 통해 데이터 수신 응답을 최소화하여 전송 효율성을 높인다.
  • 혼잡 제어
    • 발신 측 장비가 수행하는 유량 조정이다. 한꺼번에 많은 장비가 인터넷을 사용하게 되면 네트워크상의 패킷이 몰려서 혼잡이 발생한다. 패킷이 많아지면 네트워크 장비에서의 처리가 느려지거나 회선 대역폭 제한에 걸려 패킷이 사라지는 등의 문제가 발생하게 된다. 그 결과 사용자는 속도가 느려진다고 체감하게 된다.
    • 이 책에서는 패킷 손실을 감지하여 혼잡을 판단하는 CUBIC 알고리즘을 중심으로 설명한다. 혼잡 제어 알고리즘에는 여러 종류가 있으나 2023년 기준 가장 널리 쓰이는 것이 CUBIC이다.
    • CUBIC은 패킷 손실을 감지하면 혼잡이 발생했다고 판단하여 혼잡 윈도우를 줄이고, 패킷 손실이 감지되지 않으면 혼잡이 해소된 것으로 판단하고 혼잡 윈도우를 늘린다.
  • 재전송 제어
    • 패킷 손실이 발생했을 때 수행하는 패킷 재전송 기능. TCP는ACK 패킷을 통해 패킷 손실을 감지하고 재전송한다.
    • 재전송 제어는 수신 측에서 트리거하는 Duplicate ACK와 발신 측에서 트리거하는 Retransmission Time Out 두가지가 있다.
    • Duplicate ACK
      • 전송받은 TCK 세그먼트의 시퀀스 번호가 건너뛰었다면 패킷 손실이 발생했다고 판단하고 확인 응답이 동일한 ACK 패킷을 연속적으로 발송한다. 같은 ACK 패킷을 전송하기 때문에 Duplicate ACK 라고 부른다.
      • 데이터를 보내는 쪽에서 일정 횟수 이상의 중복 ACK를 수신하면 해당 TCP 세그먼트를 재전송한다. SACK 사용 여부에 따라서 누락된 TCP 세그먼트만 재전송 하거나 누락이 발생한 TCP 세트먼트부터 이후 데이터 전체를 재전송 할 수 있다.
      • 중복 ACK를 트리거로 하는 재전송 제어를 Fast Retransmit 이라고 한다.
    • Retransmission Time Out
      • Retransmission Timer란 TCP 세그먼트를 전송한 후 ACK 패킷을 기다리는 동안의 시간을 말한다. 이 타이머는 RTT(Round Trip Time, 패킷 왕복 시간)에서 수학적 로직에 따라 계산된다. RTT가 짧을수록 Retransmission Timer도 짧아진다.
      • Retransmission Timer는 ACK 패킷을 받으면 재설정된다.

커넥션 종료 단계

커넥션 종료 단계에서는 FIN, RST 패킷을 주고받으면서 커넥션을 닫는다. 커넥션을 닫는데 실패하면 불필요하면 커넥션이 쌓여서 리소스 부족 현상을 초래 할 수 있기 때문에 오픈 보다 더 철저하고 신중하게 진행하도록 설계되었다.

FIN 은 “더 이상 전송할 데이터가 없다”는 의미이고, RST는 TCP가 예기치 않은 오류가 발생 했을때 커넥션을 강제로 끊기 위해 사용된다. 책에서는 FIN 패킷을 이용하는 일반적인 클로즈 처리에 대해서 설명한다.

커넥션을 오픈 할 때 3-way handshake를 사용한 것 처럼, 커넥션을 닫을때는 보통 4-way handsahek를 사용해서 커넥션을 닫는다. 그러나 효율성을 위해 4-way 대신 3-way 로 커넥션을 닫기도 한다.

연결을 시작하는 것은 항상 클라이언트가 먼저 요청하지만, 연결을 닫는 것은 클라이언트나 서버 둘 중 아무나 할 수 있다.

  1. 연결을 끊고자 하는 곳(여기서는 클라이언트라고 가정한다)에서 FIN, ACK 패킷을 전송한다.
  2. 클라이언트는 FIN/ACK 패킷을 보내고 ESTABLISHEDFIN-WAIT-1 로 상태를 전환한다.
  3. FIN/ACK 패킷을 받은 서버는 ACK 응답을 보내고 소켓을 ESTABLISHEDCLOSE-WAIT 상태로 전환한다.
    1. ACK 패킷을 받은 서버는 FIN-WAIT-1FIN-WAIT-2 로 상태를 전환한다.
  4. 서버가 더 전송할 데이터가 남아있다면 남은 데이터를 모두 전송한다.
  5. 데이터 전송이 완료되면 서버는 FIN, ACK 패킷을 전송하고 소켓을 CLOSE-WAITLAST-ACK 상태로 전환한다.
  6. 클라이언트는 FIN/ACK 패킷을 받고서 ACK 패킷을 응답하고 FIN-WAIT-2TIME-WAIT 로 변경한다. 늦게 도착하는 패킷을 대비해 OS에 지정된 일정 시간을 기다린 후 CLOSED 로 전환 후에 삭제한다.
  7. 서버는 ACK 패킷을 받으면 소켓을 LAST-ACKCLOSED 로 전환하고 연결을 삭제한다.

앞서 언급한 것처럼 경우에 따라서는 서버가 최초에 FIN/ACK 요청을 받고서 더 보낼 데이터가 없다고 판단하면 ACK를 보내고 FIN/ACK를 보내는 대신 바로 FIN/ACK를 보내서 요청 단계를 3-way 로 줄이기도 한다.

2

No comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/019ab01c-e8da-7cdc-af19-c7c5cff6c0ef on your instance and reply to it.