기본 단계에서는 “TCP는 이렇게 동작한다”고 설명하고 넘어간 것들이 많습니다. 심화 단계는 그걸 원문으로 검증하는 구간입니다. 첫 글은 스펙 자체부터 봅니다 — TCP의 원본 명세인 RFC 793(1981)과, 그걸 대체한 최신 명세 RFC 9293(2022) 사이에는 41년의 간격이 있습니다. 그동안 뭐가 왜 바뀌었을까요.

RFC 9293은 혼자가 아니다

RFC 9293은 RFC 793 하나만 대체한 게 아닙니다. 원문에 이렇게 나와 있습니다.

Obsoletes: 793, 879, 2873, 6093, 6429, 6528, 6691 Updates: 1011, 1122, 5961

41년 동안 TCP는 RFC 793 본문을 그대로 두고, 여러 개의 별도 RFC로 조각조각 보완돼 왔습니다. RFC 9293은 이 흩어진 조각들을 다시 하나의 문서로 합친 겁니다. 이 중 이 글에서는 실제로 기본편 내용과 맞닿아 있는 두 가지 — ISN 생성(RFC 6528)과 스푸핑 방어(RFC 5961) — 를 원문으로 뜯어봅니다.

사례 1: ISN 생성 방식 — 3편에서 예고했던 그 이야기

3편에서 “ISN을 예측 불가능하게 무작위로 고른다”고 했습니다. 원문을 보면 이게 처음부터 그랬던 게 아니라는 걸 알 수 있습니다.

RFC 793 (1981), Section 3.3:

“an initial sequence number (ISN) generator is employed which selects a new 32 bit ISN. The generator is bound to a (possibly fictitious) 32 bit clock whose low order bit is incremented roughly every 4 microseconds.”

원래는 그냥 4마이크로초마다 증가하는 시계였습니다. 예측 불가능하게 만들려는 의도가 아니라, 그냥 “매번 다른 값이 나오게” 하려는 실용적인 장치였습니다. 이 방식대로면 ISN은 시계를 보고 그대로 계산할 수 있습니다.

RFC 9293 (2022), Section 3.4.1:

“ISN = M + F(localip, localport, remoteip, remoteport, secretkey)”

M은 여전히 시간 기반 값이지만, 여기에 F()라는 의사난수함수가 더해졌습니다. RFC 9293은 이 F()에 대해 명시적으로 요구사항을 답니다: “F() MUST NOT be computable from the outside” — 즉 공격자가 다음 ISN을 계산해낼 수 있으면 안 됩니다. 이 요구사항은 원래 RFC 6528(2012)이 별도로 추가했던 걸 RFC 9293이 흡수한 겁니다.

3편에서 “ISN을 예측할 수 있으면 세션 하이재킹이 가능하다”고 설명했던 게, 정확히 이 변화의 이유입니다. 1981년의 TCP는 이 위협 자체를 고려하지 않았고, 그로부터 31년 뒤에야 스펙에 명시적으로 박혔습니다.

사례 2: MSL — 7편에서 나온 “2분”이 진짜 스펙값이었다

7편에서 “리눅스는 관례적으로 MSL을 30초로 잡는다”고 했습니다. “관례적으로”라고 쓴 이유가 있습니다. RFC 793 원문을 보면:

“the maximum segment lifetime (MSL) … Arbitrarily defined to be 2 minutes.”

스펙이 정의하는 값은 2분입니다. 그런데 실제 구현체(Linux 포함)는 이 값을 그대로 쓰지 않고 30초로 줄여서 씁니다. 스펙이 “arbitrarily defined”(임의로 정함)라고 스스로 인정하고 있어서, 구현체들이 실용적인 값으로 조정해도 스펙 위반이 아닙니다. 이게 바로 심화 단계에서 하려는 것의 좋은 예시입니다 — 기본편에서 “관례적으로 30초”라고 뭉뚱그렸던 게, 원문을 보니 “스펙은 2분이라고 하지만 아무도 그대로 안 쓴다”는 훨씬 구체적인 그림으로 바뀝니다.

사례 3: RFC 5961 — RST 하나로 연결을 끊을 수 있었다

3편에서 RST를 잠깐 언급했습니다(“클라이언트가 요청하지 않은 SYN-ACK에는… 오히려 RST를 보내”). RFC 793 원문에서 RST가 유효한지 검증하는 기준은 이렇습니다.

“A reset is valid if its sequence number is in the window.”

즉 시퀀스 넘버가 현재 수신 윈도우 범위 안에만 있으면 RST가 그대로 받아들여집니다. 문제는 윈도우가 생각보다 큰 값(수만 바이트)일 수 있다는 겁니다. 공격자가 5-tuple(2편에서 배운 그 5-tuple)만 추측할 수 있다면, 시퀀스 넘버를 윈도우 크기만큼만 시도해보면 됩니다 — 32KB 윈도우라면 최대 65,536번 시도로 남의 연결을 끊을 수 있었습니다. 실제 연결도 아니면서, 순전히 시퀀스 넘버 범위만 노려서요.

RFC 5961(2010)은 이 검증을 훨씬 엄격하게 바꿨습니다.

“A reset is valid if its sequence number exactly matches the next expected sequence number.”

시퀀스 넘버가 정확히 RCV.NXT와 일치해야만 RST가 즉시 받아들여집니다. 그럼 윈도우 안이지만 정확히 일치하지는 않는 애매한 경우는 어떻게 할까요? 바로 거부하지 않고, 대신 challenge ACK를 보냅니다.

sequenceDiagram
    participant X as 공격자 (오프패스, 5-tuple만 추측)
    participant B as Host B
    participant A as Host A
    X->>B: 위조된 RST (seq는 윈도우 안, 정확히 일치는 아님)
    Note over B: RFC 5961: 즉시 끊지 않는다
    B->>A: challenge ACK (seq=SND.NXT, ack=RCV.NXT)
    Note over A: 진짜 A는 이 ACK를 확인하고<br/>정상 트래픽을 계속 보낸다
    Note over B: A로부터 정상 트래픽이 계속 오면<br/>방금 그 RST는 위조로 판단, 무시

정상적인 상대방(Host A)이라면 이 challenge ACK를 받아도 자기 연결 상태와 비교해서 “어, 나는 이런 걸 보낸 적 없는데”라며 그냥 무시하고 원래 하던 통신을 계속합니다. 반면 공격자는 애초에 이 challenge ACK를 볼 수 있는 위치(오프패스)에 있지 않으므로 응답할 수 없습니다. 결과적으로 공격 난이도가 “윈도우 크기만큼의 시도”에서 “시퀀스 넘버 공간 전체의 절반(2^31)”으로 뛰어오릅니다 — 사실상 무작위로 맞히는 수준입니다.

SYN 공격도 비슷하게 바뀌었습니다. RFC 793 방식은 윈도우 안의 SYN을 받으면 RST로 응답했는데, RFC 5961은 여기도 challenge ACK로 바꿨습니다. 진짜 상대방이 재시작했다면 이 challenge ACK에 RST로 응답할 것이고(자기가 그런 연결을 모르니까), 위조된 SYN이었다면 그냥 무시됩니다.

사례 4: Urgent pointer — 사소하지만 실존했던 모호함

1편에서 다루지 않고 넘어갔던 필드가 하나 있습니다. RFC 793은 urgent pointer가 정확히 어느 바이트를 가리키는지에 대해 구현체마다 다르게 해석할 여지를 남겨뒀습니다. RFC 9293은 이렇게 명확히 합니다.

“The urgent pointer points to the sequence number of the octet following the urgent data.”

“urgent data 바로 다음 바이트”라고 못박은 겁니다. 이건 보안 이슈라기보다는, 서로 다른 OS의 TCP 구현체들이 미묘하게 다르게 동작하던 걸 RFC 6093(2011)이 정리하고 RFC 9293이 흡수한 사례입니다. 지금은 urgent pointer 자체를 쓰는 애플리케이션이 거의 없어서 실무 영향은 크지 않지만, “스펙이 모호하면 구현체마다 갈린다”는 걸 보여주는 좋은 예시입니다.

지금 구현체들은 최신 스펙을 따르는가

리눅스를 포함한 대부분의 현대 TCP 구현체는 RFC 5961의 challenge ACK 방식과 RFC 6528의 ISN 요구사항을 이미 구현하고 있습니다 — 정확히 어떤 커널 코드가 이걸 처리하는지는 11편(Linux TCP 상태머신)에서 SYN cookie와 함께 직접 소스를 따라가며 확인합니다.

다음 글

지금까지는 스펙의 “본문”에서 바뀐 지점들을 봤습니다. 다음 글에서는 시선을 좁혀서 TCP 헤더 20바이트를 비트 단위로 완전히 분해합니다. 방금 나온 ISN도, 그리고 5편·6편에서 예고했던 SACK·ECN도 전부 이 헤더의 특정 비트 자리에 대응합니다.