aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/design/006-native-protocol.md3
-rw-r--r--docs/design/011-pure-transport.md199
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. 채택 여부. 명세 문서