# 011 — 연결 없는 전송: 전송의 상태를 앱의 데이터로 작성일: 2026-09-28(같은 날 첫 판을 버리고 다시 씀). 상태: 설계. 1단계를 구현했고 M1을 쟀다. [외부 피드백 002](../outside-feedback/feedback-002.md)의 "QUIC을 능가할 자체 raw socket 프로토콜"에서 시작한다. 1단계(링크 시제품과 kill -9 검사)는 [093](../research/093-connectionless-link.md)에 있다. 어느 쪽을 죽여도 핸드셰이크 없이 이어지고, 잃거나 겹친 것은 없다. 손실과 재정렬 아래에서도 그렇다. 아래 "링크의 행"은 첫 에폭에서는 필요 없었다. 키는 두 노드의 키와 보내는 쪽 run(실행마다 무작위)에서 나오고, 번호 예약 대신 run이 논스를 가른다. 링크가 저장소에 더하는 것은 0 바이트다. M1은 [094](../research/094-idle-links.md)에 있다. 쉬는 링크 하나가 노드 메모리 132 B를 쓴다. 허용 목록 73 B와 들은 번호 약 60 B이고, 전송 상태는 0이다. quinn의 쉬는 연결은 38,106 B다. 1/289로 중단 기준(1/100)을 넘는다. 설계가 말한 "링크의 행"은 앱이 이미 가진 들은 번호였다. 그래서 들은 번호를 목록으로 바꿨고, 저장소의 한 항목은 약 20 B다. 남은 걸림돌은 M2다. 활성 상태를 비우면 비밀도 잊으므로, 재시작한 노드는 링크마다 X25519를 한 번 한다. ## 첫 판을 버린 까닭 첫 판(1953051)은 QUIC의 층 몇 개를 Pure가 이미 가진 장치로 대신하는 설계였다. 저장소를 재전송 버퍼로 쓰고, 봉인을 한 번만 하고, 첫 패킷에 데이터를 싣는 것이다. 이것은 혁신이라 부를 수 없었다. - 기법이 모두 이미 있었다. 끝에서 끝 논증(1984), WireGuard, Aeron, Kafka다. - 기대할 수 있는 것이 상수배였다. CPU를 1.5–3배 줄이고 첫 연결에서 왕복 하나를 아끼는 정도다. - 무엇보다 '연결'이라는 개념을 그대로 두었다. QUIC을 조금 가볍게 한 것이었다. 패킷을 깎아서 얻는 것에는 하한이 있다. 바이트, 왕복, 암호가 그 하한을 정한다. 큰 차이는 무엇을 누가 기억하느냐에서 난다. Pure는 앱의 상태를 트랜잭션으로 저장하는 런타임이다. 그렇다면 전송의 상태도 거기에 있을 수 있다. 그러면 연결이 필요 없다. ## 결정 Pure 노드 사이에는 연결이 없다. - 없는 것: connect·accept·close, 연결마다의 핸드셰이크, 재연결, 세션을 붙잡는 keepalive, 연결 풀, 앱이 받는 "connection reset", 연결마다 잡아 두는 버퍼. - 있는 것은 넷이다. - 노드: 키를 가진다. - 링크: 배선한 출력 하나에서 입력 하나로 간다. 이미 앱의 배선에 있다. - 경로: 주소다. 마지막으로 인증된 패킷이 온 곳의 캐시일 뿐이다. - 패킷. - 링크의 전송 상태는 저장소의 행이다. 키 에폭, 번호, 확인, 허가가 모두 앱 상태와 같은 커밋에 원자적으로 들어간다. 메모리에 있는 것은 활성 링크의 캐시와 경로마다의 혼잡 상태뿐이다. ## 쉬운 말로 - 서버를 kill -9 하고 다시 띄워도, 붙어 있던 기기들은 알아채지 못한다. 핸드셰이크도 재연결 폭주도 없고, 잃거나 겹치는 메시지도 없다. - 기기 100만 대가 붙은 노드가 쉬는 기기에는 메모리를 쓰지 않는다. 쉬는 기기는 저장소의 행 하나다. - 기기가 잠에서 깨거나 와이파이에서 LTE로 바뀌면 첫 패킷으로 이어진다. - 저장소를 다른 기계로 옮기면 링크가 그대로 따라간다. 장애 조치가 곧 저장소 옮기기다. - 근거: 아직 설계뿐이다. 아래 가설을 측정으로 확인한다. ## 착수 기준 - **바꿀 결정.** 노드 사이의 전달을 무엇으로 할지 정한다. HTTP 전달은 어느 경우든 제3자와 브라우저를 위한 경계로 남는다 ([006](006-native-protocol.md)). - (가) 연결 없는 전송 - (나) QUIC 위의 Pure 형식 - (다) 지금의 HTTP 전달([083](../research/083-delivery-over-http.md), [088](../research/088-sealed-sessions.md)) - **남은 질문.** 1. 전송 상태를 앱 커밋에 묶으면 패킷마다 동기화가 필요해지지 않는가. 2. 쉬는 링크 하나의 비용이 정말 행 하나(약 100 B)인가. 깨울 때 첫 패킷에 드는 시간은 얼마인가. 3. 재시작 뒤 재전송 공격을 막는 창을 잃지 않는가. 가장 가까운 선행 기술인 OSCORE는 여기서 한 왕복이 든다. 4. 핸드셰이크가 없을 때 전방 비밀성, CPU 소모 공격, 증폭을 어떻게 다루는가. 5. 활성 링크의 전달당 CPU가 QUIC 기준선보다 나쁘지 않은가. - **최소 확인.** 1. 링크 행을 앱 상태에 두는 시제품을 만든다. 한 노드 쌍에 링크 여럿을 둔다. 어느 쪽이든 kill -9로 죽였다 살린 뒤, 핸드셰이크 없이 다음 패킷으로 이어지고 잃거나 겹치는 기록이 없는지 본다. 2. 쉬는 링크를 1만, 10만, 100만 개로 늘린다. 프로세스 메모리와 저장소 크기의 기울기를 quinn·ngtcp2 연결당 메모리와 댄다. - **중단 기준.** 아래 "중단 기준" 절에 둔다. ## 의심하는 관례 | 관례 | 이 설계 | |---|---| | 통신하려면 연결을 열고(핸드셰이크) 끝나면 닫는다 | 링크는 배선할 때 생긴다. 서로의 키를 알므로 첫 패킷부터 봉인된 기록이 간다 | | 연결 상태는 프로세스 메모리에 있다 | 저장소의 행이다. 메모리는 그 캐시다 | | 서버를 다시 시작하면 연결이 모두 끊기고 클라이언트가 다시 잇는다 | 재시작해도 행이 남는다. 다음 패킷이 그대로 이어진다 | | 연결은 주소로 식별한다 | 키로 식별한다. 주소는 마지막으로 인증된 패킷이 온 곳이다 | | 전송의 재전송 버퍼, 앱의 outbox와 재시도, 받는 쪽의 중복 제거가 따로 있다 | 보내는 앱의 저장소가 재전송 버퍼다. 받는 쪽이 저장해 둔 들은 번호가 곧 재전송 방지다 | | 전송 암호와 본문 봉인을 따로 한다 | 직접 링크는 링크의 키 에폭으로 한 번 봉인한다. 중계를 지날 때만 기록을 따로 봉인한다 | | 흐름 제어는 바이트 창이다 | 받는 쪽이 다음 저장에 들일 수 있는 만큼을 허가한다 | | 전방 비밀성은 연결마다의 임시 키 교환으로 얻는다 | 연결이 없으므로 에폭 교체(래칫)로 얻는다. 기준은 핸드셰이크가 아니라 키의 나이다 | | 선 위의 형식과 저장 형식이 다르다 | 저장한 기록의 바이트와 필드 표가 곧 선 위의 바이트다 | ## 구조(후보) 패킷의 바이트 배치는 시제품과 측정으로 정한다. **링크의 행(저장).** 목록(B+트리, [090](../research/090-tables.md))의 한 항목이다. 대략 100 B다. - 상대 노드: 키 표의 번호 - 링크 이름: 출력, 입력 - 키 에폭: 번호, 보내는 키와 받는 키(AEAD 키와 IV 기반) - 저장 확인된 번호. 보낸 기록 자체는 이미 outbox에 있다. - 들은 번호. `pure_app_deliver`가 이미 적용과 함께 저장한다. - 패킷 번호 예약 상한. 번호를 K개씩 예약해 커밋하고, 재시작하면 상한부터 쓴다(RFC 8613 B.1.1과 같은 방법). 이 커밋은 앱의 저장에 실려 가므로 따로 동기화하지 않는다. **메모리(캐시).** - 활성 링크의 행과 AEAD 문맥 - 경로마다: 주소, RTT, 혼잡 창, 손실 탐지 상태(RFC 9002). 모두 휘발한다. 다시 시작하면 처음 창부터 간다. TCP가 쉬었다 다시 보낼 때와 같은 규칙이다(RFC 5681 §4.1). - 받았지만 아직 저장하지 않은 기록. 허가가 그 양을 묶는다. 기록 하나만큼은 늘 허가해 진행이 막히지 않게 한다. **패킷.** - 평문 머리: 판, 종류, 링크 식별자, 패킷 번호. 링크 식별자는 받는 쪽이 행을 곧바로 찾는 짧은 번호다. - 봉인된 몸에는 프레임이 든다. - 기록: 링크, 첫 번호, 개수, 저장한 바이트와 필드 표 - 패킷 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와 GRO도 쓴다. - 저장소의 바이트를 패킷 버퍼로 봉인하는 것이 유일한 복사다. - `SOCK_RAW`와 커널 우회는 쓰지 않는다. 권한이 필요하고 NAT를 지나지 못한다. **경계(갈라파고스 금지).** - UDP가 막히면 HTTP 전달로 간다. 평문으로 낮추지는 않는다. - 제3자는 HTTP로 잇는다. - 암호와 혼잡 제어는 표준만 쓴다. - 명세를 문서로 두어 다른 구현이 가능하게 한다. 판 협상과 오류 부호도 거기서 정한다. ## 이미 있는 것과의 차이 | 이미 있는 것 | 닮은 점 | 다른 점 | |---|---|---| | 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 | 끝점의 신뢰성, 로그를 버퍼로, 저장 형식이 곧 선 위 형식 | 연결 모델은 그대로다 | 기법 하나하나는 새것이 아니다. 가설은 모델에 있다. 전송 전체를 앱의 트랜잭션 저장에 원자적으로 넣는다. 키 에폭, 신뢰성, 재전송 방지, 흐름, 앱의 적용이 모두 거기에 든다. 그러면 연결이라는 개념이 사라진다. 이 조합의 신규성은 문헌 조사로 다시 확인하며, 측정 전에는 주장하지 않는다. ## 가설 - **H1.** 쉬는 링크의 비용은 저장소의 행(약 100 B)이다. 프로세스 메모리는 활성 링크 수에만 비례한다. - **H2.** 어느 쪽 노드를 kill -9 해도, 다시 뜬 뒤 첫 패킷으로 이어진다. 핸드셰이크는 없고, 잃거나 겹치는 기록도 없다. - **H3.** 저장소를 다른 프로세스나 주소로 옮겨도 링크가 이어진다. - **H4.** 활성 링크의 전달당 CPU는 QUIC 기준선보다 나쁘지 않다. 층을 합친 이득이 행을 찾는 비용을 덮는다. ## 측정 계획: quinn·ngtcp2 대비 **비교 대상.** 모두 같은 의미를 지킨다. 보내는 앱이 저장한 것만 보내고, 링크마다 순서대로 보내며, 받는 쪽이 저장한 뒤에만 덜어 내고, 한 번만 적용한다. 암호는 AES-128-GCM이다. - quinn 0.11: 이 Pi의 cargo 저장소에 있다. - ngtcp2: 원본에서 OpenSSL 3.5 백엔드로 빌드한다. - OpenSSL 3.5.7의 QUIC: 참고로 둔다. - 지금의 HTTP 전달 - 시제품 **작업.** | | 무엇 | 보는 값 | 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다. 지연·손실·병목은 비특권 네트워크 이름공간과 netem·tbf로 만든다. 이 Pi에서 되는 것을 확인했다(한쪽 10 ms로 RTT 20 ms). - 저장소는 RAM(tmpfs)과 SD 카드 두 가지로 한다. - 후보는 번갈아 돌리고, 세 라운드 이상 한다. 원시 결과는 `research/experiments/`에 남긴다. ## 중단 기준 | 조건 | 결정 | |---|---| | H2·H3의 검사에서 한 번이라도 잃거나 겹친다. 또는 재시작 뒤 논스를 다시 쓴다 | 되돌리고 원인을 찾는다 | | M1: 쉬는 링크당 프로세스 메모리가 QUIC 연결당 메모리의 1/100 이하가 아니다 | 모델의 주장이 틀렸다. 다시 설계한다 | | M2: 재시작 뒤 핸드셰이크 없이 이어지지 않는다. 또는 이어지는 데 링크 수에 비례하는 비대칭 연산이 든다 | 다시 설계한다 | | 전송 상태 때문에 패킷마다 동기화가 필요해진다(지연이 RTT와 저장 한 번을 넘는다) | 예약 방식을 다시 본다 | | M5·M6: 전달당 CPU가 QUIC보다 10 % 넘게 나쁘다. 또는 TCP와의 몫이 ±25 %를 벗어난다 | 고친 뒤에 채택한다 | | 보안 검사(증폭, 보낸 이 위장, 재전송, 변조)에 실패한다 | 인터넷용으로 켜지 않는다 | | 전송 코드가 C 5,000줄을 넘는다 | 설계를 다시 본다 | | H1–H4가 모두 서고 검사를 모두 통과한다 | 노드 사이 전달의 기본으로 삼는다 | ## 이 너머: 기다리지 않는 상호작용 연결을 없애도 왕복 자체는 남는다. 빛의 속도는 줄일 수 없다. 다음 후보는 결정적 모듈의 투기 실행이다. Pure의 모듈은 검증되어 결정적이고, 이벤트의 실행량에 상한이 있다. 보내는 노드가 받는 앱의 모듈과 관련 상태의 복제를 가지면, 답을 기다리지 않고 받는 쪽의 결과를 미리 계산해 보여 줄 수 있다. 그 뒤 실제 답으로 확정하거나 되돌린다. 게임의 롤백 넷코드와 Replicache의 뮤테이터와 같은 생각이다. 다른 점은 앱 코드를 두 벌 쓰지 않는다는 것이다. 이것은 전송이 아니라 런타임의 능력이라 따로 설계한다. ## 순서 1. 링크 행 시제품을 만든다: 한 노드 쌍, 링크 여럿, 에폭 하나, NewReno, 저장소에서 재전송. 여기에 kill -9 검사(H2)를 붙인다. 2. quinn·ngtcp2 하네스를 같은 의미로 만든다. 3. M1(메모리 기울기), M2(재시작), M4를 잰다. 4. 에폭 바꿈, 경로 바뀜, 저장소 옮김(H3, M3)과 보안 검사를 한다. 5. M5·M6 가드레일을 잰다. 채택 여부를 정하고 명세를 쓴다.