diff options
| author | eunhhu <[email protected]> | 2026-09-28 16:41:15 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-28 16:41:15 +0100 |
| commit | 5e4cf1629482a2b36d229ffb0005ce31046eaa6c (patch) | |
| tree | fa4d26721a2331b2dfc7fb43d19b2b961419f249 /docs | |
| parent | bde7c586a054e4bce18f70fc503ef8413711b8cd (diff) | |
| download | pure-5e4cf1629482a2b36d229ffb0005ce31046eaa6c.tar.gz pure-5e4cf1629482a2b36d229ffb0005ce31046eaa6c.zip | |
Design the Pure transport: stored records sent straight over UDP (011)
Outside feedback 002 asks for a protocol of Pure's own that beats QUIC.
Pure's delivery is already stored, numbered, applied once, and sealed per
link; QUIC and TCP with TLS own the same things again (retransmission
buffers, replay protection, encryption, flow control, acknowledgment).
The design takes the end-to-end argument literally: the sending app's
store is the retransmission buffer, one packet carries both the packet
ACK and the stored-through number, a direct link is sealed once with the
nodes' session keys, first contact carries records in its first packet
(the receiver's key is wired in advance and applying once makes replays
harmless), credits come from what the next save can take, the session is
known by its key rather than its addresses, and stored bytes are the
bytes on the wire. Loss detection, congestion control and cryptography
stay standard (RFC 9002, NewReno then CUBIC, HPKE), plain UDP sockets
rather than SOCK_RAW, HTTP stays the boundary to others.
Written before any code: the decision (this transport, QUIC with Pure's
format, or neither), the open questions, the smallest checks, a
measurement plan against quinn, ngtcp2 and the current HTTP delivery
(seven workloads, RAM and SD stores, loss and delay from unprivileged
namespaces with netem, checked to work on this Pi), and stop criteria:
no transport if it is under 30 % of a delivery's CPU; stop below 1.5x
the best QUIC per CPU; adopt at 2x with a round trip fewer, a quarter of
the memory per idle link, fair to TCP, every correctness and security
check passing, and under 5,000 lines.
Diffstat (limited to 'docs')
| -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. 채택 여부. 명세 문서 |
