aboutsummaryrefslogtreecommitdiffstats
path: root/docs/design/012-link-protocol.md
blob: f2e982073ced4d5aaf50068a1d5b8dcf19c29e12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# 012 — 링크 프로토콜 명세, 판 0

작성일: 2026-09-29. 상태: 명세 판 0.

- 구현은 `link/link-protocol.c`이고, 호스트 API는 `link/link-protocol.h`다.
- 설계와 측정은 [011](011-pure-transport.md)과 연구 093–101에 있다.
- 이 문서만 보고 파이썬으로 따로 쓴 받는 쪽이 `pure-serve` 노드와 맞붙는다(`tests/check.py`의
  `server_links_independent`). 그 받는 쪽은 `cryptography` 패키지만 쓰고 `link/`의 코드는 쓰지 않는다. 노드가 보낸 주문
  200건이 모두 저장되고, 첫 패킷 하나 뒤로는 두 쪽이 따로 셈한 에폭 1로 봉인된다.

이 문서는 Pure 노드 사이의 연결 없는 링크가 선 위에서 하는 일을 정한다. 다른 구현이 이 문서만 보고 Pure 노드와 주고받을 수 있게
하는 것이 목표다. 그래서 바이트 배치, 키 유도, 그리고 상대가 알아야 하는 규칙을 모두 적는다. 혼잡 제어처럼 한쪽이 혼자 정해도
되는 것은 "지역 선택"으로 따로 표시한다.

## 용어

- **노드**: X25519 키 쌍을 가진다. 공개 키는 32 B다.
- **보내는 쪽**: 한 노드의 출력 하나가 저장한 보내기를 다른 노드의 입력 하나로 보낸다. 시작할 때마다 **run**을 새로 뽑는다.
  run은 0이 아닌 무작위 u64다.
- **받는 쪽**: 받아들일 보내는 쪽을 (source 이름, 공개 키) 목록으로 가진다.
- **스트림**: source 이름과 input 이름의 쌍이다. 각 이름은 1–64 B다. 메시지의 shape(u32)가 함께 간다. 받는 쪽 입력의
  shape와 같아야 한다. 링크에게 shape는 비교만 하는 값이고, 뜻은 모듈 형식([008](008-module-format.md))이 정한다.
- **기록**: 스트림 안에서 1부터 연속된 번호(u64)를 가진 바이트열이다. 0–16 MiB다. 바이트는 보내는 앱이 저장한 그대로이고,
  링크는 읽지 않는다.
- **들은 번호**: 받는 쪽 앱이 적용하고 저장한 마지막 기록 번호다.
- **답**: 받는 쪽이 보내는 쪽의 run에 보내는 패킷이다.

정수는 모두 little-endian이다. 논스를 만들 때 패킷 번호만 big-endian으로 넣는다(QUIC과 같다).

## 전송

링크는 UDP 위에 있다. 데이터그램 하나가 패킷 하나다. 받는 쪽의 주소와 포트, 입력 이름은 배선이 준다
(`pure://host:port/INPUT`). 받는 쪽의 공개 키도 배선이 준다(`--seal OUTPUT=KEY`).

- 패킷은 37–1,452 B다. 머리 21 B, 봉인한 몸 0 B 이상, 태그 16 B다.
- 보내는 쪽은 처음에 1,200 B 이하로 보낸다. 경로가 1,452 B를 나른다고 확인한 뒤에만 그만큼 보낸다(아래 "경로 MTU").
- 답은 1,200 B 이하다.
- 이 범위를 벗어난 데이터그램은 답 없이 버린다.

## 패킷

| 위치 | 크기 | 내용 |
|---:|---:|---|
| 0 | 1 | `0x70 \| (에폭 & 0x0f)`: 높은 4비트는 판(0x7이 판 0), 낮은 4비트는 봉인한 에폭 번호의 아래 4비트 |
| 1 | 4 | 봉인한 노드의 키 id(u32): 그 노드 공개 키 32 B의 FNV-1a 32비트(시작값 2166136261, 곱수 16777619) |
| 5 | 8 | run(u64): 보내는 쪽 패킷은 보내는 쪽의 run, 답은 받는 쪽이 그 run에 쓰는 답 run |
| 13 | 8 | 패킷 번호(u64): run 안에서 0부터 하나씩 는다 |
| 21 | n | 프레임들을 AES-128-GCM으로 봉인한 것. 추가 인증 데이터는 머리 21 B 전부다 |
| 21+n | 16 | GCM 태그 |

키 id는 찾기 위한 힌트일 뿐이다. 두 키가 같은 id를 가질 수 있으므로, 받는 쪽은 그 id를 가진 허용된 키를 차례로 시도한다.

## 판

첫 바이트의 높은 4비트가 판이다. 이 문서는 판 0(0x7)을 정한다. 다른 판의 패킷은 답 없이 버린다.

판 협상은 선 위에 없다. 받는 쪽이 말하는 판은 키와 함께 배선에서 온다. 인증되지 않은 판 협상 패킷은 위조와 증폭의 길이 되고,
두 노드는 어차피 서로의 키를 미리 알아야 한다. 다음 판은 첫 바이트의 높은 4비트와 키 유도 문자열의 `0`을 함께 바꾼다. 그래서
판이 다른 두 구현은 서로의 패킷을 열 수 없고, 한 판의 키가 다른 판에서 다시 쓰이지 않는다.

## 키

**정적 비밀.** `S = X25519(자기 개인 키, 상대 공개 키)`의 32 B 그대로다. 양쪽에서 같다. 모두 0이면(작은 차수의 키) 링크를
만들지 않는다.

**HKDF.** HKDF-SHA256(RFC 5869)의 Extract와 Expand를 쓴다. salt는 ASCII `pure link 0`(11 B)이고, IKM은 32 B 비밀이다.

**패킷 키.** `HKDF(비밀, info, 28)`의 앞 16 B가 AES-128 키, 뒤 12 B가 논스 기반이다.

```
info = "pure link 0 keys" (16 B) ‖ from 공개 키 (32) ‖ to 공개 키 (32) ‖ from_run (u64) ‖ to_run (u64)
```

| 패킷 | 비밀 | from | to | from_run | to_run |
|---|---|---|---|---|---|
| 보내는 쪽 → 받는 쪽 | 보내는 쪽이 봉인하는 에폭의 비밀 | 보내는 쪽 | 받는 쪽 | 보내는 쪽 run | 0 |
| 답 | 그 run이 지금 있는 에폭의 비밀 | 받는 쪽 | 보내는 쪽 | 답 run | 보내는 쪽 run |
| LOST | 에폭 0의 비밀(S) | 받는 쪽 | 보내는 쪽 | 새로 뽑은 답 run | 보내는 쪽 run |

**논스.** 논스 기반 12 B의 4–11번째 바이트에 패킷 번호를 big-endian으로 XOR한다. run이 키에 들어가므로, 노드가 다시 시작해도
한 키 아래 논스가 겹치지 않는다. 받는 쪽은 보내는 쪽 run마다 답 run을 새로 뽑는다. 쉬던 보내는 쪽이 돌아와 자리를 다시 만들
때에도 새로 뽑는다.

**에폭.** 에폭 0의 비밀은 S다. 에폭 n(≥ 1)의 비밀은 다음과 같다.

```
E(n) = HKDF(IKM = X25519(보내는 쪽 에폭 n 개인 키, 받는 쪽 에폭 n 공개 키),
            info = "pure link 0 epoch" (17 B) ‖ n (u32) ‖ E(n-1) (32 B), 32)
```

두 개인 키는 합의한 뒤 곧 지운다. 그래서 두 노드의 정적 키가 나중에 새어도 에폭 1부터의 패킷은 열리지 않는다. 사슬을
처음부터 다시 시작할 때(아래 "에폭을 잃음")에는 E(1)을 다시 E(0) = S에서 만든다.

## 프레임

봉인한 몸은 프레임의 연속이다. 각 프레임은 1 B 종류로 시작한다. 읽는 쪽이 모르는 종류를 만나면 그 패킷의 나머지를 읽지 않는다.
PADDING도 그렇게 끝을 채운다. 이름은 u8 길이(1–64)와 그 바이트다. `names`는 source 이름 다음에 input 이름이다.

| 종류 | 이름 | 방향 | 배치 |
|---:|---|---|---|
| 0x00 | PADDING | 모두 | 패킷 끝까지 0 |
| 0x01 | STREAM | 보냄 | names, u32 shape |
| 0x02 | RECORD | 보냄 | u64 번호, u32 기록 크기, u32 오프셋, u16 길이, 바이트[길이] |
| 0x03 | ACK | 답 | u64 가장 큰 번호, u32 지연(µs), u8 구간 수, 구간마다 u64 처음과 u64 끝(새것부터) |
| 0x04 | STORED | 답 | names, u64 들은 번호, u8 깃발(1: 틈 뒤의 기록을 받지 않았다, 2: 앱이 가득 차 받은 것을 쥐고 있다), u32 허가 |
| 0x05 | PING | 보냄 | 없음 |
| 0x06 | RECORDS | 보냄 | u64 첫 번호, u8 개수, 기록마다 크기(LEB128)와 바이트 |
| 0x07 | CHALLENGE | 답 | u64 |
| 0x08 | RESPONSE | 보냄 | u64 |
| 0x09 | OFFER | 보냄 | u32 에폭, 공개 키 32 B |
| 0x0a | ACCEPT | 답 | u32 에폭, 공개 키 32 B, 답하는 OFFER 공개 키의 앞 4 B |
| 0x0b | LOST | 답 | u64 패킷 번호 |
| 0x0c | REFUSED | 답 | names, u8 이유(1: 그 이름의 메시지 입력이 없다, 2: 입력의 shape가 다르다, 3: 이 키는 그 source로 보낼 수 없다) |

- RECORD와 RECORDS는 그 앞의 가장 가까운 STREAM의 스트림에 속한다. 앞에 STREAM이 없으면 버린다.
- RECORDS는 번호가 이어진 온전한 기록들이다. LEB128은 7비트씩, 낮은 것부터, 10 B까지다. 기록 하나가 패킷 끝을 넘으면 그
  기록과 나머지 패킷을 읽지 않는다.
- RECORD는 기록의 한 조각이다. 받는 쪽은 `오프셋 > 크기`, `길이 > 크기 - 오프셋`, `길이 0이면서 오프셋 > 0`, `크기 > 16 MiB`인
  것을 버린다.
- 답에서 REFUSED는 같은 패킷의 ACK 뒤에 둔다. 보내는 쪽은 한 답 패킷을 끝까지 읽은 뒤 거절이 끝났는지 판단한다.
- 보내는 쪽 패킷은 모두 확인을 부른다. STREAM이나 PING이 늘 있기 때문이다. 답은 확인을 부르지 않는다.

## 보내는 쪽

**시작.**

1. run을 뽑는다.
2. S를 만들고 에폭 0의 키로 봉인한다.
3. 에폭 1의 키 쌍을 만들어 OFFER로 낸다.

첫 패킷부터 기록을 싣는다. 핸드셰이크는 없다.

**패킷.**

- 패킷 번호는 run 안에서 다시 쓰지 않는다. 봉인했지만 보내지 못한 패킷의 번호도 다시 쓰지 않는다.
- 탐침(PING뿐인 패킷)을 빼면 모든 패킷은 STREAM으로 시작한다. 다음에 받아 둔 CHALLENGE에 대한 RESPONSE(최근 4개)를
  넣는다. 그다음 OFFER(받아들여질 때까지), 그다음 기록을 넣는다.
- 보낸 적 없는 온전한 기록은 남은 자리에 들어가면 RECORDS에 넣는다. 번호가 이어지면 한 RECORDS에 모은다. 그 밖에는 RECORD
  조각으로 보낸다. 남은 자리가 64 B 이상이면 조각을 잘라서라도 채운다.
- 번호가 한도 이하인 기록만 보낸다. 한도는 STORED마다 `들은 번호 + min(허가, 65536)`(허가가 0이면 1)으로 계산하고, 그 가운데
  가장 큰 값이다. STORED를 듣기 전에는 저장되지 않은 가장 오래된 기록 번호 - 1 + 1024다.

**다시 보내기.** 패킷을 다시 보내지는 않는다. 그 패킷이 나른 기록 조각을 새 패킷에 싣는다. 다음 경우에 다시 보낸다.

- 패킷을 잃었다고 판단할 때. 판단은 RFC 9002를 따른다(지역 선택).
- ACK로 받았다고 들은 조각이 1초 안에 저장되지 않을 때. 받는 쪽이 다시 시작했을 수 있다. 깃발 2가 붙은 STORED를 들으면 이
  1초를 그때부터 다시 센다. 받는 쪽이 살아 있고, 받은 것을 쥔 채 앱에 자리가 나기를 기다린다는 뜻이다.
- STORED에 틈 깃발이 올 때.

STORED의 들은 번호 이하인 기록은 덜어 낸다. 보내는 앱은 그때 그 보내기를 outbox에서 지운다.

**답 받기.** 답은 머리의 키 id가 받는 쪽 키의 id인 것만 본다. 머리의 에폭 비트가 가리키는 키로 연다. 후보는 지금 에폭, 그 앞
에폭, 에폭 0이다. 답 run이 무엇이든 받는다.

- ACK와 STORED는 가장 큰 값으로만 받는다. 다시 보낸 옛 답은 아무것도 되돌리지 못한다.
- CHALLENGE는 다음에 나가는 패킷에서 RESPONSE로 되돌려 준다. 그것만을 위한 패킷은 만들지 않는다.

**에폭 바꿈.**

- 한 시간 또는 1 GiB마다 다음 에폭 n+1의 키 쌍을 OFFER로 낸다. 이 주기는 지역 선택이며 이보다 길면 안 된다.
- ACCEPT의 에폭이 낸 에폭과 같고 끝 4 B가 낸 공개 키의 앞 4 B와 같으면 E(n)을 만든다. 그 다음부터 에폭 n으로 봉인한다.
- 앞 에폭의 키는 늦게 오는 답을 열 때까지만 둔다.

**에폭을 잃음.**

- 아직 날고 있는 패킷을 가리키는 LOST를 들으면, 에폭 0으로 돌아가 에폭 1을 다시 낸다. 저장되지 않은 기록은 모두 다시 보낸다.
  날고 있던 패킷은 혼잡으로 세지 않는다. 에폭 1 이상에 있거나 사슬을 다시 시작하던 중일 때만 이렇게 한다.
- 에폭 1 이상에서 확인을 부르는 패킷이 나가 있는데 2초 동안 아무 답도 없고 탐침 4번이 연달아 답을 받지 못하면, 에폭 0으로
  돌아가 에폭 1을 다시 낸다. 이때는 받는 쪽이 받아들일 때까지 기록을 싣지 않고, OFFER만 RTT(100 ms 이상)마다 보낸다. 받은
  것이 없으니 공격자가 답을 막고 있을 수도 있어, 정적 키 아래로 기록을 다시 보내지 않는다.

**거절.** 자기 스트림에 대한 REFUSED를 들으면 기록을 보내지 않는다.

- 기다린 뒤 기록을 실은 패킷 하나로 다시 시도한다.
- 기다림은 1초에서 시작해 거절될 때마다 두 배로 늘고, 64초에서 멈춘다.
- 시도한 패킷을 확인한 답에 REFUSED가 없으면 거절이 끝난다.
- STORED를 들어도 끝나며, 이때 기다림도 처음으로 돌아간다.
- 시도한 패킷보다 앞선 패킷들에 대한 REFUSED는 무시한다.

**경로 MTU**(DPLPMTUD, RFC 8899).

- 패킷이 가득 찬 적이 있고 RTT를 한 번 잰 뒤, 1,452 B짜리 PING(나머지는 PADDING)을 탐침으로 보낸다.
- 탐침이 확인되면 그 크기로 보낸다.
- 탐침을 잃으면 1초 뒤에 다시 보낸다. 세 번 잃으면 그다음은 10분 뒤다.
- 다음 둘 가운데 하나가 되면 1,200 B로 돌아가 1초 뒤에 다시 탐침한다.
  - 작은 패킷만 확인되는 동안 큰 패킷을 연달아 3개 잃는다.
  - 탐침 시간 초과(PTO)가 2번 연달아 난다.

## 받는 쪽

**여는 순서.**

1. 크기와 판을 본다.
2. 키 id가 같은 허용된 키 가운데 그 run을 아는 것부터, 다음에 모르는 것을 차례로 시도한다.
3. run마다 다음 에폭, 지금 에폭, 앞 에폭의 키를 머리의 에폭 비트가 맞을 때 시도한다. 그 run이 에폭 1 이상에 있고 에폭
   비트가 0이면, 사슬을 다시 시작한 것일 수 있으니 에폭 0의 키도 시도한다.

- 처음 보는 run은 에폭 0에서 시작한다. 그 run의 에폭을 기억해 두었다면 그 에폭에서 시작한다(아래 "메모").
- 처음 보는 run의 키는 토큰이 있을 때만 만든다. 그 키로 열리지 않으면 토큰 하나를 쓴다. 토큰은 초당 256개 차고 256개까지
  쌓인다(지역 선택). 그래서 꾸며 낸 패킷 폭주에 드는 HKDF에 상한이 있고, 열리는 보내는 쪽 천 개가 한꺼번에 깨어나도 토큰을
  쓰지 않는다.
- run의 자리는 패킷이 열린 뒤에만 만든다. 위조한 패킷은 아무 자리도 밀어내지 못한다.
- 이미 받은 패킷 번호(구간 32개까지 기억)의 패킷은 ACK만 다시 하고 프레임은 처리하지 않는다. 그보다 오래된 번호의 패킷은
  다시 처리해도 해가 없다.
  - 기록은 들은 번호가 걸러 한 번만 적용된다.
  - 지난 OFFER는 지금 에폭 다음이 아니라 받아들여지지 않는다.
  - 지난 RESPONSE는 지금의 CHALLENGE와 맞지 않는다.
  - 주소는 가장 큰 번호의 패킷만 옮긴다.

**주소.**

- 답은 그 run에서 번호가 가장 큰 패킷이 온 곳으로 보낸다. 받은 주소(원격)와 도착한 자기 주소(로컬)에서 보낸다.
- 새 주소로는 CHALLENGE를 보낸다. 같은 값이 RESPONSE로 돌아오기 전까지, 그 주소로 보내는 답은 그 주소에서 받은 바이트의 3배를
  넘지 않는다(RFC 9000 §8).
- 옛 패킷을 다시 보내도 주소는 움직이지 않는다.

**답.**

- 확인을 부르는 패킷을 받으면 25 ms 안에 답 패킷 하나에 다음을 넣어 보낸다. 보내는 쪽의 PTO가 이 상한을 기대한다.
  - CHALLENGE(확인 전)
  - ACCEPT(다음 에폭이 쓰이기 전)
  - ACK: 가장 큰 번호를 받은 뒤 지난 시간과 구간들
  - 그 run이 마지막으로 보낸 스트림마다 STORED
  - REFUSED
- STORED는 저장이 끝났을 때에도 보낸다.
  - 허가는 받는 쪽이 정한다(지역 선택). 이 구현은 그 스트림에 남은 자리(16 MiB에서 쥔 바이트를 뺀 것)를 지난 전달의 기록당
    바이트로 나눈다. 1에서 65,536 사이이고, 아직 전달이 없으면 1,024다. 그래서 링크가 한 RTT에 나르는 기록 수가 기록의 크기를
    따라간다.
  - 들은 번호 + 65,536보다 큰 기록은 버린다. 그 안의 기록도 메모리(16 MiB)를 넘으면 받지 않는다. 보내는 쪽은 저장 소식이 없으니
    다시 보낸다.
  - 들은 번호 이하의 기록이 다시 오면 STORED를 다시 보낸다.
- 받는 쪽은 들은 번호 다음부터 번호 순서로 온전한 기록을 앱에 넘긴다. 앱이 적용하고 저장한 뒤에만 STORED의 들은 번호를 올린다.
  틈에서 멈추고, 틈 뒤의 기록을 버렸으면 깃발 1을 세운다.
- 앱이 가득 차 더 받지 못하면(앱의 메모리 예산, [013](013-memory-budget.md)) 받는 쪽은 받지 못한 기록을 쥔 채 잠시 뒤 다시
  넘긴다. 그동안 보내는 STORED에 깃발 2를 세운다. 다시 넘기는 간격은 지역 선택이지만, 보내는 쪽이 저장을 기다리는 1초보다
  짧아야 한다. 그래야 보내는 쪽이 쥐고 있는 기록을 다시 보내지 않는다.

**OFFER를 받아들임.** 다음 둘 가운데 하나일 때만 받아들인다.

- 에폭 0의 패킷에 실린 에폭 1(사슬의 시작 또는 다시 시작)
- 지금 에폭 n의 패킷에 실린 n+1

받아들이는 방법과 제한은 다음과 같다.

- 받는 쪽은 에폭 n의 키 쌍을 만들어 E(n)을 셈하고 ACCEPT를 보낸다. 그 에폭의 패킷이 처음 열릴 때까지 답마다 다시 넣는다.
- 같은 OFFER가 다시 오면 같은 ACCEPT를 보낸다.
- 한 피어에 새 에폭은 초당 한 번까지다.
- 지역 선택으로 다음 제한을 둔다.
  - 기록과 함께 온 OFFER는 초당 256번(한꺼번에 8번)까지만 받아들인다.
  - 받는 쪽 키 쌍은 1초 동안 그 초의 ACCEPT들이 함께 쓴다.
  - X25519는 답을 보낸 뒤에 한다.
- 사슬을 다시 시작한 run에는 답도 에폭 0으로 돌아가 보낸다.

**LOST.** 다음 조건을 모두 만족하면 LOST를 보낸다.

- 처음 보는 run이다.
- 에폭 비트가 0이 아니다.
- 그 에폭을 모른다. 다시 시작하면서 메모를 잃었거나, 메모가 그 피어의 다른 run을 들고 있을 때다.
- 패킷이 46 B보다 크다.
- 초당 4,096번 이하다.

LOST는 그 패킷의 번호를 담아 에폭 0의 키로 봉인한다. 답 run은 새로 뽑고, 패킷 번호는 0이다. 크기는 46 B로, 그것을 부른
패킷보다 작다. QUIC의 stateless reset(RFC 9000 §10.3)과 같은 자리이지만, LOST는 두 노드의 정적 키로 봉인되어 경로 위의
공격자가 만들 수 없다.

**REFUSED.** STREAM을 받아들이지 않으면, 그 run의 다음 답에 그 스트림에 대한 REFUSED를 한 번 넣는다.

**쉬는 피어.** 30초 동안 조용하고 쥔 것이 없는 피어는 run, 주소, 스트림을 놓는다(지역 선택). 다음 패킷이 다시 만든다.

**메모.** 선 위의 일은 아니지만 재시작에 관계된다. 받는 쪽은 피어마다 S와, 그 피어의 run마다 받아들인 에폭을 파일에 적어 둘
수 있다.

- 피어의 자리에는 run 하나를 둔다.
- 그 run이 아직 보내는 동안 같은 키의 다른 run(한 노드의 출력 여럿)은 곁의 자리 1,024개에 둔다.
- 다시 떠도, 쉬었다 돌아와도 이 run들은 비대칭 연산 없이 이어진다.
- 메모에 없는 run은 LOST를 받고 에폭을 다시 시작한다.

## 수와 지역 선택

| 이름 | 값 | 성격 |
|---|---:|---|
| 판 0의 첫 바이트 | 0x70–0x7f | 명세 |
| 머리 / 태그 | 21 B / 16 B | 명세 |
| 첫 데이터그램 / 가장 큰 데이터그램 | 1,200 B / 1,452 B | 명세 |
| 이름 | 1–64 B | 명세 |
| 기록 | 16 MiB 이하 | 명세 |
| 허가의 상한(보내는 쪽이 따르고, 받는 쪽이 받는 범위) | 65,536 | 명세 |
| ACCEPT의 되풀이 | 4 B | 명세 |
| 첫 허가(STORED 전) | 1,024 | 명세 |
| 새 주소의 증폭 한도 | 받은 바이트의 3배 | 명세 |
| LOST 크기 조건 | 부른 패킷 > 46 B | 명세 |
| 에폭 주기 | 1시간 또는 1 GiB 이하 | 상한만 명세 |
| STORED의 허가 | 남은 자리 ÷ 기록당 바이트(1–65,536, 처음 1,024) | 지역 선택 |
| 거절 뒤 기다림 | 1초부터 두 배, 64초까지 | 지역 선택 |
| 에폭을 잃었다고 볼 때 | 2초와 PTO 4번 | 지역 선택 |
| 받은 뒤 저장을 기다리는 시간 | 1초 | 지역 선택 |
| 가득 찬 앱에 다시 넘기는 간격 | 100 ms부터 두 배, 500 ms까지 | 지역 선택(저장 기다림보다 짧게) |
| ACK 지연 상한(받는 쪽이 지킨다) | 25 ms | 명세 |
| RTT 첫값, PTO 상한 | 100 ms, 1초 | 지역 선택 |
| 혼잡 제어 | CUBIC(RFC 9438, C 0.4, β 0.7), 첫 창 10 데이터그램, 페이싱, 잘못 본 손실은 되돌림 | 지역 선택(표준 알고리즘만) |
| 재정렬 인내 | 패킷 3–256개, 시간 RTT/8부터 최소 RTT의 1/4씩 RTT까지 | 지역 선택 |
| 키 만들기 / 에폭 수락 / LOST | 초당 256 / 256(한꺼번에 8) / 4,096 | 지역 선택 |
| 쉬는 피어 | 30초 | 지역 선택 |
| 피어당 run 자리 | 64까지 | 지역 선택 |

## 오류와 알림

선 위의 알림은 둘이다. 둘 다 인증된다.

- **LOST**: 받는 쪽이 그 run의 에폭을 모른다.
- **REFUSED**: 받는 쪽이 그 스트림을 받지 않는다. 이유 부호는 위와 같다. `link/link.c`는 이유를 운영자에게 한 줄로 알린다.
  HTTP 전달이 받는 쪽의 거절을 알리는 것과 같은 모양이다.

그 밖의 실패(열리지 않는 패킷, 허용되지 않은 키, 모르는 판)에는 답하지 않는다. 답하면 공격자에게 신호와 증폭을 준다.

## 보안 성질

경로 위의 공격자에 대해서는 [098](../research/098-link-security.md)과 [099](../research/099-link-epochs.md)의 검사가
`link/test-link.c`에 있다.

- **바꾼 패킷**: 열리지 않는다. 그 run을 모르는 받는 쪽이 에폭 1 이상의 패킷에 보내는 LOST(그보다 작다) 말고는 답이 없다.
- **다시 보낸 패킷**: 기록은 한 번만 적용된다. ACK와 STORED는 가장 큰 값으로만 받는다. 주소는 움직이지 않는다.
- **위장**: 허용되지 않은 키나 source로는 아무것도 열리거나 전달되지 않는다.
- **폭주**: 모르는 run의 HKDF는 초당 256번, X25519는 피어마다 한 번(메모)이다.
- **나중에 두 노드의 키를 얻음**: 에폭 0의 패킷만 열린다. run의 첫 왕복과, LOST 뒤 다음 에폭이 받아들여지기 전까지가 에폭 0이다.
  QUIC 0-RTT와 같은 한계다.

알려진 한계:

- 받는 쪽의 개인 키를 가진 이는 그 받는 쪽에게 어느 보내는 쪽으로든 봉인할 수 있다(Noise KK의 첫 메시지와 같은 KCI). 에폭은
  이것을 바꾸지 않는다. 다만 다른 run의 에폭 안으로는 들어오지 못한다.
- 메모의 곁 자리는 1,024개이고 한 run이 두 자리 가운데 하나에 든다. 한 받는 쪽에 함께 보내는 "피어의 둘째 이후 run"이
  그보다 많아 자리가 겹치면, 밀려난 run은 받는 쪽이 다시 뜬 뒤 LOST를 받고 에폭 0에서 다시 시작한다.
- 메모는 받아들인 에폭의 비밀을 그 run의 다음 에폭이 덮을 때까지 디스크에 둔다. 다시 떠도 잇기 위해서다. 그 사이 메모
  파일을 가져간 이는 그 에폭과 앞 에폭을 열 수 있다.
- NAT 뒤에서 쉬는 기기에 먼저 보내는 일은 풀지 않는다.

## 이 명세와 구현

- 구현은 `link/link-protocol.c`(상태, 소켓 없음)와 `link/link.c`(UDP, GSO/GRO, 스레드)다.
- 전송 코드는 C 3,161줄이다(프로토콜 2,574, UDP 587). 011의 한도는 5,000줄이다.
- 이 명세를 쓰면서 구현의 구멍 셋을 찾아 고쳤다. 셋 다 `link/test-link.c`에 검사가 있다. 메모의 곁 자리와 허가의 크기까지
  [102](../research/102-adoption-and-spec.md)에 있다.
  - 받는 쪽이 스트림을 받지 않을 때 보내는 쪽이 아무 말도 듣지 못해 매초 기록을 다시 보냈다. REFUSED를 더했다.
  - 보내는 쪽은 64 MiB까지의 기록을 받아 두었지만 받는 쪽은 16 MiB까지만 쥘 수 있었다. 그 사이 크기의 기록은 영원히 다시
    갔다. 기록의 상한을 16 MiB로 맞췄다.
  - 받는 쪽은 피어마다 run 자리를 둘만 두었다. 한 노드의 출력 셋 이상이 같은 노드로 가면 run들이 서로 자리를 밀어내어, 답이
    다른 run으로 갔다. 살아 있는 run이 더 오면 자리를 늘린다.