diff options
Diffstat (limited to 'docs/design')
| -rw-r--r-- | docs/design/006-native-protocol.md | 3 | ||||
| -rw-r--r-- | docs/design/011-pure-transport.md | 199 |
2 files changed, 202 insertions, 0 deletions
diff --git a/docs/design/006-native-protocol.md b/docs/design/006-native-protocol.md index 9b51fec..c4dfd3b 100644 --- a/docs/design/006-native-protocol.md +++ b/docs/design/006-native-protocol.md @@ -8,6 +8,9 @@ TLS 1.3/TCP 위에 실은 피어 호스트를 검증했다. [026 — scalar 피� [030 — 문서 피어·출력 소비 선택](../research/030-authenticated-document-peer.md)을 본다. 이 설계의 일반 RPC·QUIC·새 전송 후보가 구현된 것은 아니다. +2026-09-28: 노드 사이 전달의 새 UDP 전송 후보(Pure 전송)는 설계와 quinn·ngtcp2 대비 측정 계획을 +[011](011-pure-transport.md)에 적었다. 아래 "새 UDP 전송은 … 별도 연구 후보로 연다"는 조건은 그 0단계(비용 분해)가 판정한다. + ## 결정 Pure 간 통신은 HTTP 메서드·URL·문자열 헤더를 필수 실행 모델로 삼지 않는다. diff --git a/docs/design/011-pure-transport.md b/docs/design/011-pure-transport.md new file mode 100644 index 0000000..0d019cd --- /dev/null +++ b/docs/design/011-pure-transport.md @@ -0,0 +1,199 @@ +# 011 — Pure 전송: 저장한 기록을 곧바로 보내는 UDP 전송 + +작성일: 2026-09-28. 상태: 설계. 구현과 측정 전이다. [외부 피드백 002](../outside-feedback/feedback-002.md)의 +"QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다. + +## 쉬운 말로 + +- 알고 싶은 것: Pure 노드끼리 주고받는 전달(보내기·메신저 줄)을 QUIC보다 적은 CPU·메모리·왕복으로 옮길 수 있는가. +- 그럴 수 있다고 보는 까닭: Pure의 전달은 이미 저장되고, 번호가 붙고, 받는 쪽에서 한 번만 적용되고, 봉인된다. QUIC과 TCP+TLS는 + 같은 일(재전송 버퍼, 재전송 방지, 암호, 흐름 제어, 확인 응답)을 한 번 더 한다. 그 겹친 층을 걷어 낼 수 있는지를 본다. +- 사용 경험의 차이(가설): 메신저 줄이 상대 화면에 닿는 시간이 줄고, 처음 연결할 때 기다림이 없다. 와이파이에서 LTE로 바뀌어도 + 전달이 끊기지 않는다. 노드 사이에 HTTPS 인증서가 필요 없다. +- 아직 모르는 것: 전달 하나의 비용 가운데 전송이 차지하는 몫이 얼마인지. 몫이 작으면 새 프로토콜로 얻을 것도 작다. 그래서 그것부터 잰다. +- 근거: 지금은 문헌과 기존 측정([086](../research/086-sealed-sends.md), [088](../research/088-sealed-sessions.md))뿐이다. + QUIC 대비 수치는 아직 없다. + +## 착수 기준 + +- **바꿀 결정.** Pure 노드 사이의 전달을 지금의 HTTP/1.1(TLS) 위 HPKE 봉인([083](../research/083-delivery-over-http.md), + [088](../research/088-sealed-sessions.md))에서 무엇으로 옮길지. 후보는 셋이다. + - (가) 이 문서의 Pure 전송(UDP) + - (나) QUIC 스트림 위의 Pure 기록 형식(ngtcp2 또는 quinn) + - (다) 옮기지 않음 + HTTP 전달은 어느 경우든 제3자와 브라우저를 위한 경계로 남는다([006](006-native-protocol.md)). +- **남은 질문.** + 1. 전달 하나의 CPU 가운데 전송(형식, 암호, 시스템 호출, 복사, 확인 응답, 전송 상태)의 몫은 얼마이고, 적용·저장의 몫은 얼마인가. + 2. 보내는 앱의 저장소를 재전송 버퍼로 쓸 때 복사나 지연이 늘지 않는가. + 3. 처음 만나는 연결의 첫 패킷에 기록을 실어도 안전한가. 증폭, CPU 소모 공격, 전방 비밀성이 걸린다. + 4. 표준 혼잡 제어를 그대로 쓰면서 공정성을 지킬 수 있는가. + 5. UDP가 막힌 곳에서는 무엇으로 이어 가는가. +- **최소 확인.** + 1. 먼저 비용 분해다. 쌓인 보내기 풀기(W1)를 지금의 전달과 quinn 기준선에서 돌린다. 전달 하나의 CPU를 층별로 나눈다. + 2. 그다음 시제품이다. 한 세션·한 전달선만 둔다. 손실 탐지는 RFC 9002, 혼잡 제어는 NewReno로 하고, 재전송은 저장소에서 읽는다. + 이것을 W1과 손실·지연(W4)에서 가장 좋은 QUIC 기준선과 번갈아 잰다. +- **중단 기준.** 아래 "중단 기준" 절의 수치다. 요약하면 셋이다. + - 전송의 몫이 작으면 만들지 않는다. + - 시제품이 CPU당 전달에서 1.5배를 못 넘으면 멈추고 (나)를 쓴다. + - 한 번이라도 잃거나 겹치면 되돌린다. + +## 의심하는 관례 + +| 관례 | 이 설계 | +|---|---| +| 전송이 신뢰성을 가진다(재전송 버퍼, ACK). 그 위에서 앱은 또 outbox, 재시도, 중복 제거를 가진다 | 보내는 앱의 저장소(outbox)가 재전송 버퍼다. 전송은 잃은 것을 알아채고 저장소에서 그 번호를 다시 읽는다 | +| 전송의 확인 응답과 앱의 "저장했다"는 다른 메시지다 | 한 패킷에 둘 다 싣는다. 패킷 ACK(손실 탐지, RTT)와 전달선마다의 "N번까지 저장했다"다. 보내는 쪽은 뒤의 것으로 덜어 낸다 | +| 전송 암호(TLS, QUIC)와 본문 봉인(HPKE)을 따로 한다 | 직접 잇는 링크는 한 번만 봉인한다. 두 노드의 세션 키로 패킷을 봉인한다. 중계를 지날 때만 기록을 따로 봉인한다 | +| 0-RTT 데이터는 재전송 공격 때문에 위험하다 | 받는 쪽이 번호로 한 번만 적용하므로, 다시 보낸 기록은 적용되지 않는다. 받는 노드의 키는 배선할 때 알고 있으므로, 처음 만나는 연결도 첫 패킷부터 기록을 싣는다 | +| 흐름 제어는 바이트 창이다 | 받는 쪽이 다음 저장에 들일 수 있는 기록 수와 바이트를 알린다(지금 `X-Pure-Batch`가 하는 일을 전송이 한다) | +| 연결은 주소(5-tuple)로 식별한다 | 세션 식별자와 키로 식별한다. NAT 재바인딩, 망 이동에서 주소가 바뀌어도 세션이 이어진다 | +| 선 위의 형식과 저장 형식이 다르다(직렬화, 파싱) | 저장한 기록의 바이트와 필드 표가 곧 선 위의 바이트다. 받는 쪽은 JSON을 읽지 않고 `pure_app_deliver`에 넘긴다 | + +이것은 끝에서 끝 논증(Saltzer, Reed, Clark, "End-to-End Arguments in System Design", ACM TOCS 1984)을 그대로 쓴 것이다. +전달의 완전한 신뢰성은 끝점만 보장할 수 있다. 전송의 신뢰성은 성능을 위한 것일 뿐이다. Pure에는 끝점의 장치(저장, 번호, 한 번 +적용)가 이미 있으므로, 전송은 그것을 빌려 쓰고 제 것을 두지 않는다. + +## 구조(후보) + +패킷의 바이트 배치는 아직 고정하지 않는다. 시제품과 측정으로 정한다. + +**개념.** + +- 노드: 키 한 쌍을 갖는다(088의 `--key`). +- 세션: 두 노드 사이에 하나를 둔다. 키, 혼잡 제어기, 패킷 번호 공간을 하나씩 갖는다. 경로(주소 쌍)는 바뀔 수 있다. +- 전달선: 보내는 앱의 출력 하나에서 받는 앱의 입력 하나로 간다. 저장된 번호 공간을 갖는다. 한 세션의 여러 전달선은 혼잡 + 제어기를 나눠 쓰고, 순서는 전달선마다 따로 지킨다. 한 전달선의 손실이 다른 전달선을 막지 않는다. + +**패킷.** + +- 평문 머리: 판, 종류, 세션 식별자, 패킷 번호. 패킷 번호는 손실 탐지와 논스를 위한 것이다. 기록 번호와는 다른 공간이며, + 한 번 쓴 번호를 다시 쓰지 않는다. +- 봉인된 몸(AEAD)에는 프레임이 든다. + - 기록: 전달선, 첫 번호, 개수, 저장한 바이트와 필드 표 + - 패킷 ACK: 범위와 지연 + - 저장함: 전달선과 저장한 마지막 번호 + - 허가: 전달선, 다음 저장에 들일 기록 수와 바이트 + - 핑, 닫기 +- 잃은 패킷은 다시 보내지 않는다. 그 안에 있던 기록 가운데 아직 확인되지 않은 번호를 새 패킷에 실어 다시 보낸다(QUIC처럼 + 프레임 단위다. 다만 내용은 저장소에서 읽는다). +- 머리 보호(QUIC의 패킷 번호 가리기)는 남은 질문이다. 중간 장비가 프로토콜을 굳히지 못하게 하는 효과와 비용을 함께 본다. + +**핸드셰이크와 키.** + +- 보내는 노드는 받는 노드의 공개키를 배선할 때 안다(088의 `--seal`, `--sender`). 첫 패킷이 HPKE auth 모드로 캡슐화한 키와 + 기록을 함께 싣는다. 모양은 Noise IK의 첫 메시지와 같다. +- 받는 노드는 첫 응답에 제 임시 키를 싣는다. 그 뒤 세션은 임시 키끼리의 합의로 갈아탄다. 첫 비행 이후의 기록은 전방 비밀성을 + 얻는다. 첫 비행의 기록은 정적 키로만 보호되며, QUIC 0-RTT와 같은 한계다. 전방 비밀성이 필요한 출력은 첫 응답을 기다린 뒤에 + 보내게 할 수 있다. +- 증폭: 주소를 확인하기 전에는 받은 바이트의 3배 넘게 보내지 않는다(RFC 9000 §8.1과 같은 규칙). 받는 쪽의 답은 대개 작은 + ACK라 이 한도에 거의 닿지 않는다. +- CPU 소모 공격: 부하가 걸리면 X25519를 풀기 전에 주소에 묶인 쿠키를 먼저 요구한다. WireGuard의 쿠키 답과 같은 방식이다. + +**손실과 혼잡.** + +- 손실 탐지는 RFC 9002를 그대로 쓴다(패킷 번호, ACK 범위, RTT 추정, PTO). +- 혼잡 제어도 표준을 쓴다. NewReno부터 시작하고, CUBIC(RFC 9438)을 따른다. 자체 알고리즘은 만들지 않는다. 인터넷에서 다른 + 흐름을 굶기는 전송은 성능이 아니라 해악이다. +- 페이싱, ECN, 경로 MTU 찾기(DPLPMTUD, RFC 8899)를 쓴다. + +**두 단계의 확인.** + +- 패킷 ACK는 받았다는 뜻이다. 보내는 쪽은 그 기록을 다시 보내지 않지만 아직 덜어 내지 않는다. +- "저장함"은 받는 쪽이 동기화했다는 뜻이다. 보내는 쪽은 그때 덜어 낸다(`pure_app_trim`). +- 받는 쪽이 저장 전에 죽으면, 다시 이어질 때 "저장함"이 알리는 번호 다음부터 저장소에서 다시 보낸다. 지금의 HTTP 전달과 같은 + 의미이며, 빠른 길(패킷 손실)과 느린 길(재시작) 모두 같은 저장소에서 나온다. +- 받는 쪽이 받았지만 아직 저장하지 않은 기록은 메모리에 있다. 허가 프레임이 그 양을 다음 저장 한 번에 들일 만큼으로 묶는다. + 큰 기록이 허가보다 크면 기록 하나만큼은 늘 허가해 진행이 막히지 않게 한다([006](006-native-protocol.md)의 교착 조건). + +**송수신 경로.** + +- `sendmmsg`와 `recvmmsg`를 쓰고, Linux에서는 UDP GSO(`UDP_SEGMENT`)와 GRO(`UDP_GRO`)도 쓴다. +- 저장소의 기록 바이트를 AEAD의 출력 버퍼(곧 패킷 버퍼)로 봉인하는 것이 유일한 복사다. +- `SOCK_RAW`와 커널 우회는 쓰지 않는다. `SOCK_RAW`는 권한이 필요하고 NAT와 방화벽을 지나지 못한다. 필요하다면 측정이 그 비용을 + 보여 준 뒤에 따로 판단한다([006](006-native-protocol.md)). + +**경계(갈라파고스 금지).** + +- UDP가 막힌 곳에서는 기존 HTTP 전달(083)로 돌아간다. 같은 프레임을 TCP 위의 길이 붙은 레코드로 실을 수도 있다. 평문으로 + 낮추지는 않는다. +- 제3자와 브라우저는 HTTP 경계로 잇는다. 새 전송은 Pure 노드끼리 쓰는 것이다. +- 암호는 표준만 쓴다(HPKE RFC 9180, X25519, HKDF, AES-128-GCM이나 ChaCha20-Poly1305). 혼잡 제어도 표준이다. +- 명세를 이 저장소에 문서로 둔다. 판 협상, 모르는 프레임의 처리, 오류 부호를 정해 다른 구현이 만들어질 수 있게 한다. + +## 이미 있는 것과의 차이 + +개별 기법은 모두 이미 있다. 새 이름으로 포장하지 않는다. + +| 이미 있는 것 | 가져오는 것 | 다른 점 | +|---|---|---| +| QUIC(RFC 9000–9002) | 손실 탐지, 혼잡 제어, 증폭 한도, 프레임 단위 재전송 | QUIC은 범용 스트림이라 신뢰성과 버퍼, 암호를 전송이 소유한다 | +| WireGuard(Donenfeld, NDSS 2017), Noise IK | 미리 아는 정적 키, 첫 메시지의 키 교환, 쿠키, 카운터 논스, 키로 식별하는 세션과 로밍 | WireGuard는 신뢰성이 없는 터널이다 | +| Aeron | 로그를 버퍼로 쓰는 UDP 메시징, 받는 위치에 따른 흐름 제어 | Aeron의 로그는 메모리이고, 앱의 저장이나 한 번 적용과 묶이지 않으며, 암호가 없다 | +| Homa(SIGCOMM 2018) | 받는 쪽이 주는 허가 | 데이터센터용이다 | +| Kafka | 저장 형식이 곧 선 위의 형식(sendfile) | 전송은 TCP다 | +| KCP 같은 게임용 UDP | — | 혼잡 제어를 버리고 지연을 산다. 이 설계는 그러지 않는다 | + +가설은 조합에 있다. 이미 저장, 번호, 한 번 적용, 봉인 세션을 가진 런타임에서는 전송이 그것들을 빌려 쓸 수 있다. 그러면 전송의 +신뢰성, 전송 암호, 앱 전달 프로토콜(HTTP와 헤더, JSON)의 세 층이 한 층으로 준다. 학술적 신규성은 이 측정과 별도로 판단한다. + +## 측정 계획: quinn·ngtcp2 대비 + +**비교 대상.** 모두 같은 의미를 지킨다. 보내는 앱이 저장한 것만 보내고, 전달선마다 순서대로 보내며, 받는 쪽이 저장한 뒤에만 +덜어 내고, 받는 쪽은 한 번만 적용한다. 암호는 모두 AES-128-GCM이다(이 A76에서 가장 빠르다, [086](../research/086-sealed-sends.md)). + +1. 지금의 Pure 전달: HTTP/1.1 지속 연결과 HPKE 봉인 세션([088](../research/088-sealed-sessions.md), 봉인 보내기 약 46만/s) +2. QUIC 스트림 위의 같은 기록 형식 + - quinn 0.11(Rust, rustls): 이 Pi의 cargo 저장소에 있다. + - ngtcp2(C): 원본에서 OpenSSL 3.5 암호 백엔드로 빌드한다. 설치된 1.11은 머리 파일이 없다. + - OpenSSL 3.5.7의 QUIC: 참고로 둔다. +3. TLS 1.3/TCP 위의 같은 기록 형식([026](../research/026-secure-peer-link.md)) +4. Pure 전송 시제품 + +QUIC 기준선도 같은 의미를 위해 저장소에 기록을 남기고 앱 수준의 "저장함"을 기다린다. 두 겹의 버퍼는 그 구조에 딸린 비용이므로 +그대로 잰다. + +**작업.** + +| | 무엇 | 보는 값 | +|---|---|---| +| W1 | 쌓인 보내기 20,000개 풀기(64 B, 1 KiB, 16 KiB) | 전달/s, 전달당 CPU(양쪽의 user+sys), 전달당 선 위 바이트 | +| W2 | 한 줄 왕복(메신저): 저장에서 상대의 저장 확인까지 | p50, p99 지연 | +| W3 | 처음 만남: 프로세스 시작부터 첫 기록의 적용까지 | 시간, 왕복 수 | +| W4 | 손실과 지연: RTT 20·100 ms, 손실 1·5 %, 재정렬 | 처리량, 꼬리 지연, 재전송 바이트 | +| W5 | 쉬는 전달선 1,000개 | 세션당, 전달선당 메모리 | +| W6 | 공정성: 50 Mbit/s 병목에서 TCP CUBIC 흐름과 나란히 | 대역의 몫 | +| W7 | 주소 바뀜: 전달 중에 보내는 쪽의 포트를 바꿈 | 끊김 시간, 잃거나 겹친 기록 | + +**조건.** + +- 장치는 Raspberry Pi 5(A76 네 개)다. 지연, 손실, 재정렬, 대역 제한은 비특권 네트워크 이름공간(`unshare -rn`)과 netem·tbf로 + 만든다. 이 Pi에서 되는 것을 확인했다(한쪽 10 ms 지연으로 RTT 20 ms). +- 저장소는 두 가지로 한다. RAM(tmpfs)에서는 전송 비용이 드러나고, SD 카드에서는 실제 모습이 나온다 + ([092](../research/092-tpcc.md)의 저장 장치 차이를 참고한다). +- 후보는 번갈아 돌린다. 세 라운드 이상 하고, 원시 결과를 `research/experiments/`에 남긴다. 처리량과 함께 CPU, 메모리, 선 위 + 바이트를 적는다. 헤더 바이트 절감만으로 우위를 말하지 않는다. + +**0단계, 비용 분해.** W1을 지금의 전달과 quinn 기준선에서 돌린다. 전달 하나의 CPU를 적용·저장, 형식(JSON이나 기록), 암호, +시스템 호출, 복사, 전송 상태(ACK, 타이머)로 나눈다. `perf`를 쓸 수 있으면 쓰고, 없으면 계측으로 한다. + +## 중단 기준 + +| 단계 | 조건 | 결정 | +|---|---|---| +| 0 비용 분해 | RAM 저장소에서 전송(형식, 암호, 시스템 호출, 복사, 전송 상태)이 전달당 CPU의 30 % 미만이다 | 새 전송을 만들지 않는다. (나)를 쓰고 이 문서를 닫는다 | +| 1 시제품(한 전달선) | W1에서 CPU당 전달이 가장 좋은 QUIC 기준선의 1.5배 미만이다 | 멈추고 (나)를 쓴다 | +| 채택 | 조건 모두를 만족한다: <br>- W1 CPU당 전달 2배 이상<br>- W3 한 왕복 적음<br>- W5 쉬는 전달선당 메모리 1/4 이하<br>- W4 처리량이 QUIC보다 10 % 넘게 나쁘지 않음<br>- W6 TCP와의 몫 차이 ±25 % 안<br>- 아래 검사 모두 통과 | 노드 사이 전달의 기본으로 삼는다. HTTP 전달은 경계로 남는다 | +| 언제든 | 무작위 검사에서 한 번이라도 잃거나 겹친다. 검사는 kill -9와 손실·중복·재정렬·변조·잘림 퍼징이다 | 되돌리고 원인을 찾는다 | +| 언제든 | 보안 검사 가운데 하나라도 통과하지 못한다. 검사는 증폭 한도, 쿠키, 재전송된 패킷, 보낸 이 위장, 변조된 머리다 | 인터넷용으로 켜지 않는다 | +| 언제든 | 전송 코드가 C 5,000줄이나 `text` 64 KiB를 넘는다 | 설계를 다시 본다. QUIC 구현을 다시 만드는 일이 되어서는 안 된다 | + +"차별화가 없다"도 유효한 결론이다([000](../research/000-working-method.md)). 그때는 (나)로 옮기고, 이 문서는 그 근거로 남는다. + +## 순서 + +1. 0단계 비용 분해(지금의 전달, quinn 기준선) → 첫 중단 기준 +2. quinn과 ngtcp2에 같은 의미의 전달 하네스를 만든다 +3. 시제품: 한 세션, 한 전달선, RFC 9002 손실 탐지, NewReno, 저장소에서 재전송, 정적 키 첫 비행 +4. W1–W4 측정 → 둘째 중단 기준 +5. 핸드셰이크 완성(임시 키, 쿠키), 경로 바뀜, 여러 전달선, 허가 → W5–W7, 보안 검사 +6. 채택 여부. 명세 문서 |
