From 745efcd4f19b59bcfb131df2d127a3387a544e84 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 28 Sep 2026 17:37:41 +0100 Subject: Link two nodes without a connection over UDP (093, design 011's first stage) pure-serve --send OUTPUT=pure://host:port/INPUT sends an output's saved sends to another node's --link-port with no handshake: the first packet carries records. Keys come from the two nodes' keys (X25519, HKDF-SHA256, AES-128-GCM), with a random run in each packet's header so no nonce repeats across restarts; nothing is stored for them. What lasts is the apps' own: the sender's outbox and the receiver's heard numbers (pure_app_heard, new). The sender detects losses as RFC 9002 says, with NewReno, and trims a send once the receiver says it stored it; the receiver hands the owner whole records in order, which it applies from the one after the source's last heard number, stopping at a gap, and answers how far it stored with every acknowledgment. Stored bytes are the bytes on the wire: an output and an input of one shape need no JSON. server_links_udp kills the receiver, the sender, then both (-9) while four clients buy: every saved order is applied once, with no handshake (five runs, 10k-11k orders each). server_links_udp_lossy does it again in a namespace of its own with the link's packets delayed, lost (3 %) and reordered (10 %) by netem (three runs, 11k-12k orders). --- docs/research/093-connectionless-link.md | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/research/093-connectionless-link.md (limited to 'docs/research/093-connectionless-link.md') diff --git a/docs/research/093-connectionless-link.md b/docs/research/093-connectionless-link.md new file mode 100644 index 0000000..ad36e47 --- /dev/null +++ b/docs/research/093-connectionless-link.md @@ -0,0 +1,84 @@ +# 093 — 연결 없는 링크: 핸드셰이크 없이, 재시작을 모르게 + +2026-09-28. [설계 011](../design/011-pure-transport.md)의 첫 단계다. + +착수 기준: + +- **바꿀 결정.** 011의 모델이 실제로 재시작을 견디는지 본다. 링크의 오래 가는 상태는 앱이 이미 가진 것(보내는 앱의 outbox, 받는 + 앱의 들은 번호)이고, 키는 두 노드의 키에서 나온다. 견디지 못하면 설계를 다시 본다. +- **최소 확인.** 두 `pure-serve`를 UDP 링크로 잇는다. 네 클라이언트가 주문하는 동안 받는 노드, 보내는 노드, 둘 다를 차례로 + `kill -9`로 죽였다 살린다. 받는 앱이 보내는 앱이 저장한 주문을 정확히 한 번씩 적용했는지 본다. 손실과 재정렬 아래에서도 본다. +- **중단 기준.** 한 번이라도 잃거나 겹치면 되돌린다. 재시작 뒤 이어지는 데 핸드셰이크(왕복)가 필요하면 다시 설계한다. + +## 쉬운 말로 + +- 알아낸 것: 두 노드가 연결을 맺지 않고 전달한다. 어느 쪽을 `kill -9` 해도 다시 뜬 뒤 첫 패킷으로 이어진다. 잃거나 겹친 + 주문은 없었다. 손실 3 %, 재정렬 10 %에서도 그렇다. +- 아직 모르는 것: QUIC(quinn, ngtcp2)과 댄 수치다. 쉬는 링크의 메모리, 재시작에 드는 시간과 CPU, 전달당 CPU를 아직 재지 않았다. +- 사용 경험의 차이: 노드를 다시 띄워도 상대 노드와 사용자는 모른다. 재연결도, 다시 맺는 암호 교환도 없다. + +## 한 것 + +**키.** + +- 두 노드의 정적 키로 X25519를 하고, HKDF-SHA256을 거쳐 AES-128-GCM 키를 만든다. +- 보내는 쪽은 실행마다 무작위 run(64비트)을 패킷 머리에 싣는다. 키가 run마다 다르므로 재시작해도 논스가 겹치지 않는다. +- 저장할 키도 예약할 번호도 없다. 011은 OSCORE처럼 패킷 번호를 예약해 저장하는 방법을 후보로 적었다. run 소금이 더 단순하다. + 쓰기가 하나도 없다. +- 받는 쪽은 처음 보는 run의 첫 패킷에서 키를 만든다. 그 패킷이 열려야만 자리를 준다. 그래서 위조 패킷이 정상 run을 밀어내지 + 못한다. + +**링크의 오래 가는 상태는 앱이 이미 가진 것이다.** + +- 보내는 앱의 outbox, 받는 앱의 들은 번호다. 들은 번호를 읽는 `pure_app_heard`만 코어에 더했다. +- 새로 저장하는 전송 상태는 없다. 011이 적은 "링크의 행(약 100 B)"도 필요 없었다. 첫 에폭에서 링크가 저장소에 더하는 것은 + 0 바이트다. + +**보내는 쪽([serve/link.c](../../serve/link.c)의 `PureLink`).** + +- 전달선과 같은 자리에 붙는다. `--send OUTPUT=pure://host:port/INPUT`로 열고, 받는 노드의 키는 `--seal`로 준다. +- outbox의 기록을 패킷에 채워 보낸다. 한 패킷에 여러 기록이 들고, 큰 기록은 1000 B 조각으로 나눈다. +- 손실 탐지는 RFC 9002를 따른다: 패킷 셋, 또는 RTT의 9/8이 지나면 잃은 것으로 본다. PTO는 받는 쪽이 없는 동안 1초를 넘지 + 않게 했다. 혼잡 제어는 NewReno다. +- 확인은 두 단계다. 패킷 ACK는 받았다는 뜻이라 그 기록을 다시 보내지 않는다. STORED는 저장했다는 뜻이라 그때 outbox에서 덜어 + 낸다. +- 받았다는 기록이 1초 동안 저장되지 않으면 다시 보낸다. 받는 쪽이 저장 전에 죽은 경우다. +- 흐름 허가는 저장되지 않은 가장 오래된 기록부터 센다. 재시작한 보내는 쪽은 받는 쪽이 어디까지 저장했는지 아직 모르기 때문이다. + +**받는 쪽(리스너, `--link-port`).** + +- 기록을 번호순으로 모아, 이어지는 것만 소유자에게 넘긴다. +- 소유자는 그 출처의 들은 번호 다음부터 적용하고, 빈틈을 만나면 멈춘다. 재시작한 리스너는 들은 번호를 모르므로, 그것을 소유자에게 + 묻는 셈이다. +- 저장이 끝나면 STORED(들은 번호, 빈틈 표시)를 돌려보낸다. 빈틈 표시를 받은 보내는 쪽은 그 뒤를 다시 보낸다. +- STORED는 모든 ACK에 실어 보낸다. 그래서 잃은 STORED도 다음 패킷에서 다시 전해진다. + +**저장된 바이트가 곧 선 위의 바이트다.** 보내는 출력과 받는 입력의 모양이 같으면(모양 요약값으로 확인한다), 받는 쪽은 JSON도 +필드 표도 없이 저장된 바이트를 그대로 `pure_app_deliver`에 넘긴다. + +## 검사 + +- `server_links_udp`: 착수 기준의 확인 그대로다. 다섯 번 연달아 통과했다. 회당 주문은 10,063–10,954건이다. 끝에는 거절되는 주문 + (수량 150)과 그 뒤의 주문을 보내, 거절이 뒤를 막지 않는 것까지 본다. +- `server_links_udp_lossy`: 같은 것을 비특권 네트워크 이름공간(`unshare -rn`)에서 한다. 링크의 UDP만 netem을 거친다(지연 + 5±2 ms, 손실 3 %, 재정렬 10 %). 세 번 통과했고, 회당 주문은 10,954–11,982건이다. + - 재정렬 때문에 잃었다고 잘못 판정해 다시 보내는 것이 많다. 한 실행에서 패킷 395개에 다시 보낸 조각이 739개였다. + - 정확성에는 영향이 없다. 효율은 다음 일이다. + +## 첫 수치 + +루프백, 주문 50건을 하나씩 보냈다. + +- 보내는 쪽: 패킷 50개, 기록 50개, 다시 보냄 0, 확인 50. +- 받는 쪽: 소유자에게 넘김 49번, 답 패킷 96개(ACK와 STORED). + +## 다음 + +- **M1.** 쉬는 링크 1만–100만의 메모리를 quinn·ngtcp2의 연결당 메모리와 댄다. 받는 쪽의 링크 상태(run 슬롯과 흐름)를 활성인 + 것만 두고 쉬는 것은 비우는 일부터 한다. +- **quinn·ngtcp2 하네스.** 같은 의미로 만든다. 그 위에서 M2(재시작)와 M4(처음 만남)를 잰다. +- **효율.** 재정렬에 맞춰 손실 판정의 문턱을 넓힌다(RFC 9002의 적응, RACK). 받는 쪽 ACK를 늦춰 묶는다. 한 노드 쌍의 링크들이 + 혼잡 제어를 나눠 쓰게 한다(지금은 링크마다 하나). +- **보안.** 에폭 교체(전방 비밀성), 새 주소의 확인과 증폭 한도, 부하 때의 쿠키. +- **한계.** 첫 에폭은 정적 키뿐이라 전방 비밀성이 없다. 정적 키끼리의 DH라서, 받는 노드의 키를 훔친 사람은 보내는 노드로 행세할 + 수 있다(KCI). 지금의 HPKE auth 모드 봉인과 같은 한계다. -- cgit v1.2.3