aboutsummaryrefslogtreecommitdiffstats
path: root/docs/design
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design')
-rw-r--r--docs/design/006-native-protocol.md5
-rw-r--r--docs/design/011-pure-transport.md318
2 files changed, 172 insertions, 151 deletions
diff --git a/docs/design/006-native-protocol.md b/docs/design/006-native-protocol.md
index c4dfd3b..09c4075 100644
--- a/docs/design/006-native-protocol.md
+++ b/docs/design/006-native-protocol.md
@@ -8,8 +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단계(비용 분해)가 판정한다.
+2026-09-28: 노드 사이 전달의 새 UDP 전송 후보는 [011 — 연결 없는 전송](011-pure-transport.md)이다. 링크의 전송
+상태를 앱 상태와 한 커밋에 두어 연결 개념을 없앤다. 아래 "새 UDP 전송은 … 별도 연구 후보로 연다"는 조건은 그 가설과
+중단 기준이 판정한다.
## 결정
diff --git a/docs/design/011-pure-transport.md b/docs/design/011-pure-transport.md
index 0d019cd..52bdd44 100644
--- a/docs/design/011-pure-transport.md
+++ b/docs/design/011-pure-transport.md
@@ -1,199 +1,219 @@
-# 011 — Pure 전송: 저장한 기록을 곧바로 보내는 UDP 전송
+# 011 — 연결 없는 전송: 전송의 상태를 앱의 데이터로
-작성일: 2026-09-28. 상태: 설계. 구현과 측정 전이다. [외부 피드백 002](../outside-feedback/feedback-002.md)의
-"QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다.
+작성일: 2026-09-28(같은 날 첫 판을 버리고 다시 씀). 상태: 설계. 구현과 측정 전이다.
+[외부 피드백 002](../outside-feedback/feedback-002.md)의 "QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다.
+
+## 첫 판을 버린 까닭
+
+첫 판(1953051)은 QUIC의 층 몇 개를 Pure가 이미 가진 장치로 대신하는 설계였다. 저장소를 재전송 버퍼로 쓰고, 봉인을 한 번만
+하고, 첫 패킷에 데이터를 싣는 것이다. 이것은 혁신이라 부를 수 없었다.
+
+- 기법이 모두 이미 있었다. 끝에서 끝 논증(1984), WireGuard, Aeron, Kafka다.
+- 기대할 수 있는 것이 상수배였다. CPU를 1.5–3배 줄이고 첫 연결에서 왕복 하나를 아끼는 정도다.
+- 무엇보다 '연결'이라는 개념을 그대로 두었다. QUIC을 조금 가볍게 한 것이었다.
+
+패킷을 깎아서 얻는 것에는 하한이 있다. 바이트, 왕복, 암호가 그 하한을 정한다. 큰 차이는 무엇을 누가 기억하느냐에서 난다.
+Pure는 앱의 상태를 트랜잭션으로 저장하는 런타임이다. 그렇다면 전송의 상태도 거기에 있을 수 있다. 그러면 연결이 필요 없다.
+
+## 결정
+
+Pure 노드 사이에는 연결이 없다.
+
+- 없는 것: connect·accept·close, 연결마다의 핸드셰이크, 재연결, 세션을 붙잡는 keepalive, 연결 풀, 앱이 받는 "connection
+ reset", 연결마다 잡아 두는 버퍼.
+- 있는 것은 넷이다.
+ - 노드: 키를 가진다.
+ - 링크: 배선한 출력 하나에서 입력 하나로 간다. 이미 앱의 배선에 있다.
+ - 경로: 주소다. 마지막으로 인증된 패킷이 온 곳의 캐시일 뿐이다.
+ - 패킷.
+- 링크의 전송 상태는 저장소의 행이다. 키 에폭, 번호, 확인, 허가가 모두 앱 상태와 같은 커밋에 원자적으로 들어간다. 메모리에
+ 있는 것은 활성 링크의 캐시와 경로마다의 혼잡 상태뿐이다.
## 쉬운 말로
-- 알고 싶은 것: Pure 노드끼리 주고받는 전달(보내기·메신저 줄)을 QUIC보다 적은 CPU·메모리·왕복으로 옮길 수 있는가.
-- 그럴 수 있다고 보는 까닭: Pure의 전달은 이미 저장되고, 번호가 붙고, 받는 쪽에서 한 번만 적용되고, 봉인된다. QUIC과 TCP+TLS는
- 같은 일(재전송 버퍼, 재전송 방지, 암호, 흐름 제어, 확인 응답)을 한 번 더 한다. 그 겹친 층을 걷어 낼 수 있는지를 본다.
-- 사용 경험의 차이(가설): 메신저 줄이 상대 화면에 닿는 시간이 줄고, 처음 연결할 때 기다림이 없다. 와이파이에서 LTE로 바뀌어도
- 전달이 끊기지 않는다. 노드 사이에 HTTPS 인증서가 필요 없다.
-- 아직 모르는 것: 전달 하나의 비용 가운데 전송이 차지하는 몫이 얼마인지. 몫이 작으면 새 프로토콜로 얻을 것도 작다. 그래서 그것부터 잰다.
-- 근거: 지금은 문헌과 기존 측정([086](../research/086-sealed-sends.md), [088](../research/088-sealed-sessions.md))뿐이다.
- QUIC 대비 수치는 아직 없다.
+- 서버를 kill -9 하고 다시 띄워도, 붙어 있던 기기들은 알아채지 못한다. 핸드셰이크도 재연결 폭주도 없고, 잃거나 겹치는 메시지도
+ 없다.
+- 기기 100만 대가 붙은 노드가 쉬는 기기에는 메모리를 쓰지 않는다. 쉬는 기기는 저장소의 행 하나다.
+- 기기가 잠에서 깨거나 와이파이에서 LTE로 바뀌면 첫 패킷으로 이어진다.
+- 저장소를 다른 기계로 옮기면 링크가 그대로 따라간다. 장애 조치가 곧 저장소 옮기기다.
+- 근거: 아직 설계뿐이다. 아래 가설을 측정으로 확인한다.
## 착수 기준
-- **바꿀 결정.** 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)).
+- **바꿀 결정.** 노드 사이의 전달을 무엇으로 할지 정한다. HTTP 전달은 어느 경우든 제3자와 브라우저를 위한 경계로 남는다
+ ([006](006-native-protocol.md)).
+ - (가) 연결 없는 전송
+ - (나) QUIC 위의 Pure 형식
+ - (다) 지금의 HTTP 전달([083](../research/083-delivery-over-http.md), [088](../research/088-sealed-sessions.md))
- **남은 질문.**
- 1. 전달 하나의 CPU 가운데 전송(형식, 암호, 시스템 호출, 복사, 확인 응답, 전송 상태)의 몫은 얼마이고, 적용·저장의 몫은 얼마인가.
- 2. 보내는 앱의 저장소를 재전송 버퍼로 쓸 때 복사나 지연이 늘지 않는가.
- 3. 처음 만나는 연결의 첫 패킷에 기록을 실어도 안전한가. 증폭, CPU 소모 공격, 전방 비밀성이 걸린다.
- 4. 표준 혼잡 제어를 그대로 쓰면서 공정성을 지킬 수 있는가.
- 5. UDP가 막힌 곳에서는 무엇으로 이어 가는가.
+ 1. 전송 상태를 앱 커밋에 묶으면 패킷마다 동기화가 필요해지지 않는가.
+ 2. 쉬는 링크 하나의 비용이 정말 행 하나(약 100 B)인가. 깨울 때 첫 패킷에 드는 시간은 얼마인가.
+ 3. 재시작 뒤 재전송 공격을 막는 창을 잃지 않는가. 가장 가까운 선행 기술인 OSCORE는 여기서 한 왕복이 든다.
+ 4. 핸드셰이크가 없을 때 전방 비밀성, CPU 소모 공격, 증폭을 어떻게 다루는가.
+ 5. 활성 링크의 전달당 CPU가 QUIC 기준선보다 나쁘지 않은가.
- **최소 확인.**
- 1. 먼저 비용 분해다. 쌓인 보내기 풀기(W1)를 지금의 전달과 quinn 기준선에서 돌린다. 전달 하나의 CPU를 층별로 나눈다.
- 2. 그다음 시제품이다. 한 세션·한 전달선만 둔다. 손실 탐지는 RFC 9002, 혼잡 제어는 NewReno로 하고, 재전송은 저장소에서 읽는다.
- 이것을 W1과 손실·지연(W4)에서 가장 좋은 QUIC 기준선과 번갈아 잰다.
-- **중단 기준.** 아래 "중단 기준" 절의 수치다. 요약하면 셋이다.
- - 전송의 몫이 작으면 만들지 않는다.
- - 시제품이 CPU당 전달에서 1.5배를 못 넘으면 멈추고 (나)를 쓴다.
- - 한 번이라도 잃거나 겹치면 되돌린다.
+ 1. 링크 행을 앱 상태에 두는 시제품을 만든다. 한 노드 쌍에 링크 여럿을 둔다. 어느 쪽이든 kill -9로 죽였다 살린 뒤, 핸드셰이크
+ 없이 다음 패킷으로 이어지고 잃거나 겹치는 기록이 없는지 본다.
+ 2. 쉬는 링크를 1만, 10만, 100만 개로 늘린다. 프로세스 메모리와 저장소 크기의 기울기를 quinn·ngtcp2 연결당 메모리와 댄다.
+- **중단 기준.** 아래 "중단 기준" 절에 둔다.
## 의심하는 관례
| 관례 | 이 설계 |
|---|---|
-| 전송이 신뢰성을 가진다(재전송 버퍼, 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에는 끝점의 장치(저장, 번호, 한 번
-적용)가 이미 있으므로, 전송은 그것을 빌려 쓰고 제 것을 두지 않는다.
+| 통신하려면 연결을 열고(핸드셰이크) 끝나면 닫는다 | 링크는 배선할 때 생긴다. 서로의 키를 알므로 첫 패킷부터 봉인된 기록이 간다 |
+| 연결 상태는 프로세스 메모리에 있다 | 저장소의 행이다. 메모리는 그 캐시다 |
+| 서버를 다시 시작하면 연결이 모두 끊기고 클라이언트가 다시 잇는다 | 재시작해도 행이 남는다. 다음 패킷이 그대로 이어진다 |
+| 연결은 주소로 식별한다 | 키로 식별한다. 주소는 마지막으로 인증된 패킷이 온 곳이다 |
+| 전송의 재전송 버퍼, 앱의 outbox와 재시도, 받는 쪽의 중복 제거가 따로 있다 | 보내는 앱의 저장소가 재전송 버퍼다. 받는 쪽이 저장해 둔 들은 번호가 곧 재전송 방지다 |
+| 전송 암호와 본문 봉인을 따로 한다 | 직접 링크는 링크의 키 에폭으로 한 번 봉인한다. 중계를 지날 때만 기록을 따로 봉인한다 |
+| 흐름 제어는 바이트 창이다 | 받는 쪽이 다음 저장에 들일 수 있는 만큼을 허가한다 |
+| 전방 비밀성은 연결마다의 임시 키 교환으로 얻는다 | 연결이 없으므로 에폭 교체(래칫)로 얻는다. 기준은 핸드셰이크가 아니라 키의 나이다 |
+| 선 위의 형식과 저장 형식이 다르다 | 저장한 기록의 바이트와 필드 표가 곧 선 위의 바이트다 |
## 구조(후보)
-패킷의 바이트 배치는 아직 고정하지 않는다. 시제품과 측정으로 정한다.
+패킷의 바이트 배치는 시제품과 측정으로 정한다.
+
+**링크의 행(저장).** 목록(B+트리, [090](../research/090-tables.md))의 한 항목이다. 대략 100 B다.
-**개념.**
+- 상대 노드: 키 표의 번호
+- 링크 이름: 출력, 입력
+- 키 에폭: 번호, 보내는 키와 받는 키(AEAD 키와 IV 기반)
+- 저장 확인된 번호. 보낸 기록 자체는 이미 outbox에 있다.
+- 들은 번호. `pure_app_deliver`가 이미 적용과 함께 저장한다.
+- 패킷 번호 예약 상한. 번호를 K개씩 예약해 커밋하고, 재시작하면 상한부터 쓴다(RFC 8613 B.1.1과 같은 방법). 이 커밋은 앱의
+ 저장에 실려 가므로 따로 동기화하지 않는다.
-- 노드: 키 한 쌍을 갖는다(088의 `--key`).
-- 세션: 두 노드 사이에 하나를 둔다. 키, 혼잡 제어기, 패킷 번호 공간을 하나씩 갖는다. 경로(주소 쌍)는 바뀔 수 있다.
-- 전달선: 보내는 앱의 출력 하나에서 받는 앱의 입력 하나로 간다. 저장된 번호 공간을 갖는다. 한 세션의 여러 전달선은 혼잡
- 제어기를 나눠 쓰고, 순서는 전달선마다 따로 지킨다. 한 전달선의 손실이 다른 전달선을 막지 않는다.
+**메모리(캐시).**
+
+- 활성 링크의 행과 AEAD 문맥
+- 경로마다: 주소, RTT, 혼잡 창, 손실 탐지 상태(RFC 9002). 모두 휘발한다. 다시 시작하면 처음 창부터 간다. TCP가 쉬었다 다시
+ 보낼 때와 같은 규칙이다(RFC 5681 §4.1).
+- 받았지만 아직 저장하지 않은 기록. 허가가 그 양을 묶는다. 기록 하나만큼은 늘 허가해 진행이 막히지 않게 한다.
**패킷.**
-- 평문 머리: 판, 종류, 세션 식별자, 패킷 번호. 패킷 번호는 손실 탐지와 논스를 위한 것이다. 기록 번호와는 다른 공간이며,
- 한 번 쓴 번호를 다시 쓰지 않는다.
-- 봉인된 몸(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)의 교착 조건).
+- 평문 머리: 판, 종류, 링크 식별자, 패킷 번호. 링크 식별자는 받는 쪽이 행을 곧바로 찾는 짧은 번호다.
+- 봉인된 몸에는 프레임이 든다.
+ - 기록: 링크, 첫 번호, 개수, 저장한 바이트와 필드 표
+ - 패킷 ACK
+ - 저장함: 이 번호까지 저장했다
+ - 허가
+ - 에폭 바꿈
+ - 핑
+- 잃은 패킷은 다시 보내지 않는다. 그 안의 확인되지 않은 기록을 저장소에서 읽어 새 패킷에 싣는다.
+- 확인은 두 단계다. 패킷 ACK는 받았다는 뜻이고, 보내는 쪽은 그 기록을 다시 보내지 않는다. "저장함"은 동기화했다는 뜻이고,
+ 보내는 쪽은 그때 덜어 낸다(`pure_app_trim`). 받는 쪽이 저장 전에 죽으면, 이어질 때 "저장함" 다음 번호부터 다시 간다.
+
+**키.**
+
+- 첫 에폭은 배선할 때 두 노드의 정적 키로 만든다(HPKE auth 모드, Noise KK 모양). 첫 패킷부터 쓴다.
+- 에폭은 주기적으로 바꾼다. 기준은 시간과 바이트다. 임시 키 교환을 패킷에 싣고, 새 에폭은 저장하며, 지난 에폭 키는 지운다.
+ 전방 비밀성은 이렇게 에폭 단위로 얻는다. 첫 에폭의 기록은 정적 키로만 보호된다. QUIC 0-RTT와 같은 한계다.
+- 재전송 공격:
+ - 기록은 저장된 들은 번호로 걸러지므로, 재시작 뒤에도 창을 잃지 않는다. OSCORE가 재부팅 뒤 쓰는 Echo 왕복(RFC 8613 B.1.2)이
+ 필요 없다.
+ - 패킷 안의 나머지(ACK, 허가)는 최댓값으로만 받아들인다. 그래서 다시 보낸 패킷이 상태를 되돌리지 못한다.
+- CPU 소모: 링크 식별자로 행을 찾고 AEAD 한 번으로 버린다. 비대칭 연산은 에폭 바꿈에만 있고, 비율을 제한한다.
+- 증폭: 새 주소로는 확인하기 전에 받은 바이트의 3배까지만 보낸다(RFC 9000 §8.1과 같은 규칙).
+- NAT 뒤의 쉬는 기기에 먼저 보내는 일은 이 설계가 풀지 않는다. 푸시나 기기의 주기적 패킷이 여전히 필요하다. 이 설계가 보장하는
+ 것은, 기기가 깨어 보낸 첫 패킷으로 핸드셰이크 없이 이어진다는 것이다.
+
+**손실과 혼잡은 표준대로 한다.** 손실 탐지는 RFC 9002, 혼잡 제어는 NewReno에서 CUBIC(RFC 9438)으로 간다. 페이싱, ECN,
+DPLPMTUD(RFC 8899)를 쓴다. 자체 혼잡 제어 알고리즘은 만들지 않는다. 다른 흐름을 굶기는 전송은 해악이다.
**송수신 경로.**
-- `sendmmsg`와 `recvmmsg`를 쓰고, Linux에서는 UDP GSO(`UDP_SEGMENT`)와 GRO(`UDP_GRO`)도 쓴다.
-- 저장소의 기록 바이트를 AEAD의 출력 버퍼(곧 패킷 버퍼)로 봉인하는 것이 유일한 복사다.
-- `SOCK_RAW`와 커널 우회는 쓰지 않는다. `SOCK_RAW`는 권한이 필요하고 NAT와 방화벽을 지나지 못한다. 필요하다면 측정이 그 비용을
- 보여 준 뒤에 따로 판단한다([006](006-native-protocol.md)).
+- `sendmmsg`와 `recvmmsg`를 쓰고, Linux에서는 UDP GSO와 GRO도 쓴다.
+- 저장소의 바이트를 패킷 버퍼로 봉인하는 것이 유일한 복사다.
+- `SOCK_RAW`와 커널 우회는 쓰지 않는다. 권한이 필요하고 NAT를 지나지 못한다.
**경계(갈라파고스 금지).**
-- UDP가 막힌 곳에서는 기존 HTTP 전달(083)로 돌아간다. 같은 프레임을 TCP 위의 길이 붙은 레코드로 실을 수도 있다. 평문으로
- 낮추지는 않는다.
-- 제3자와 브라우저는 HTTP 경계로 잇는다. 새 전송은 Pure 노드끼리 쓰는 것이다.
-- 암호는 표준만 쓴다(HPKE RFC 9180, X25519, HKDF, AES-128-GCM이나 ChaCha20-Poly1305). 혼잡 제어도 표준이다.
-- 명세를 이 저장소에 문서로 둔다. 판 협상, 모르는 프레임의 처리, 오류 부호를 정해 다른 구현이 만들어질 수 있게 한다.
+- UDP가 막히면 HTTP 전달로 간다. 평문으로 낮추지는 않는다.
+- 제3자는 HTTP로 잇는다.
+- 암호와 혼잡 제어는 표준만 쓴다.
+- 명세를 문서로 두어 다른 구현이 가능하게 한다. 판 협상과 오류 부호도 거기서 정한다.
## 이미 있는 것과의 차이
-개별 기법은 모두 이미 있다. 새 이름으로 포장하지 않는다.
-
-| 이미 있는 것 | 가져오는 것 | 다른 점 |
+| 이미 있는 것 | 닮은 점 | 다른 점 |
|---|---|---|
-| QUIC(RFC 9000–9002) | 손실 탐지, 혼잡 제어, 증폭 한도, 프레임 단위 재전송 | QUIC은 범용 스트림이라 신뢰성과 버퍼, 암호를 전송이 소유한다 |
-| WireGuard(Donenfeld, NDSS 2017), Noise IK | 미리 아는 정적 키, 첫 메시지의 키 교환, 쿠키, 카운터 논스, 키로 식별하는 세션과 로밍 | WireGuard는 신뢰성이 없는 터널이다 |
-| Aeron | 로그를 버퍼로 쓰는 UDP 메시징, 받는 위치에 따른 흐름 제어 | Aeron의 로그는 메모리이고, 앱의 저장이나 한 번 적용과 묶이지 않으며, 암호가 없다 |
-| Homa(SIGCOMM 2018) | 받는 쪽이 주는 허가 | 데이터센터용이다 |
-| Kafka | 저장 형식이 곧 선 위의 형식(sendfile) | 전송은 TCP다 |
-| KCP 같은 게임용 UDP | — | 혼잡 제어를 버리고 지연을 산다. 이 설계는 그러지 않는다 |
+| CoAP·OSCORE(RFC 7252, 8613), EDHOC(RFC 9528) | 가장 가깝다. 미리 세운 보안 문맥, 메시지 단위 AEAD, 재부팅을 견디는 번호 예약 | 재부팅하면 재전송 창을 잃어 Echo 왕복으로 다시 세운다. 신뢰성은 수명이 짧은 메시지 ID 창이다. 혼잡 제어가 단순하다. 앱의 적용과 원자적이지 않다 |
+| MQTT 지속 세션 | 끊긴 클라이언트의 구독과 메시지를 브로커가 보관한다 | TCP(와 TLS) 연결은 다시 연다 |
+| Signal | 래칫 상태를 기기에 저장해 재시작을 견딘다 | 전송(TLS, 웹소켓)은 따로 연다 |
+| Orleans 가상 액터 | 액터가 저장에서 필요할 때 깨어난다 | 사일로 사이는 TCP 연결이다 |
+| WireGuard, Noise | 키로 식별하는 피어, 로밍, 카운터 논스 | 세션 키가 메모리에 있어 재시작하면 새 핸드셰이크를 한다. 신뢰성이 없다 |
+| QUIC | 연결 ID로 주소 이동, 티켓으로 0-RTT 재개 | 연결 상태가 메모리에 있어 서버가 재시작하면 새 연결을 맺는다 |
+| 끝에서 끝 논증, Aeron, Kafka | 끝점의 신뢰성, 로그를 버퍼로, 저장 형식이 곧 선 위 형식 | 연결 모델은 그대로다 |
-가설은 조합에 있다. 이미 저장, 번호, 한 번 적용, 봉인 세션을 가진 런타임에서는 전송이 그것들을 빌려 쓸 수 있다. 그러면 전송의
-신뢰성, 전송 암호, 앱 전달 프로토콜(HTTP와 헤더, JSON)의 세 층이 한 층으로 준다. 학술적 신규성은 이 측정과 별도로 판단한다.
+기법 하나하나는 새것이 아니다. 가설은 모델에 있다. 전송 전체를 앱의 트랜잭션 저장에 원자적으로 넣는다. 키 에폭, 신뢰성,
+재전송 방지, 흐름, 앱의 적용이 모두 거기에 든다. 그러면 연결이라는 개념이 사라진다. 이 조합의 신규성은 문헌 조사로 다시
+확인하며, 측정 전에는 주장하지 않는다.
-## 측정 계획: quinn·ngtcp2 대비
+## 가설
+
+- **H1.** 쉬는 링크의 비용은 저장소의 행(약 100 B)이다. 프로세스 메모리는 활성 링크 수에만 비례한다.
+- **H2.** 어느 쪽 노드를 kill -9 해도, 다시 뜬 뒤 첫 패킷으로 이어진다. 핸드셰이크는 없고, 잃거나 겹치는 기록도 없다.
+- **H3.** 저장소를 다른 프로세스나 주소로 옮겨도 링크가 이어진다.
+- **H4.** 활성 링크의 전달당 CPU는 QUIC 기준선보다 나쁘지 않다. 층을 합친 이득이 행을 찾는 비용을 덮는다.
-**비교 대상.** 모두 같은 의미를 지킨다. 보내는 앱이 저장한 것만 보내고, 전달선마다 순서대로 보내며, 받는 쪽이 저장한 뒤에만
-덜어 내고, 받는 쪽은 한 번만 적용한다. 암호는 모두 AES-128-GCM이다(이 A76에서 가장 빠르다, [086](../research/086-sealed-sends.md)).
+## 측정 계획: quinn·ngtcp2 대비
-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 전송 시제품
+**비교 대상.** 모두 같은 의미를 지킨다. 보내는 앱이 저장한 것만 보내고, 링크마다 순서대로 보내며, 받는 쪽이 저장한 뒤에만
+덜어 내고, 한 번만 적용한다. 암호는 AES-128-GCM이다.
-QUIC 기준선도 같은 의미를 위해 저장소에 기록을 남기고 앱 수준의 "저장함"을 기다린다. 두 겹의 버퍼는 그 구조에 딸린 비용이므로
-그대로 잰다.
+- quinn 0.11: 이 Pi의 cargo 저장소에 있다.
+- ngtcp2: 원본에서 OpenSSL 3.5 백엔드로 빌드한다.
+- OpenSSL 3.5.7의 QUIC: 참고로 둔다.
+- 지금의 HTTP 전달
+- 시제품
**작업.**
-| | 무엇 | 보는 값 |
-|---|---|---|
-| 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 | 주소 바뀜: 전달 중에 보내는 쪽의 포트를 바꿈 | 끊김 시간, 잃거나 겹친 기록 |
+| | 무엇 | 보는 값 | QUIC 쪽 |
+|---|---|---|---|
+| M1 | 쉬는 링크 1만, 10만, 100만 개 | 프로세스 RSS와 저장소 크기의 링크당 기울기, 깨우는 첫 패킷의 지연 | 이 Pi에서 늘릴 수 있는 만큼 늘려 연결당 기울기를 잰다 |
+| M2 | 활성 링크 1만 개에 부하를 건 채 kill -9 | 다시 전달하기까지의 시간, 양쪽 CPU와 비대칭 연산 횟수, 잃음·겹침 | 연결 1만 개를 다시 맺는다(1-RTT, 티켓 0-RTT) |
+| M3 | 저장소를 다른 프로세스로 옮김, 주소가 바뀜 | 끊긴 시간, 잃음·겹침 | 주소 이동은 같은 프로세스 안에서만 된다 |
+| M4 | 처음 만남 | 첫 기록을 적용하기까지의 왕복 수 | 1-RTT |
+| M5 | 쌓인 보내기 20,000개 풀기(64 B, 1 KiB, 16 KiB) | 전달/s, 전달당 CPU | 가드레일 |
+| M6 | RTT 20·100 ms에 손실 1·5 %와 재정렬. 50 Mbit/s 병목에서 TCP CUBIC과 나란히 | 처리량, 꼬리 지연, 대역의 몫 | 가드레일 |
**조건.**
-- 장치는 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`를 쓸 수 있으면 쓰고, 없으면 계측으로 한다.
+- 장치는 Raspberry Pi 5다. 지연·손실·병목은 비특권 네트워크 이름공간과 netem·tbf로 만든다. 이 Pi에서 되는 것을 확인했다(한쪽
+ 10 ms로 RTT 20 ms).
+- 저장소는 RAM(tmpfs)과 SD 카드 두 가지로 한다.
+- 후보는 번갈아 돌리고, 세 라운드 이상 한다. 원시 결과는 `research/experiments/`에 남긴다.
## 중단 기준
-| 단계 | 조건 | 결정 |
-|---|---|---|
-| 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 구현을 다시 만드는 일이 되어서는 안 된다 |
+| 조건 | 결정 |
+|---|---|
+| H2·H3의 검사에서 한 번이라도 잃거나 겹친다. 또는 재시작 뒤 논스를 다시 쓴다 | 되돌리고 원인을 찾는다 |
+| M1: 쉬는 링크당 프로세스 메모리가 QUIC 연결당 메모리의 1/100 이하가 아니다 | 모델의 주장이 틀렸다. 다시 설계한다 |
+| M2: 재시작 뒤 핸드셰이크 없이 이어지지 않는다. 또는 이어지는 데 링크 수에 비례하는 비대칭 연산이 든다 | 다시 설계한다 |
+| 전송 상태 때문에 패킷마다 동기화가 필요해진다(지연이 RTT와 저장 한 번을 넘는다) | 예약 방식을 다시 본다 |
+| M5·M6: 전달당 CPU가 QUIC보다 10 % 넘게 나쁘다. 또는 TCP와의 몫이 ±25 %를 벗어난다 | 고친 뒤에 채택한다 |
+| 보안 검사(증폭, 보낸 이 위장, 재전송, 변조)에 실패한다 | 인터넷용으로 켜지 않는다 |
+| 전송 코드가 C 5,000줄을 넘는다 | 설계를 다시 본다 |
+| H1–H4가 모두 서고 검사를 모두 통과한다 | 노드 사이 전달의 기본으로 삼는다 |
+
+## 이 너머: 기다리지 않는 상호작용
+
+연결을 없애도 왕복 자체는 남는다. 빛의 속도는 줄일 수 없다. 다음 후보는 결정적 모듈의 투기 실행이다.
-"차별화가 없다"도 유효한 결론이다([000](../research/000-working-method.md)). 그때는 (나)로 옮기고, 이 문서는 그 근거로 남는다.
+Pure의 모듈은 검증되어 결정적이고, 이벤트의 실행량에 상한이 있다. 보내는 노드가 받는 앱의 모듈과 관련 상태의 복제를 가지면,
+답을 기다리지 않고 받는 쪽의 결과를 미리 계산해 보여 줄 수 있다. 그 뒤 실제 답으로 확정하거나 되돌린다. 게임의 롤백 넷코드와
+Replicache의 뮤테이터와 같은 생각이다. 다른 점은 앱 코드를 두 벌 쓰지 않는다는 것이다. 이것은 전송이 아니라 런타임의 능력이라
+따로 설계한다.
## 순서
-1. 0단계 비용 분해(지금의 전달, quinn 기준선) → 첫 중단 기준
-2. quinn과 ngtcp2에 같은 의미의 전달 하네스를 만든다
-3. 시제품: 한 세션, 한 전달선, RFC 9002 손실 탐지, NewReno, 저장소에서 재전송, 정적 키 첫 비행
-4. W1–W4 측정 → 둘째 중단 기준
-5. 핸드셰이크 완성(임시 키, 쿠키), 경로 바뀜, 여러 전달선, 허가 → W5–W7, 보안 검사
-6. 채택 여부. 명세 문서
+1. 링크 행 시제품을 만든다: 한 노드 쌍, 링크 여럿, 에폭 하나, NewReno, 저장소에서 재전송. 여기에 kill -9 검사(H2)를 붙인다.
+2. quinn·ngtcp2 하네스를 같은 의미로 만든다.
+3. M1(메모리 기울기), M2(재시작), M4를 잰다.
+4. 에폭 바꿈, 경로 바뀜, 저장소 옮김(H3, M3)과 보안 검사를 한다.
+5. M5·M6 가드레일을 잰다. 채택 여부를 정하고 명세를 쓴다.