인터넷의 네트워크 계층에는 패킷이 순서대로, 손실 없이 전달될 것이라는 보장이, 심지어는 패킷이 전달될 것이라는 보장도 없다. 그런데 TCP는 그 위에서 신뢰성 있는 데이터 전송을, 그러니까 패킷이 손상없이, 빠짐없이, 중복 없이, 순서대로 도착될 것임을 보장한다.
이전 글에서는 전송되었지만 아직 ACK
을 받지 못한 각 세그먼트에 개별적인 타이머가 연결되어 있다고 가정했는데, 이 방식은 잘 동작하기는 하지만 타이머 관리를 위한 상당한 오버헤드가 발생할 수 있다. 따라서 TCP에서는 보통 여러 개의 세그먼트가 아닌, 하나의 세그먼트(전송/비확인)에 대해서만 재전송 타이머를 사용한다.
그렇다면 어떻게 TCP는 신뢰성 있는 데이터 전송을 제공할까?
일단은 타임아웃만을 사용하는 간단한 모델을 살펴보자. TCP 송신자는 다음의 세 주요 이벤트를 확인하고 처리해야 한다.
SEQ
에는 세그먼트 내 첫 번째 데이터 바이트의 바이트 스트림 번호가 들어간다.ACK
수신ACK
세그먼트를 받으면 ACK
번호를 윈도우의 SendBase
와 비교한다. ACK
방식을 사용하므로 송신 측에서 ACK
번호 y
를 받으면 y
이전까지의 모든 바이트를 수신했음을 알 수 있다. y > SendBase
를 받은 송신자는 윈도우를 옆으로 옮기고, 여전히 확인되지 않은 세그먼트가 있으면 타이머를 재시작한다.타임아웃 기반 재전송에는 타임아웃 시간 자체가 너무 길 수 있다는 단점이 있다. 세그먼트가 손실된 경우 송신자는 이 긴 시간동안 패킷 재전송을 위해 대기해야하고, 결론적으로는 지연이 증가하게 된다. 이를 해결하기 위한 방법 중 하나가 빠른 재전송으로, 중복 ACK
감지를 통해 타임아웃 이벤트 발생 전에 패킷 손실을 감지하는 방법이다.
TCP 수신자는 다음과 같은 경우 ACK
를 보낸다.
ACK
전송ACK
을 즉시 전송ACK
를 전송(즉 마지막으로 제대로 순서대로 수신한 바이트에 대한 ACK
을 재전송!)ACK
즉시 전송이때 송신자는 여러 개의 연속된 세그먼트를 전송할 수도 있다. 이렇게 연속적으로 보낸 세그먼트 중 하나가 손실되면, 그 이전 세그먼트에 대한 여러 개의 ACK
이 중복될 수도 있다. 송신자는 이렇게 동일한 ACK
이 세 번 중복되면 해당 세그먼트가 손실되었다는 신호로 간주하고 재전송한다.
Q. 중복
ACK
을 한 번 받았을 때 재전송하면 안될까? 왜 세 번일까?
A. 세그먼트가 정말로 손실됐을 가능성이 높다고 판단하기 위해 중복ACK
한 번은 부족하다.예를 들어 하나의 중복
ACK
이 오면 세그먼트를 재전송한다고 하자. 송신자는 1, 2, 3, 4번 세그먼트를 보낸다.수신자는 1번 세그먼트를 받고
ACK = 2
를 보내고, 2번 세그먼트를 기다린다. 그런데 네트워크의 문제로 2번 세그먼트보다 3, 4번이 먼저 도착했다고 해보자. 순서가 잘못됐으니 수신자는 두 번ACK = 2
를 보낸다.이때 만약 2번 세그먼트가 뒤늦게 수신자에게 도착했다면 송신자는 2번 세그먼트를 다시 보낼 필요가 없다. 하지만 중복
ACK
를 하나만 받아도 수신자는 재전송하므로 수신자는 불필요한 2번 세그먼트를 재전송한다.중복
ACK
세 번은 세그먼트가 손실됐을 가능성이 높다고 판단하기 위해 필요한, 경험적으로 최적으로 알려진 횟수(라고 한)다.
TCP에서 ACyK
은 누적되고, 올바르게 수신되었지만 순서가 어긋난 세그먼트에 대해서는 개별적으로 ACK
을 보내지 않는다. 이런 면에서 TCP는 GBN 스타일 프로토콜처럼 보인다.
TCP에서는 GBN과 달리 올바르게 수신되었지만 순서가 어긋난 세그먼트들을 버퍼링하고 이용할 수도 있다.
예를 들어 송신자가 1, 2, 3, 4, 5번 세그먼트를 보냈는데, 수신자는 1, 2, 4, 5만 받았다고 하자. 수신자는 3번이 비었음을 알고 ACK 3
과 함께 SACK
(selective ACK
) 옵션을 이용해 4, 5번은 받았으니 3번만 다시 보내달라는 응답을 보낼 수 있다.
// 2번까지는 제대로 받았고, 4, 5번도 제대로 받았음... 같은 형식으로
ACK 3 (SACK: 4, 5)
결론은 TCP는 기본적으로 GBN처럼 동작하지만, 옵셔널하게는 SR처럼 동작하게 할 수도 있다.
TCP에서는 송수신자가 모두 수신 버퍼를 가지고 있는데, 송신자가 수신자가 처리할 수 있는 양 이상으로 데이터를 보내는 경우 수신 버퍼에 오버플로가 발생할 수 있다. TCP는 이런 문제를 방지하기 위해 흐름 제어 서비스를 제공한다.
흐름 제어(flow control)
송신자가 데이터를 보내는 속도와 수신자가 데이터를 읽는 속도를 맞추는 서비스
구체적으로는 수신 윈도우를 사용한다. 수신 윈도우는 수신자 버퍼에 남아 있는 여유 공간을 송신자에게 알려주는 것으로, 동적으로 시간에 따라 변한다.
호스트 A가 B에게 파일을 전송할 때, B는 수신 버퍼를 할당하고 B의 애플리케이션은 이 버퍼에서 데이터를 읽는다. 이때 사용되는 변수는
LastByteRead
: 애플리케이션 프로세스가 읽은 데이터 스트림의 마지막 바이트 번호LastByteRcvd
: 네트워크에서 수신된 데이터 스트림의 마지막 번호. 즉 수신 버퍼에 들어온 마지막 바이트 번호가 있다. 수신 버퍼가 넘지 않으려면 LastByteRcvd - LastByteRead <= RcvBuffer
가 되어야 하며, 따라서 수신 버퍼의 여유 공간인 수신 윈도우(rwnd
)은 rwnd = RcvBuffer - (LastByteRcvd - LastByteRead)
로 계산할 수 있다.
B는 매번 TCP 세그먼트의 수신 윈도우 필드에 rwnd
의 값을 넣어 송신자에게 알려준다. 처음에는 rwnd = RcvBuffer
로 설정되고, 이후 시간에 지남에 따라 변하는 값을 지속적으로 알려준다.
만약 수신 버퍼가 가득 차서 rwnd = 0
이 된다고 하자. 이렇게 수신 버퍼가 가득 찬 상태에서 호스트 B는 데이터를 더 읽어야 하는데, A의 입장에서는 수신 버퍼가 언제 비게 될지 알지 못한다. 때문에 TCP에서는 A는 rwnd = 0
을 받으면 세그먼트에 1바이트의 데이터를 포함시켜 보내고, 이에 대한 ACK
에 새로운 rwnd
가 들어오는 걸 보면 수신 버퍼에 여유가 생겼음을 알 수 있게 된다.
이제 TCP 연결이 어떻게 설정되고 해제되는지 더 자세히 알아보자.
늘 그렇듯 클라이언트는 연결 요청을 하는 쪽, 서버는 요청을 받는 쪽이다.
SYN
세그먼트를 하나 보낸다.CLOSED
에서 SYN_SENT
로 변한다.SYN
플래그를 1로 설정한다.client_isn
)를 시퀀스 번호 필드에 넣는다.SYN
세그먼트를 받으면 연결을 위한 버퍼와 변수를 할당하고, SYNACK
세그먼트로 응답한다.LISTEN
에서 SYN_RCVD
로 변한다.SYN
플래그와 ACK
플래그가 모두 1이다.ACK
번호 필드에는 client_isn + 1
이 들어간다.SYN
번호 필드에는 서버 자신이 랜덤하게 정한 초기 시퀀스 번호(server_isn
)가 들어간다.SYNACK
를 받은 클라이언트는 연결을 위한 버퍼/변수를 할당하고, 서버에 ACK
를 보낸다.SYN_SENT
에서 ESTABLISHED
로 변한다.ACK
플래그는 1, SYN
플래그는 0으로 설정된다.SEQ
필드는 client_isn + 1
, ACK
필드는 server_isn + 1
로 설정된다.ACK
를 받으면 상태가 SYN_RCVD
에서 ESTABLISHED
로 변한다.위의 세 단계가 완료되고나면 클라이언트와 서버는 서로에게 데이터가 포함된 세그먼트를 보낼 수 있게 된다.
Q. 잘못된 포트로 연결 요청을 보내거나, 리슨 소켓의 백로그가 가득 찬 경우에는?
A. 잘못된 포트로 연결 요청을 보내는 경우RST
플래그를 1로, 리슨 소켓 백로그가 가득 차서 더 이상 새 연결을 받을 수 없는 경우에는RST + ACK
을 1로 설정한다.
Q. 왜 초기 시퀀스 번호를 교환할까?
A. 신뢰성 있는 데이터 전송을 위해서는 시퀀스 번호가 필요하다. TCP는 전이중 통신으로, 양방향 통신이 가능하므로 서로의 ISN을 교환해야 한다.
Q. 왜 랜덤한 초기 시퀀스 번호일까?
A1. ISN이 고정되어 있거나 예측 가능한 경우, 중간자의 세션 하이재킹이 쉬워진다. 공격자는 클라이언트가 보낸 패킷을 가로채고, 클라이언트인 척, 가짜SYN
, 가짜ACK
을 날릴 수 있따.
A2. 이전에 비정상적으로 종료된 연결과 동일한 TCP 4-tuple(소스 IP/포트, 목적지 IP/포트)을 사용하는 경우, 새로 맺은 연결의 시퀀스 번호가 예전과 같거나 너무 가까우면 예전 연결의 지연된 패킷이 새로운 연결에 잘못 섞여 들어갈 위험이 있다(세그먼트 혼동 문제).
Q. 왜 2-way가 아니라 3-way일까?
A1. 서버가SYNACK
를 보낸 후 클라이언트가 실제 응답 없이 사라질 수도 있다. 2-way에서는 불필요한 리소스 할당이 일어나게 된다.
A2. TCP는 양방향이다. 시퀀스 번호의 동기화가 완전히 이루어져야 한다.
더 이상 연결이 필요하지 않으면 TCP 연결을 종료해야 한다. 종료는 두 프로세스 중 어느 쪽이든 먼저 시작할 수 있다. 우선 클라이언트가 연결 종료를 원한다고 해보자.
close
명령을 호출하면, 이 명령은 클라이언트 TCP가 서버에게 FIN
세그먼트를 보내게 한다.ESTABLISHED
에서 FIN_WAIT_1
로 변한다.FIN
플래그가 1이다.FIN
세그먼트를 받은 서버는 클라이언트에게 ACK
를 보내고, 서버 애플리케이션에서의 종료 작업을 수행한다.ESTABLISHED
에서 CLOSE_WAIT
으로 변한다.ACK
를 받은 클라이언트의 상태는 FIN_WAIT_2
로 변한다.CLOSE_WAIT
에서 LAST_ACK
으로 변한다.FIN
플래그가 1이다.ACK
를 보낸다.TIME_WAIT
으로 변한다. 이 ACK
가 손실될 경우 재전송할 수 있도록 TCP 클라이언트에 시간을 주기 위함이다. 대기 시간(30초~2분 정도)이 지나면 클라이언트의 상태가 CLOSED
로 변한다.ACK
를 받은 서버는 CLOSED
상태로 변한다.반대로 서버가 연결 종료를 원할 수도 있다. 서버가 FIN
세그먼트를 먼저 보낸다는 점만 제외하면 상태 전이나 전송되는 세그먼트나 동일하다.
SYN
Flood 공격3-way 핸드셰이크에서 SYN
을 받은 서버는 관련 리소스를 할당하고 SYNACK
를 보낸다. 그런데 이때 클라이언트가 따로 ACK
을 보내지 않으면 서버는 일정 시간이 지나고 나서야 해당 연결을 종료하고 자원을 회수한다.
그렇다면 공격자가 너무 많은 SYN
세그먼트를 보내기만 하면 어떻게 될까? 서버는 수많은 자원을 반쯤 열린 연결(half-open connection)에 할당하고, 정작 정상적인 클라이언트는 서비스를 이용할 수 없게 된다.
위와 같은 SYN
Flood 공격을 막으려면 SYN
을 받은 시점에는 리소스를 할당하지 않고, 완전히 연결이 수립됐을 때 리소스를 할당하면 된다.
SYN
을 받은 서버는 SYN
세그먼트의 소스/목적지 IP/포트, 서버만 알고 있는 비밀값 등을 이용한 해시 함수로 server_isn
(SYN
쿠키라고도 부른다)을 만들고, SYNACK
의 SEQ
필드에 담아 보낸다. 이때 서버는 따로 자원 할당을 하거나 연결에 대한 상태 정보를 기억하지는 않는다.
정상적인 클라이언트는 ACK
을 보내줄 것이고, ACK
를 받은 서버는 이 ACK
가 자신의 SYNACK
에 대한 정보인지 확인한다.
ACK
세그먼트의 ACK
번호는 서버가 보낸 server_isn
에 1을 더한 값이다.ACK
번호와 같다면 정상적인 연결임을 인식하고 실제로 연결을 생성한다.Q. 왜 굳이 해시 함수를 사용할까?
A.SYN
Flood 공격 방지를 위해 어떠한 상태 정보도 저장하지 않기 때문이다. 만약 임의의 랜덤값을 배정한다면, 서버에서는 이 값을 또 어딘가에 저장해둬야 한다. 하지만SYN
쿠키와 해시 함수를 사용하면 연결을 맺기 전에는 서버를 무상태적으로 유지할 수 있다.
Q. 너무 늦게
ACK
가 와버리면 어떨까?
A. 서버는SYNACK
를 보내고 상태를 유지하지 않는다. 그렇다면 클라이언트가 너무 늦게, 예를 들어 1시간 후에ACK
를 보내도 연결을 만들어줘야 할 수도 있다. 이런 상황을 방지하기 위해 서버는server_isn
을 만들 때, 해시값에 타임스탬프를 붙이는 식으로 만든다. 일종의 쿠키의 수명을 정하는 방식으로 생각할 수 있는데, 너무 늦게 도착한 클라이언트로ACK
으로 연결을 수립하는 것을 방지한다.
Q. 따로 자원 할당을 하지 않는데 수신 윈도우의 크기는 어떻게 보낼까?
A. 따로 자원 할당은 하지 않지만, 수신 버퍼의 기본 크기는 알고 있다. 그냥 보내면 된다.