geiserx_tailscale 0.56.2

A work-in-progress pure-Rust Tailscale implementation (fork of tailscale/tailscale-rs)
Documentation
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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
# Porting ledger: upstream Go `tailscale` → this repository

| | |
| --- | --- |
| **Upstream source** | `https://github.com/tailscale/tailscale` (Go) |
| **Upstream commit this ledger was written against** | `e2ed432399c9b0fda7aa14e9eb27784d2d893c55` (2026-09-11, `go.toolchain.rev: bump for stack debugging API`) — **held a third time**: `git ls-remote https://github.com/tailscale/tailscale HEAD` still returned it as upstream's default-branch HEAD when this revision was derived, on 2026-09-13. *The date is corrected at this revision*: the previous two revisions wrote 2026-09-12, and `git log -1 --format=%ad --date=short e2ed43239` says **2026-09-11** (author and committer alike; 20:17 UTC, so no plausible zone rounds it up) |
| **Upstream `tailcfg.CurrentCapabilityVersion` at that commit** | **147** (2026-09-09) — unchanged; see §A |
| **This repository at ledger time** | `9675565` — workspace version `0.56.1` |
| **`ts_capabilityversion::CapabilityVersion::CURRENT` here** | **125** (2025-08-11) — held below 126; see §B, *c2n endpoints behind the declared capability version* |
| **Gap window this ledger covers** | capability version **131 → 147**, i.e. upstream commits from 2025-10-06 to 2026-09-11 (the window is anchored to when capver 130 landed upstream, and closes at the pinned commit above — the end of the window and the pin's date are one fact, so they move together; the declaration here being 125 rather than 130 does not change what upstream added) |
| **Previous pin** | `e2ed432399c9b0fda7aa14e9eb27784d2d893c55`, with this tree at `0c8796a` — **neither moved, and this tree did not move in code at all.** `e2ed43239..` upstream HEAD is empty for the third consecutive revision, and `0c8796a..HEAD` here is two commits, both documentation: #465 (the previous revision of this document) and #466 (a correction to two entries in the backlog file it wrote). No Rust changed anywhere in the interval, so none of the four sources of change that need a moving tree could fire. The reading is the whole of this revision, and it went where neither the sweep, the node-attribute walk nor the wire-type checks can see: the **DERP frame-type enumeration**, walked frame by frame against `ts_derp` and its callers. Three rows, all in the relay path — a `PeerGone` frame this node decodes and throws away, a learned DERP route Go prefers over dialling a peer's home region, and `FrameNotePreferred`, which this tree models and never sends. All three are in §B |

> This repository is also a fork of the Rust port `tailscale/tailscale-rs` — see
> [`VENDOR.md`](VENDOR.md) for that provenance. This ledger is about the *other* upstream: the Go
> client, which is the behavioural reference both of them are measured against.

## The parity mission

The mission of this repository is **100% behavioural parity with upstream Go Tailscale**
(`github.com/tailscale/tailscale`), maintained in Rust. "Behavioural" is the operative word: the
goal is not a line-by-line transliteration of Go, but a node that a real Tailscale control plane,
a real Go `tailscaled` peer, a `wireguard-go` peer and a kernel WireGuard peer cannot distinguish
from a Go client on the wire — same control-protocol requests and responses, same DERP and disco
framing, same WireGuard handshake and timer behaviour, same packet-filter verdicts, same
fail-closed decisions when something goes wrong. Where Rust idiom differs from Go (typed errors
instead of sentinel values, actors instead of goroutines-plus-mutexes, `smoltcp` instead of
gVisor), the internals may differ freely; what may never differ is what a peer or a control plane
observes. This engine is always the *dialing client* against implementations it does not control,
so a divergence is a bug even when the divergence looks like an improvement, and no change may
assume a peer implements a fork-specific behaviour.

## Adding the upstream source

Upstream Go Tailscale is not vendored into this tree, and git remotes are local configuration that
cannot be committed. Add it once per checkout:

```sh
git remote add upstream-go https://github.com/tailscale/tailscale.git
git fetch upstream-go
```

Every command in this document is reproducible against that remote, or against a standalone
clone. The pinned commit above is what every assessment below was checked against; re-derive the
window before cutting new porting beads (see [Re-deriving this ledger](#re-deriving-this-ledger)).

## Package mapping

Upstream Go package → the crate or module that carries its behaviour here. `→` means "this is
where that behaviour lives", not "this is a transliteration of that file".

### Control plane

| Upstream Go | Here |
| --- | --- |
| `control/controlclient` | [`ts_control`](ts_control/src/lib.rs) (register, map poll, session resumption, c2n ping responder) |
| `control/controlbase` (Noise IK) | [`ts_control_noise`](ts_control_noise/src/lib.rs) |
| `control/controlhttp` (dial + upgrade) | [`ts_control`](ts_control/src/lib.rs) dial path, on [`ts_http_util`](ts_http_util/src/lib.rs) + [`ts_tls_util`](ts_tls_util/src/lib.rs) |
| `tailcfg` (wire types) | [`ts_control_serde`](ts_control_serde/src/lib.rs) (+ [`ts_packetfilter_serde`](ts_packetfilter_serde/src/lib.rs) for filter rules) |
| `tailcfg.CapabilityVersion` | [`ts_capabilityversion`](ts_capabilityversion/src/lib.rs) |
| `tailcfg/nodecap` (split out upstream, #20639) | [`ts_nodecapability`](ts_nodecapability/src/lib.rs) |
| `tailcfg/peercap` | [`ts_peercapability`](ts_peercapability/src/lib.rs) |
| `types/key` | [`ts_keys`](ts_keys/src/lib.rs) |
| `tka` (tailnet lock) | [`ts_tka`](ts_tka/src/lib.rs) + the peer-trust chokepoint in [`ts_runtime`](ts_runtime/src/peer_tracker/mod.rs) |
| `feature/identityfederation` (WIF/OAuth bootstrap) | [`ts_control::wif`](ts_control/src/wif.rs) |
| `net/tlsdial`, `net/bakedroots` | [`ts_tls_util`](ts_tls_util/src/lib.rs) |
| `control/controlknobs` | **no single counterpart** — each control-driven toggle is read where it applies, as a bare node-attribute string at its consumer ([`ts_nodecapability`](ts_nodecapability/src/lib.rs) supplies only the map type, not a list of names; the map itself reaches the tree as [`ts_control::Node::cap_map`](ts_control/src/node.rs) and is queried with `has_node_attr`). Added to this mapping at the **previous** revision, which read its fifteen commits in the window, found them all already assessed, and concluded it paid nothing. Reading the *struct* instead of its commits opened **eight** rows at this revision: of the twenty-seven knobs it carries, this tree honours one (`cache-network-maps`, with `disable-cache-network-maps`), and five have a live target here and are ignored. See §B |

### Data plane

| Upstream Go | Here |
| --- | --- |
| `wgengine/magicsock` | [`ts_magicsock`](ts_magicsock/src/lib.rs) + [`ts_runtime::direct`](ts_runtime/src/direct.rs) |
| `disco` | [`ts_disco_protocol`](ts_disco_protocol/src/lib.rs) |
| `net/stun` | STUN parsing/probing inside [`ts_magicsock`](ts_magicsock/src/lib.rs) |
| `net/netcheck` | [`ts_netcheck`](ts_netcheck/src/lib.rs) |
| `derp`, `derp/derphttp` (client half only) | [`ts_derp`](ts_derp/src/lib.rs) — **note added at this revision:** the crate models and decodes all sixteen of `derp.FrameType`'s constants, which is why every structural check here reported it complete; what it does *with* three of them diverges from `magicsock`'s reader, and those are the first three rows of §B |
| `net/packet` | [`ts_packet`](ts_packet/src/lib.rs) + the decode/classify path in [`ts_dataplane`](ts_dataplane/src/lib.rs) |
| `wgengine/filter` | [`ts_packetfilter`](ts_packetfilter/src/lib.rs), [`ts_bart_packetfilter`](ts_bart_packetfilter/src/lib.rs), [`ts_packetfilter_state`](ts_packetfilter_state/src/lib.rs) |
| `wgengine` packet flow + `wgengine/wgcfg` | [`ts_dataplane`](ts_dataplane/src/lib.rs) |
| `golang.zx2c4.com/wireguard` device (upstream dependency) | [`ts_tunnel`](ts_tunnel/src/lib.rs) (partial WireGuard implementation) |
| `net/tstun` | [`ts_transport_tun`](ts_transport_tun/src/lib.rs) behind the [`ts_transport`](ts_transport/src/lib.rs) traits |
| `wgengine/netstack` (gVisor) | [`ts_netstack_smoltcp`](ts_netstack_smoltcp/src/lib.rs), [`…_core`](ts_netstack_smoltcp_core/src/lib.rs), [`…_socket`](ts_netstack_smoltcp_socket/src/lib.rs) |
| `wgengine/netstack` forwarding (subnet router / exit node) + `net/tsdial` | [`ts_forwarder`](ts_forwarder/src/lib.rs) (plus the fork-only upstream-proxy egress, see [`AGENTS.md`](AGENTS.md)) |
| peer/route selection (Go keeps this inside `magicsock`/`wgengine`) | [`ts_overlay_router`](ts_overlay_router/src/lib.rs), [`ts_underlay_router`](ts_underlay_router/src/lib.rs) |
| `net/routemanager` (split out of `wgengine/wgcfg`, `a5102d3fc`) | [`ts_overlay_router`](ts_overlay_router/src/lib.rs) + the route/IP indexes in [`ts_runtime::peer_tracker`](ts_runtime/src/peer_tracker/peer_db.rs) — new to this mapping at the **previous** revision; it is where upstream now keeps the per-peer data-plane attributes (`PeerRoute.Jailed`, `MasqAddr4`/`MasqAddr6`) that `net/tstun` reads, and two §B rows come out of it |
| `wgengine/router`, OS side of `net/dns` | [`ts_host_net`](ts_host_net/src/lib.rs) (Linux `ip`/`resolvectl`, macOS `route`/`scutil`) |
| `net/dns/resolver` wire encoding | [`ts_dns_wire`](ts_dns_wire/src/lib.rs) + the MagicDNS server in [`ts_runtime::magic_dns`](ts_runtime/src/magic_dns.rs) |
| `net/netmon` | [`ts_netmon`](ts_netmon/src/lib.rs) |
| `net/art` + `github.com/gaissmai/bart` | [`ts_bart`](ts_bart/src/lib.rs) (+ [`ts_array256`](ts_array256/src/lib.rs), [`ts_bitset`](ts_bitset/src/lib.rs), [`ts_dynbitset`](ts_dynbitset/src/lib.rs)) |

### Runtime, API and utilities

| Upstream Go | Here |
| --- | --- |
| `tsnet` | the [`tailscale`](src/lib.rs) crate; [`tailscale::tsnet`](src/tsnet.rs) is the Go-shaped facade (see [`docs/TSNET_PARITY.md`](docs/TSNET_PARITY.md)) |
| `ipn/ipnlocal`, `tsd` (backend wiring, netmap → engine) | [`ts_runtime`](ts_runtime/src/lib.rs) (actor-per-concern) |
| `ipn` bus / `ipn/ipnstate` | [`ts_runtime::ipn_bus`](ts_runtime/src/ipn_bus.rs), [`ts_runtime::status`](ts_runtime/src/status.rs) |
| `ipn/store` (`FileStore`), `types/persist` | [`tsnet::StateStore` / `FileStore`](src/tsnet.rs) over `Config::key_state` ([`ts_keys::PersistState`](ts_keys/src/lib.rs)) |
| `net/socks5` (as used by `tsnet.Server.Loopback`) | [`src/loopback.rs`](src/loopback.rs) |
| `feature/taildrop` | [`ts_runtime::taildrop`](ts_runtime/src/taildrop.rs), [`…::taildrop_send`](ts_runtime/src/taildrop_send.rs), [`ts_runtime::peerapi`](ts_runtime/src/peerapi.rs) |
| `feature/ssh` / `ssh/tailssh` | [`src/ssh/`](src/ssh/mod.rs) (behind the `ssh` feature) |
| `sessionrecording` (client half only) | [`src/ssh/recording.rs`](src/ssh/recording.rs) — moved out of the no-counterpart list four revisions ago; a rule carrying `recorders` now streams the session to them and applies Go's `onRecordingFailure`, instead of refusing the session. **Added to the sweep list at this revision**, three revisions late: it is its own top-level upstream package with its own commits, and the standing argument that `ssh/tailssh` covers it is false — see §B |
| `feature/acme` + serve/funnel | [`ts_runtime::serve`](ts_runtime/src/serve.rs) (+ the `acme` feature) |
| `util/clientmetric` | [`ts_metrics`](ts_metrics/src/lib.rs) |
| `tstime` | [`ts_time`](ts_time/src/lib.rs) |
| `tstest` | [`ts_test_util`](ts_test_util/src/lib.rs) |
| `tool/` + CI plumbing | [`checks`](checks/src/main.rs) / [`bin/check`](bin/check), [`ts_devtools`](ts_devtools/src), [`ts_cli_util`](ts_cli_util/src/lib.rs), [`ts_hexdump`](ts_hexdump/src/lib.rs) |

### Upstream packages with no counterpart here

Not a backlog — most of these are deliberate scope decisions. Listed so a future porting bead is
cut with its eyes open. Items already tracked in
[`docs/PARITY_ROADMAP.md`](docs/PARITY_ROADMAP.md) are marked *(roadmap)*.

- `derp/derpserver`, `cmd/derper` — DERP **server**/mesh. Client half only here *(roadmap)*.
- `net/udprelay`, `feature/relayserver` — peer-relay endpoint allocation and relay **serving**.
  The relay *client* half is here (`ts_magicsock`'s relay module: the disco `0x04`–`0x09` codecs,
  the 3-way bind handshake and the Geneve-framed relay data path); this node never serves as a
  relay itself, and does not send `AllocateUDPRelayEndpointRequest` — a relay-capable peer
  allocates on our behalf and announces the endpoint with a `CallMeMaybeVia`.
- `net/portmapper`, `feature/portmapper`, `feature/debugportmapper` — UPnP / PCP / NAT-PMP *(roadmap)*.
- `appc`, `feature/conn25`, `types/appctype` — app connectors (classic and conn25) *(roadmap)*.
- `drive`, `feature/drive` — Taildrive.
- `ipn/ipnserver`, `cmd/tailscaled`, `cmd/tailscale` — the daemon and its CLI. This is an embedded
  library; status/WhoIs/id-token are typed methods on `Device` instead.
- `ipn/localapi`, `client/local` — **partial**: the `tsnet` facade serves a one-route LocalAPI
  (`GET /localapi/v0/status`, with Go's `Sec-Tailscale: localapi` header check and Basic auth) in
  [`src/tsnet.rs`](src/tsnet.rs) `mod localapi`; Go's dozens of other endpoints return 404, and
  `Device::loopback` deliberately serves SOCKS5 only ([`src/loopback.rs`](src/loopback.rs)).
- `health` — the health tracker. `tailcfg.DisplayMessage` is modelled in `ts_control_serde`, but
  Go's tracker semantics (warnable state machine, self-diagnosis) are not.
- `logtail`, `logpolicy`, `feature/syslog` — client log upload.
- `wgengine/netlog`, `feature/netlog` — network flow logs *(roadmap: externally blocked)*.
- `net/captivedetection`, `feature/captiveportal` — captive-portal detection.
- `feature/androiddns`, `feature/androidbin` — Android support for *raw* binaries: DNS through
  Android's system `dnsproxyd` cache (`86b3cd5aa`), and a `net/netmon` interface-getter fallback plus
  an Android CA-root path for binaries that have no `/etc/resolv.conf`, no bionic libc and no
  `NETLINK_ROUTE` (`60d9c54b6`). `ts_host_net` has Linux and macOS backends only, and
  `ts_netmon` has no OS backend at all yet, so neither has a target here.
- `net/connreject`, `feature/connreject` — the opt-in, LRU-bounded aggregator of recent
  connection-rejection events (TSMP rejects received and sent, and pendopen timeouts) behind
  `nodecap.ConnReject` and the `GET /debug/rejects` c2n endpoint, added at `85c1efb46` — the commit
  that raised `CurrentCapabilityVersion` to 146. The *diagnostics* are out of scope for an embedded
  node (§A row 146). The TSMP rejected-connection messages they count were a §B row of their own at
  the previous revision and are now implemented in both directions (#421); the aggregator on top of
  them stays out.
- `net/dnscache`, `net/dnsfallback`, `feature/dnsresolvecache` — the resolution cache in front of Go's
  *control-plane and DERP* dials, its DERP-based bootstrap-DNS fallback, and the on-disk persistence
  of last-known-good answers for those hostnames (`aa2681ac5`, tightened at this pin by `023255e8a`
  so an answer is persisted only once it has passed TLS verification). No counterpart: this
  fork dials control through control's own `DialPlan` (`ts_control::dial_plan`, which carries literal
  addresses) and falls back to the system resolver, so there is no client-side answer cache to persist.
  Upstream links the new feature into `tailscaled` and deliberately **not** into `tsnet`.
- `portlist`, `posture`, `feature/posture` — port-list and device-posture reporting to control.
- `net/tshttpproxy` — HTTP proxy support for *outbound control/DERP* dials. (The fork's
  `ProxyExitDialer` is the opposite direction — exit-node egress — and is not a port of this.)
- `tsconsensus`, `prober`, `safeweb`, `tsweb`, `wf`, `util/syspolicy`,
  `clientupdate`, `feature/wakeonlan`, `feature/tap`, `feature/tpm`, `feature/bird`,
  `feature/linkspeed`, `feature/tundevstats`, `feature/routecheck`,
  `feature/favorites`, `feature/serviceclientprefs`, `k8s-operator`, `kube` — platform, operator
  and product surfaces outside the embedded-node scope.
- `feature/remoteconfig` — **partial**, and moved out of the list above two revisions ago: its
  `c2nPrefix`, `localAPIStrip` and `handleC2NRemoteAPI` (the c2n → LocalAPI proxy of capver 142) are
  ported into `ts_control/src/tokio/ping.rs`; the rest of the package — the remote-config prefs
  surface and its CLI — is not.
- `ts_ffi`, `ts_python`, `ts_elixir` have no upstream counterpart in `tailscale/tailscale` at all —
  Go's C bindings live in the separate `tailscale/libtailscale` repository.

## Gap list

Every row was checked against the pinned upstream commit **and** against this tree; the evidence
is named inline so a reviewer can re-check a single row without re-deriving the whole ledger.
Assessments are one of **needs port**, **not applicable**, **already covered**.

### A. Capability versions 131 → 147

This is the sharpest available axis: `tailcfg.CurrentCapabilityVersion` is upstream's own record of
every client behaviour change that control can observe. The window is anchored to capver 130, the
last version this port tracked before the ledger existed; the declaration here is **125**, held
below 126, see §B. Upstream is at **147** at the pin — `tailcfg/tailcfg.go:199` — so the window is
**seventeen** versions, as at the previous revision. Descriptions are upstream's own
(`tailcfg/tailcfg.go`, `tailcfg/nodecap`).

**No row is new at this revision.** The pin did not move, and the two §A commands were re-run against
it anyway: `tailcfg/tailcfg.go:199` still reads `CurrentCapabilityVersion CapabilityVersion = 147`, and
the capability-history comment returns exactly **18** lines, 130 through 147 — the same count the three
previous revisions derived. An unmoved pin is when that count earns its keep: "upstream
added nothing", "I re-ran it against the same tree" and "the pattern broke" all feel the same, and
only the count tells them apart. An empty or short result still means the pattern broke.

**No row changed assessment, and this revision is the first at which nothing could have changed one.**
Rows **133** and **147** are still the two open capability-version rows. The interval's only two commits
are documentation (#465, #466), so every §A row's tree-side evidence is byte-for-byte what the previous
revision checked, and every row's upstream-side evidence is at the same pin. That is worth stating rather
than skipping: an §A section that reports "no change" after a re-derivation it did not run is
indistinguishable from one that ran it, and the counts below are the only thing that tells the two apart.
Row **133**, which the previous revision had to re-check because `magic_dns` had moved underneath it,
needed no such re-check this time and is re-stated below anyway on its unchanged evidence. Row **137**
keeps its *already covered* verdict and the caveat the previous revision gave it — upstream's register
path now tests `isRateLimitedResponse(res)`, which also accepts a `503` carrying a `Retry-After` — and
that widening is still row 147's business rather than 137's. The rows earlier revisions flipped (135,
142, 144) still say why they flipped, because that history is what makes the row re-checkable.

One cross-check is the cheapest confirmation this section has, and re-run at this revision it returned
the same **65** unhandled attributes out of **85** constants, as at the previous three revisions: the node-attribute walk described under
[Re-deriving this ledger](#re-deriving-this-ledger) reports,
independently of this table, which attribute strings appear nowhere in this workspace. Every
attribute named by a row below still comes back **unhandled** — `default-auto-update` (131),
`disable-hosts-file-updates` (132), `force-register-magicdns-ipv4-only` (133),
`disable-android-bind-to-active-network` (134), `disable-linux-cgnat-drop-rule` (136),
`emit-runtime-metrics` (139), the four GRO/GSO names (140), `never-gso-equal-tail` (141),
`scope-quad100-macos` (145) and `debug-conn-reject` (146) — and the two attributes of the one row
marked *already covered* on an attribute, 135, come back **handled**. That is the table agreeing
with the tree by a route that does not read the table. It also says something the table does not:
for every row above assessed *not applicable*, the attribute is not merely unimplemented but
unnamed, so a future reader grepping for it finds nothing and must come back here. Row 147 is the
exception that proves the walk's limit — it is gated on no attribute at all, so the walk cannot see
it, and it had to come from the delta.


| Ver | Date | Upstream change | Assessment |
| --- | --- | --- | --- |
| 131 | 2025-11-25 | Client respects `NodeAttrDefaultAutoUpdate` | **not applicable** — self-updating a client binary; this is an embedded library with no updatable binary (`Hostinfo.allows_update` is modelled and false by default) |
| 132 | 2026-02-13 | Client respects `NodeAttrDisableHostsFileUpdates` | **not applicable** — nothing here writes a hosts file; upstream notes the attr is Windows-only as of 2026-02, and there is no Windows `ts_host_net` backend |
| 133 | 2026-02-17 | `NodeAttrForceRegisterMagicDNSIPv4Only`; MagicDNS IPv6 registered with the OS by default | **needs port** — upstream `net/dns/config.go` `serviceIPs` registers **both** `100.100.100.100` and the IPv6 service IP with the OS resolver *by default*, and falls back to IPv4-only when control sets the attr. This tree registers IPv4 only *unconditionally* — which is upstream's attr-set branch, not its default — so the behaviours are not equivalent. Wider than a type signature: the IPv6 MagicDNS service IP is not served here at all (`ts_runtime::magic_dns` binds `100.100.100.100:53` only; `ts_host_net::HostDns::nameservers` is `Vec<Ipv4Addr>` for both the Linux and macOS backends), so registering it before serving it would point the host resolver at a dead address. The port is: serve MagicDNS on the IPv6 service IP, register both by default, honour the attr to drop back to IPv4-only. Host-OS-facing, not wire-facing — no peer or control plane observes it directly — and it pairs with `Config::enable_ipv6` |
| 134 | 2026-03-09 | Client understands `NodeAttrDisableAndroidBindToActiveNetwork` | **not applicable** — Android-only socket binding |
| 135 | 2026-03-30 | Client understands `NodeAttrCacheNetworkMaps` (and `DisableCacheNetworkMaps`, #19947) | **already covered** — *changed from "needs port (optional)"*: the cache landed here in #320, two revisions before this one. `ts_control/src/tokio/netmap_cache.rs` persists the raw decompressed `MapResponse` to `<Config::netmap_cache_dir>/netmap.json` (0600 under a 0700 directory, temp-file rename), `ts_runtime/src/control_runner.rs:1472` loads it before the control client exists, and *both* attributes are honoured — `disable-cache-network-maps` takes precedence and discards an existing cache, as upstream documents. Inert unless the embedder configures storage **and** control grants the attribute |
| 136 | 2026-04-09 | Client understands `NodeAttrDisableLinuxCGNATDropRule` | **not applicable** — `ts_host_net` programs routes and DNS only; it never installs firewall rules, so there is no CGNAT DROP rule to disable |
| 137 | 2026-04-15 | Client handles 429 responses to `/machine/register` | **already covered** — `ts_control/src/tokio/register.rs:261` parses the 429 plus its retry delay into a typed rate-limit error instead of an opaque HTTP error. *Caveat new at the previous revision:* `29cfb0b4c` replaced upstream's literal `res.StatusCode == 429` at this same call site (`control/controlclient/direct.go:828`) with `isRateLimitedResponse(res)`, which also accepts a `503` carrying a `Retry-After`. This row's own claim is still met — a 429 to `/machine/register` is handled — but the call site it names is now wider upstream than here. See row 147 |
| 138 | 2026-03-31 | Can handle c2n `/debug/tka` (`/debug/tka/log`) | **not applicable (declaration held below it)** — the c2n responder (`ts_control/src/tokio/ping.rs`) serves `/echo`, `GET /vip-services` and the `/remoteapi/localapi/*` prefix; `/debug/tka/log` is not among them and takes Go's own `400`/`unknown c2n path` fallthrough, which is asserted by test. The declared capability version is held below the versions that promise it, so control never asks. Resolved together with 127 and 128; see §B |
| 139 | 2026-05-22 | Client understands `NodeAttrEmitRuntimeMetrics` (emit Go `runtime/metrics` as clientmetrics) | **not applicable** — the attr exports the *Go runtime's* metrics; there is no Rust equivalent. `ts_metrics` already mirrors `util/clientmetric` itself |
| 140 | 2026-05-27 | Client understands `NodeAttrDisableUDPGRO` / `DisableUDPGSO` / `DisableTUNUDPGRO` / `DisableTUNTCPGRO` | **not applicable** — no GRO/GSO offload on this datapath (`ts_transport_tun` is single-queue, no offload), so there is nothing for control to disable |
| 141 | 2026-05-28 | Client understands `NodeAttrNeverGSOEqualTail` | **not applicable** — same: the attr is a workaround for kernel GSO batching this port does not do |
| 142 | 2026-07-06 | Client understands c2n `/remoteapi/localapi/*` proxy (`feature/remoteconfig`) | **already covered** — *changed from "needs port (narrow)"*: #317 gave the responder the prefix route it lacked. `ts_control/src/tokio/ping.rs` now walks Go's own dispatch order (exact method+path, exact path, then prefixes, then the 400), strips `/remoteapi`, and carries all four of `handleC2NRemoteAPI`'s refusals. Caveat worth keeping in view: control gates this request on the *declared* capability version, so with 125 declared the handler is implemented but unreachable. A capability version is a contiguous claim, so it becomes live only once 126 through 141 are all implementable — see §B for the full list standing in the way |
| 143 | 2026-07-22 | Client correctly ignores conn25 node attributes when not enabled by environment variable | **not applicable** — no app connector of either generation here, so conn25 attributes are already ignored |
| 144 | 2026-07-31 | Client sends `packet.TSMPDiscoKeyAdvertisement` around WireGuard handshakes | **already covered** — *changed from "needs port"*: the send half landed in #314 and #318, so both halves are now here. `ts_packet::tsmp` marshals against Go's own `TestTSMPDiscoKeyAdvertisementMarshal` vectors, `ts_tunnel` reports the two moments `wireguard-go` calls `SendPriorityMessage`, and `ts_dataplane` chooses the content (Go `magicsock.Conn.PriorityMessageForPeer`). Unlike 142 this is peer-observable regardless of the declared version — the client sends it unprompted — so it is the one changed row a real Go peer can see |
| 145 | 2026-08-04 | Client understands `NodeAttrScopeQuad100OnMacOS` | **not applicable** — the attr changes resolver ordering for the *sandboxed* macOS app; `ts_host_net::macos` installs a service-scoped `scutil` DNS dictionary and has no default-resolver behaviour to scope |
| 146 | 2026-09-02 | Client understands `NodeAttrConnReject` (`debug-conn-reject`); can handle c2n `GET /debug/rejects` | **not applicable (declaration held below it)** — *new two revisions ago* (`85c1efb46`). The attribute turns on an in-memory, LRU-bounded aggregator of recent connection-rejection events (TSMP rejects received, TSMP rejects sent on ACL-blocked inbound flows, pendopen timeouts) keyed by direction/proto/peer/reason, and exposes it over a LocalAPI route and a c2n `GET /debug/rejects`. Both surfaces are out of scope here for reasons already recorded: the c2n route joins 127, 128 and 138 behind the held declaration and takes Go's own `400`/`unknown c2n path` fallthrough, and this fork's LocalAPI serves one route. The attribute is off by default at the control plane, so ignoring it is what a Go client without the feature does. The §B row this version opened when it was new — the TSMP rejected-connection messages the aggregator *counts* — closed at the previous revision: both halves landed here in #421. The aggregator on top of them stays out of scope for the reasons above, so this row does not move |
| 147 | 2026-09-09 | Client handles 429/503 responses with `Retry-After` headers to `/machine/` endpoints | **needs port** — *new at the previous revision* (`29cfb0b4c`, the one delta commit that moved the capability version). Three separable behaviours, and this tree has the first only in part. (1) **Which responses count.** `control/controlclient/direct.go:621` `isRateLimitedResponse` returns true for `429` always, and for `503` **only when a `Retry-After` header is present** — a bare `503` stays a generic failure subject to backoff. That asymmetry is the refusal to port, not decoration. Here, `ts_control/src/tokio/register.rs:261` tests `status.as_u16() == 429` and nothing else. (2) **Where it is checked.** Upstream now also checks it in `sendMapRequest` (`direct.go:1196`): a non-200 **map** response that is rate-limited becomes a typed `rateLimitError` instead of the generic `initial fetch failed %d: %.200s`. Here, `ts_control/src/tokio/map_stream.rs:543` collapses every non-success map status into an opaque `MapStreamError::Http`, reading no header — so `ts_control::Error::RateLimited` is constructed in exactly one place in this tree (`register.rs:274`) and the map poll can never produce one. The rate-limit arm that already exists in `ts_control/src/tokio/client.rs:659` is therefore reachable only for the *re-register* inside the poll loop, which is what its own log message at `:671` says. (3) **How long to wait.** `control/controlclient/auto.go:655` `waitRetryAfter` caps the server-requested delay at `maxRetryWindow = 5 * time.Minute`, and in `mapRoutine` (`auto.go:631`) a rate-limited poll **skips `bo.BackOff` entirely** rather than backing off as well. This tree clamps at `MAX_RETRY_AFTER = 1 hour` (`register.rs:303`, mirroring Go's older `parseRateLimitError` bound, which upstream kept) and has no second, tighter cap at the wait site. Control-observable: a rate-limited node here reconnects on local backoff and ignores the cooldown control asked for |

Net: of the seventeen versions upstream added, **two need a port** — 133, host-OS-facing, and 147,
control-observable — **four are already covered** (135, 137, 142, 144), and the remaining eleven are
not applicable to an embedded userspace node (138 and 146 among them, once the declaration was held
below the versions that promise them). The count is unchanged from the previous revision: nothing that
was open closed, and nothing that was closed reopened.

Row 133 was re-derived against the tree at this revision — this time against a tree in which nothing
at all changed, which is a weaker check than the previous revision's and is recorded as such — and is
still open: `ts_host_net::HostDns::nameservers`
([`ts_host_net/src/lib.rs:44`](ts_host_net/src/lib.rs)) is still
a `Vec<Ipv4Addr>`, its doc comment still says "IPv4 nameservers", the Linux backend still fills
it with the single literal `100.100.100.100`, `ts_runtime::magic_dns` still binds
`100.100.100.100:53` only (`MAGIC_DNS_IP` is an `Ipv4Addr` constant, `magic_dns.rs:146`), and
`force-register-magicdns-ipv4-only` still appears nowhere in the tree
— so there is still no IPv6 MagicDNS address either to serve or to register, and no attr to drop back
from if there were. The previous revision had to argue that #462 and #464 did not touch the row,
because they changed `magic_dns`; at this revision no Rust changed at all, so the four pieces of
evidence above are literally the same bytes it checked. The row is still *not* closed by #347 (the quad-100 absorption fix in §B), which made the TUN
transport absorb every quad-100 packet whatever its port and protocol — that is about traffic already
addressed to `100.100.100.100`, and it neither serves nor registers the IPv6 service IP, which is
what 133 asks for.

Row 133 sits at the centre of **two** rows rather than three since the previous revision, when the
third closed. A reader who finds one wants both, so they are named here once:

- 133 itself — which *addresses of this node's resolver* the host OS is pointed at (register both
  service IPs by default; honour `force-register-magicdns-ipv4-only` to drop back to IPv4).
- the `magicdns-aaaa` row in §B — which *addresses of peers* that resolver hands out.

The third, the *host route set* row — which *peer prefixes* the host FIB carries, and Go's
`one-cgnat` knob over it — was ported in #438 and is closed. It did not leave the group cleanly: the
audit of that port (§B) found the tri-state inverted and upstream's platform gate missing, so two
**new** rows stand where the old one did. Both are still host-OS-facing, which is what put the group
together in the first place.

All of these are host-OS-facing rather than wire-facing, and all are decided in this tree by local
`Config` rather than by what control asked for. Grouping them is not a claim that they are one job.
Row 147, by contrast, is control-facing and belongs to none of this group.


### B. Behaviour upstream changed in the window that is not capver-gated

Derived from `git log --since=2025-10-06` over the packages that map to crates here, with
docs/typo/refactor commits filtered out. **The sweep list is unchanged at this revision and was
re-run in full** — thirty-eight entries, **854** distinct commits across them at the pin — and, as at the
previous revision, nothing the list itself got wrong: every package in
[Package mapping](#package-mapping) that has an upstream path has a loop entry, and
`git log --diff-filter=A --oneline e2ed43239..<new-pin> -- '*/*.go'` is empty by construction,
because the new pin is the old one. The five times the list *was* short are written up under
[Re-deriving this ledger](#re-deriving-this-ledger) and those lessons stand unchanged.

**The 854/857 discrepancy the previous revision recorded is resolved at this one, in the only way it
could be: by a third run.** The loop as printed under [Re-deriving this ledger](#re-deriving-this-ledger)
returns **854** again at the unmoved pin — 38 entries, 854 distinct commits — so 854 has now been
derived twice and 857 once, and 857 is the outlier. The value of writing the number down was never the
number; it was that a count which drifts is the only cheap signal that the command, and not upstream,
is what changed. Two agreeing runs of a command printed verbatim is what closes that, and it is why the
discipline the previous revision proposed — **when a derivation records a count, record the command that
produced it exactly** — stays in the recipe.

**What is different at this revision is that nothing moved on either side, and the reading still paid
three rows.** Upstream is at the pin for the third consecutive revision; this tree's only two commits
are documentation. So of the four sources of change named under
[Re-deriving this ledger](#re-deriving-this-ledger), three could not fire — upstream did not move, the
tree did not move, and no port merged to audit. Only the third could: **the sweep can be read more
carefully, or widened, and surface something that was true all along.** What paid was the fifth
technique, *read the enumerations, not their history*, applied to an enumeration no revision had
opened: **`derp/derp.go`'s sixteen `FrameType` constants**, taken one at a time and asked not "does
`ts_derp` model this frame" (it models all sixteen) but "what does this node *do* when it arrives, and
when does it send one". Three rows, all in the DERP relay path. The generalisation is the one
`controlknobs` established and this revision confirms on a second enumeration: **a package that
enumerates something owes you a read of the enumeration**, and modelling every member of it is not the
same as acting on every member of it. `tailcfg.PeerChange` was walked the same way at this revision and
paid nothing — all eleven fields are modelled *and* applied — which is what a clean enumeration read
looks like, and is recorded below so the next revision does not re-derive it.

A note on wording: rows carried from before this revision keep the revision-relative phrasing
they were written with ("new at this revision", "unchanged at this pin"), and that phrasing refers to
the revision that wrote the row. Text written at *this* revision is the header table, §A's opening
paragraphs and its row-133 re-derivation, this section's opening, [What changed at this
revision](#what-changed-at-this-revision), the first three rows under [Rows](#rows), and [Read at this
revision](#read-at-this-revision). The previous revision's three DNS rows keep their own wording and
are now the fourth, fifth and sixth rows below.

#### What changed at this revision

Read this first: it is the shortest honest summary of the diff between this ledger revision and the
last one.

- **Neither side moved.** `git ls-remote https://github.com/tailscale/tailscale HEAD` returned
  `e2ed432399c9b0fda7aa14e9eb27784d2d893c55`, the pin, for the third revision running, so
  `git log --oneline e2ed43239..<new-pin>` is empty. `git log --oneline 0c8796a..HEAD` is two commits,
  both documentation: `50a0db7` (#465, the previous revision of this document) and `9675565` (#466,
  a correction to two entries in the backlog file #465 wrote). **No Rust changed in the interval.**
  That is a stronger statement than "the delta was small", and it is what makes the rest of this list
  short: no row could close, no carried row's tree-side evidence could move, and there was no merged
  port to audit.
- **The mechanical commands were re-run anyway, and every count matched.** **18** capability-history
  lines, 130 through 147; **38** sweep entries over **854** distinct commits — the same 854 the previous
  revision derived, which settles the 854/857 question above; **85** node-attribute constants, **65**
  unhandled and **20** matched, the same sixteen genuine reads and four known false positives. Running
  them against an unchanged tree at an unchanged pin proves nothing about upstream and everything about
  the commands: a count that matches is a command that still works. **The two wire-type walks and the
  three judgement-bearing techniques were not re-derived**, because both sides of what they compare are
  unchanged; which were skipped and why is stated explicitly under
  [Re-deriving this ledger](#re-deriving-this-ledger), so the next revision inherits a claim it can
  check rather than an implication it cannot.
- **Three rows opened, all from one enumeration nobody had opened.** `derp/derp.go` defines sixteen
  `FrameType` constants (`derp.go:71`–`:134`). `ts_derp` models all sixteen and decodes all sixteen,
  which is why every earlier pass over the DERP package read as complete. Asked instead what this node
  *does* with each, three answers diverge from Go: a `PeerGone` frame is decoded and thrown away where
  Go uses it to invalidate a route (`wgengine/magicsock/derp.go:665`); the route Go learns from a
  received frame is consulted here only when the netmap named no home region, where Go prefers it over
  dialling the peer's home region at all (`derp.go:375`); and `FrameNotePreferred` is modelled and
  never sent, where Go sends it on every connection to the region it considers home
  (`derp/derphttp/derphttp_client.go:423`, `:569`). The first three rows below.
- **One enumeration was walked and came back clean, which is recorded rather than dropped.**
  `tailcfg.PeerChange` — the eleven fields control may patch onto a peer mid-session — is modelled
  field for field by `ts_control_serde::netmap::PeerChange` (`netmap.rs:546`) **and** applied field for
  field by `PeerTracker::apply_peer_patches` (`ts_runtime/src/peer_tracker/mod.rs:1856`), including the
  two that are easy to model and forget, `Online` and `LastSeen`. A clean enumeration read is worth a
  paragraph because the alternative is that the next revision spends the same hour on it.
- **No carried row changed assessment, and this time that claim costs nothing to make.** The eight §B
  rows carried from the previous revision — the three DNS forwarder rows, `disable-relay-client`, the
  two `one-cgnat` rows and the two `disable-delta-updates` rows — are stated against tree code that did
  not change, at a pin that did not move. The same holds for the five deliberate divergences recorded
  at earlier revisions (DNS-after-router-failure, SSH `acceptEnv`, the `callMeMaybe` gate, the peerAPI
  DoH server's authoritative-answer widening, and the race's treatment of an unmatched datagram).
- **Nothing closed, and nothing was audited, for the same reason.** The previous revision closed a row
  and its audit of that closure opened three. This revision has no closure to audit: the audit source
  needs a merged port and there was none. The previous revision's audit section is
  kept verbatim below under [Read at this revision](#read-at-this-revision), because a verdict whose
  evidence is still true does not stop being useful when the revision that wrote it passes.
- **No recipe changed.** Of the three commands earlier revisions had to fix, the one re-run here — the
  node-attribute walk, with its widened character class — behaved exactly as it did last time; the two
  wire-name walks were not re-run, per the bullet above. The only line of the recipe that moves every
  revision — the tree-delta range in
  [Re-deriving this ledger](#re-deriving-this-ledger) — follows the header table as usual.

#### Rows

- **A DERP `PeerGone` frame is decoded and thrown away, so a learned relay route is never invalidated**
  (`wgengine/magicsock/derp.go:652`–`:665`, `:566`–`:569`; `derp/derp.go:81`–`:88` for the frame's own
  contract) — **needs port.** Upstream's `runDerpReader` treats `derp.PeerGoneMessage` as a route
  event: whatever the reason byte says, it calls `c.removeDerpPeerRoute(peer, regionID, dc)`
  (`derp.go:665`), deleting the `derpRoute[peer]` entry it added at `:626` when that peer's first
  packet arrived on this connection. It does the same thing wholesale when the connection itself dies
  — on any `RecvDetail` error it walks `peerPresent` and removes the route for every peer it learned
  on that connection (`:566`–`:569`). The frame exists for exactly this: `derp/derp.go:81`–`:88` says
  `FramePeerGone` is sent "so B can forget that a reverse path exists on that connection to get back
  to A", and it is *also* sent when "A tries to send a CallMeMaybe to B and the server has no record
  of B" — the `PeerGoneReasonNotHere` case, which Go counts separately
  (`metricRecvDiscoDERPPeerNotHere`, `derp.go:657`) precisely because it means "this server cannot
  reach that peer". Here the frame gets as far as being parsed correctly and no further.
  `ts_derp::Client::recv_one` (`ts_derp/src/client.rs:249`–`:282`) decodes the 32-byte key, decodes the
  optional reason byte totally, logs at `debug` — and then loops. `recv_one` returns only
  `(NodePublicKey, PacketMut)` for a `RecvPacket`, so **there is no channel by which a `PeerGone` can
  leave `ts_derp` at all**, and `grep -rn PeerGone --include='*.rs'` outside that crate returns
  nothing. The map it should invalidate is `Multiderp::observed_routes`
  (`ts_runtime/src/multiderp.rs:100`), written on every received frame by `record_observed_route`
  (`:421`, called at `:559`) and pruned only against the live netmap on a peer-state update (`:751`).
  So a peer whose route this node learned keeps it until control removes the peer: after the server
  says the peer is gone — including the `NotHere` case, which is the server stating it has no path —
  `RouteUpdater` (`ts_runtime/src/route_updater.rs:261`) keeps handing that region's transport id to
  every WireGuard packet for that peer. Peer-observable, as a black hole rather than an error: the
  packets are relayed to a server that will drop them, the handshake never completes, and nothing in
  this node's own logs distinguishes it from a peer that is simply offline. The port has three
  separable pieces and they should be one change: give `recv_one` a way to surface a control event
  (or give the region runner in `run_derp_once` the frame directly — it already sees every frame and
  already knows its own region id, which is what `record_observed_route` needs), remove the observed
  route on `PeerGone` **scoped to the region the frame arrived on**, exactly as Go's
  `removeDerpPeerRoute` compares `derpRoute{regionID, dc}` before deleting (`derp.go:56`–`:59`) so a
  frame from one region cannot clear a route learned on another, and drop every route learned on a
  connection when that connection fails, as `:566`–`:569` does. Test the scoping and the reason
  totality, not just the happy path: a `PeerGone` for a peer whose observed route points at a
  *different* region must leave that route alone; a 32-byte reason-less `PeerGone` must invalidate
  exactly as a 33-byte one does (`ts_derp` already accepts both, and the existing
  `reasonless_32_byte_peer_gone_is_accepted_then_continues` test must keep passing); and an unknown
  future reason byte must invalidate too, because Go's `default` arm removes the route after counting
  `metricRecvDiscoDERPPeerGoneUnknown`. Assert on the map the production code mutates, not on a
  re-derivation of it.

- **The DERP route learned from a peer's traffic is used only when the netmap named no home region,
  where Go prefers it over dialling that peer's home region** (`wgengine/magicsock/derp.go:339`
  `derpWriteChanForRegion`, the `derpRoute` arm at `:375`; `endpoint.go:1130` for the other use) —
  **needs port, and it is a decision, not a transcription.** Go asks the question in one place and in a
  fixed order. `derpWriteChanForRegion(regionID, peer)` is called with the peer's *home* region and:
  (1) if there is already an active connection to that region, use it (`:364`); (2) **otherwise, if
  `derpRoute[peer]` names a region this node is already connected to, use that connection instead**
  (`:375`–`:381`) — the comment is explicit about why, and names the issue it fixed: "perhaps peer's
  home is Frankfurt, but they dialed our home DERP node in SF to reach us, so we can reply to them
  using our SF connection rather than dialing Frankfurt. (Issue 150)"; (3) only then open a connection
  to the home region (`:389`). The separate `fallbackDERPRegionForPeer` (`:82`) is a *third*, narrower
  use, reached from `endpoint.go:1130` when a peer has neither a valid UDP endpoint nor a DERP home
  at all. **This tree ported (3) and the third use, and not (2).** `RouteUpdater`
  (`ts_runtime/src/route_updater.rs:261`) takes `peer.derp_region` whenever the netmap carried one and
  consults `Multiderp::region_for_peer` only in the `None` arm; `region_for_peer`
  (`ts_runtime/src/multiderp.rs:250`) and its doc comment (`:237`–`:249`) are written for that arm
  alone, and `docs/PARITY_ROADMAP.md` records the port (#24, v0.8.1) as "a peer with no netmap home
  region was denied any underlay route" — which is upstream's *third* use, faithfully done. The
  consequence is not a dropped packet, it is a connection: `Multiderp` spawns a runner for every
  region in the derp map (`multiderp.rs:726`) and lets non-home runners idle their connection out, so
  sending to a peer whose home is Frankfurt wakes a connection to Frankfurt even when this node is
  already connected to SF and has heard from that peer there. DERP-server-observable (a node appears
  at a relay a Go client would not have dialled) and cost-observable (one more concurrent DERP
  connection per remote region in which a peer talks to us). **The decision to make is whether to port
  it at all**, and there is a real argument on both sides, which is why this is a row rather than a
  bug. For: it is upstream behaviour, it is peer-visible in aggregate, and the data is already
  collected here — `record_observed_route` runs for every peer, not only for home-less ones, so only
  the *consumption* is narrow. Against: this fork's DERP model is a runner per region with an idle
  timeout, not Go's `activeDerp` map with a `lastWrite` clock, so "already connected" is a question
  `RouteUpdater` cannot ask today without new plumbing, and preferring an observed route over a
  netmap-stated home makes this node's relay choice depend on inbound traffic in a way the current
  design deliberately does not. Whichever way it goes, **write it down at `region_for_peer` and here**:
  the current doc comment claims "Go `derpRoute` parity" for a port that implements one of Go's three
  uses of `derpRoute`, and that sentence is how the next reader concludes there is nothing left to do.
  If it is ported, keep what the existing arm gets right — only a region with a live transport is ever
  returned (`resolve_region_for_peer`, `:387`) — and test it as Go frames it: a peer whose netmap home
  is region B, an observed route on region A, a live transport on A and none on B, must relay over A;
  the same peer with a live transport on B must relay over B, because Go checks the home region first.

- **`FrameNotePreferred` is modelled and never sent, so no DERP server this node connects to is ever
  told which of them it considers home** (`derp/derp.go:79`; `derp/derp_client.go:344` `NotePreferred`;
  sent from `derp/derphttp/derphttp_client.go:423` and `:569` on connect, and from
  `wgengine/magicsock/derp.go:292` and `:423` when the home region changes) — **needs port (narrow),
  and upstream says what it is worth.** A Go client sends a one-byte `0x07` frame — `0x01` for "you are
  my home", `0x00` for "you are not" — to each DERP server: on connect when it is the preferred one
  (`derphttp_client.go:423`, `:569`), and to **every** active connection when the home region moves,
  including the `false` to the region that just stopped being home (`derp.go:292`,
  `go ad.c.NotePreferred(i == c.myDerp)`). This tree has the frame type (`FrameType::NotePreferred`,
  `ts_derp/src/frame/frame_type.rs:28`) and its body (`ts_derp/src/frame/body/note_preferred.rs`), and
  nothing constructs one: the symbol appears only in those two files and the `mod.rs` re-export.
  `Multiderp` already knows the answer and already distributes it — `RegionEntry::home_derp`
  (`ts_runtime/src/multiderp.rs:116`) is a `watch` channel set true for the home region and false for
  the rest, and `run_derp_once` (`:483`) already awaits changes on it to arm its idle timeout — so the
  port is to send the frame on that edge and once after the handshake, not to build new state.
  **State the value honestly, because upstream does:** `derphttp_client.go:1066` says of
  `NotePreferred`, "It's only used for stats", and the server side bears that out —
  `derpserver.setPreferred` moves the `curHomeClients` gauge and the `homeMovesIn`/`homeMovesOut`
  counters and changes nothing about how a packet is delivered. So this is server-observable and not
  peer-observable: a real Tailscale relay's home-client accounting never counts this node, and this
  node's home moves never appear in it. That is a small thing to be wrong about and a cheap thing to
  fix, and the reason it is a row rather than a shrug is the shape it shares with the two rows above —
  a frame this tree models completely and acts on not at all. Port the edge as well as the connect:
  Go's `derphttp.Client.NotePreferred` (`:1067`) is idempotent (it returns early when the value is
  unchanged) and drops the connection for reconnect if the write fails (`:1079`), and a port that only
  sends `true` on connect, and never the `false` to a former home, tells a relay this node is still
  homed there forever.

- **The query forwarded upstream keeps the EDNS size its client asked for, where Go clamps it to
  4095 first** (`net/dns/resolver/forwarder.go:198` `clampEDNSSize`, called at `:1238` on the query
  and `:875` on the UDP answer; `net/dns/resolver/tsdns.go:50` `maxResponseBytes`) — **needs port**,
  and *new at this revision from auditing #462*, not from upstream: `forwarder.go` is byte-identical
  between the commit #462 cites and the pin. Go rewrites the requestor's UDP payload size **in
  place**, in the client's own OPT record, down to `maxResponseBytes` (4095) before any resolver is
  asked, and again on the datagram the resolver returned. The clamp is deliberately partial —
  `findOPTRecord` (`:152`) only recognises an OPT record at the very end of the message, with
  `RDLEN == 0` and EDNS version 0, and `clampEDNSSize` returns untouched when it finds none. This
  tree forwards the query byte for byte: `forward_query` (`ts_runtime/src/magic_dns.rs:1164`) hands
  `query` straight to `ask_upstream_udp`, and the module docs state the verbatim relay as a design
  fact (`magic_dns.rs:130`) rather than as a departure from Go. Nothing rewrites the answer's OPT
  record either. Two things follow. **Resolver-observable:** a stub advertising 8192 makes this node
  put 8192 on the wire where a Go node puts 4095, so the resolver sizes its reply for a datagram this
  node cannot receive — the netstack UDP receive ring is 4096 (`netcore::Config::udp_buffer_size`)
  and smoltcp drops an over-ring datagram at enqueue rather than delivering a short one. **And it
  interacts with what #462 landed:** since #462 that dropped datagram is a failed UDP hop, so the
  name still resolves, over a TCP connection to the resolver that a Go node would not have opened,
  after the 2-second `UDP_RACE_TIMEOUT`. Before #462 it did not resolve at all. The pieces are
  already here: `find_opt_record` (`magic_dns.rs:1038`) is `findOPTRecord` with the same three
  restrictions, `MAX_UPSTREAM_RESPONSE` (`:142`) is `maxResponseBytes`, and `cap_response` (`:941`)
  is the place the answer is already rewritten. The decision to make is whether to clamp both sides
  as Go does, or only the outbound query — note Go itself leaves the TCP answer alone, and the call
  is commented out at `forwarder.go:1056`, so "clamp everything" is *not* the upstream behaviour.
  Test the refusal too: a query whose OPT record is not last, or carries options (`RDLEN != 0`), or
  is a version this node will not act on, must be forwarded **unmodified**, exactly as
  `find_opt_record` already refuses to read it.

- **A `SERVFAIL` or `REFUSED` datagram wins the race here, where in Go it starts the TCP hop against
  the same resolver** (`net/dns/resolver/forwarder.go:850`, `:855` `sendUDP`; `:767`
  `rcodeResponseError`; `util/race/race.go:102`) — **needs port**, and *new at this revision from
  auditing #462*. Go's `sendUDP` does not return a soft-error answer; it returns
  `rcodeResponseError{rcode, out}`, an **error** carrying the bytes. `firstUDP` propagates it
  unchanged, and `Race.Start` closes `startFallback` on any error from the first function
  (`race.go:102`), so `thenTCP` dials the same resolver **immediately** — not after
  `udpRaceTimeout`. The resolver gets a second chance on a different transport before the query
  moves anywhere else, and only if TCP also fails do the bytes reach `forwardWithDestChan`'s
  tally as `firstErr`. Here `classify_udp` (`ts_runtime/src/magic_dns.rs:1300`) reads exactly two
  things — does the datagram match the query, and did it fit — so a matching, well-fitting `SERVFAIL`
  is `UdpOutcome::Answer`, wins the race, and `ask_upstream` returns it; `forward_walk` (`:1612`)
  then applies `is_soft_error` (`:1145`) and moves to the *next* upstream, having never asked this
  one over TCP. The two behaviours are not the same even in the single-resolver case: Go retries the
  transport, this tree does not. Note that Go does the same thing on the TCP side (`:1047`, `:1052`),
  so a resolver that refuses on both transports still ends as a refusal — the difference is only
  whether the second transport is tried. The port has a natural shape here, because the truncated
  arm already is it: score a soft-error datagram like `UdpOutcome::Truncated` — hold the bytes,
  start the TCP hop, and fall back to the held bytes if TCP brings nothing — which leaves
  `forward_walk`'s soft-error precedence untouched. `TcpRetry::Disabled` must shut this arm out too,
  as it shuts out the others. Resolver-observable (a connection Go opens and this tree does not) and
  client-observable (a name that resolves over the refusing resolver's TCP path).

- **The upstream walk asks one resolver at a time, where Go asks all of them at once**
  (`net/dns/resolver/forwarder.go:1282`–`:1305`; `:287` `resolverAndDelay`, whose `startDelay` is
  non-zero only for the DoH upgrades at `:423`–`:439`) — **needs port, or a recorded divergence**,
  and *new at this revision from auditing #462*. Upstream starts one goroutine per resolver in the
  list, with no delay between them for any resolver this fork can have, and takes the **first**
  success off `resc` (`:1314`); the error tally that begins at `:1309` acts only once `numErr`
  reaches `len(resolvers)` (`:1335`). `forward_walk` (`ts_runtime/src/magic_dns.rs:1612`) is a `for`
  loop: it awaits each upstream's whole answer before the next upstream is asked at all. Since #462
  a dead resolver costs more than it used to, not less — for a UDP client the UDP hop gets
  `UDP_RACE_TIMEOUT` alone (two seconds, `:1212`), the TCP hop then starts and runs its own
  `UPSTREAM_TIMEOUT` (five seconds, `:96`), so roughly seven seconds pass before the second resolver
  is tried. Go answers at the healthy resolver's latency. Nothing here bounds the walk as a whole —
  `serve` spawns the forward and `UPSTREAM_TIMEOUT` is per hop — so on a list of two or more dead
  resolvers the *client's* stub gives up long before this node reaches the healthy one, and the
  answer that eventually arrives is sent to a socket nobody is waiting on. **This one may well
  be right to keep**, and the reason is in the tree already: `ask_upstream`'s doc calls walking on to
  the next upstream "a second party learning a name it was never asked about", and Go's fan-out hands
  every configured resolver every query. That is a real privacy difference, and it is this fork's
  kind of decision. What is missing is not the fan-out — it is the *record*: the divergence is
  nowhere in this ledger, nowhere in `forward_walk`'s doc comment as a divergence, and the row that
  closed on `forward_walk` (below, `0b4c0f208`) describes Go's behaviour as a walk, which it is not.
  So the decision to make is explicit: port the fan-out with Go's tally rules, or keep the sequential
  walk and write the privacy argument down here and at `forward_walk`, with the latency cost stated.
  Either way the ledger stops claiming a shape upstream does not have.

- **`disable-relay-client` is ignored, so this node keeps using peer-relay paths control switched
  off** (`wgengine/magicsock/magicsock.go:3000`–`:3002`, `:2331`, `:2858`, `:3050`;
  `tailcfg/nodecap/nodecap.go`: `DisableRelayClient`) — **needs port**, and **the one row of the
  previous revision's seven that did not close.** Upstream computes
  `relayClientEnabled := self.Valid() && !self.HasCap(nodecap.DisableRelayClient) &&
  !self.HasCap(nodecap.OnlyTCP443)` once per netmap and then gates three things on it: an inbound
  `CallMeMaybeVia` from a peer is **dropped** (`:2331`, logging `ignoring %s from %v;
  disable-relay-client node attr is set`) — silently as far as the peer is concerned; the relay
  server set is **cleared** rather than recomputed (`:3050` calls
  `relayManager.handleRelayServersSet(nil)`); and `SetFilter` **returns early** (`:2858`) instead of
  re-deriving the relay server set from the new filter. `nodecap.go` documents the attribute as
  dynamically settable: adding it to a running node stops new allocations and new paths at its next
  network map, and deliberately does **not** tear down paths already in use.

  This tree has the relay client half and no gate on it. `ts_magicsock`'s relay module implements the
  `CallMeMaybeVia` → 3-way bind handshake → relayed ping/pong path
  ([`ts_magicsock/src/relay.rs`](ts_magicsock/src/relay.rs)), and the ingress arms at
  [`ts_magicsock/src/sock.rs:1384`](ts_magicsock/src/sock.rs), `:1398` and `:2379` admit a
  `CallMeMaybeVia` on `call_me_maybe_sender_allowed` (`:2026`) alone — a peer-identity check with no
  node-attribute check beside it. `disable-relay-client` appears nowhere in the workspace.

  **Re-verified at this revision, and unchanged:** `disable-relay-client` still appears nowhere in the
  workspace. **At the previous revision one thing about it changed:** its sibling did. The predicate
  upstream writes reads *both* attributes, and `only-tcp-443` is now honoured here — #448 added
  `ts_control::Node::NODE_ATTR_ONLY_TCP_443` (`ts_control/src/node.rs:800`) and the UDP-send gate
  above it. So the shared predicate this row asked the *first* of the two ports to introduce was not
  introduced: #448 ported `only-tcp-443` for its UDP-send meaning only, and its relay-client meaning
  is still unread. Whoever takes this row should write Go's predicate whole, with both attributes in
  it, and route #448's existing attribute read through it rather than adding a second, divergent one.
  `call_me_maybe_sender_allowed` is already the right chokepoint and already carries the metric for
  the refusal (`disco_call_me_maybe_via_recv_rejected`). Carry Go's two bounds exactly, because they
  are the behaviour rather than decoration: the drop is **silent** to the peer (no disco reply of any
  kind, just a log), and an existing relayed path is **not** torn down when the attribute appears —
  only new ones are refused, so a test must assert that an already-established relay path keeps
  carrying traffic.

- **`one-cgnat?v=false` disables the CGNAT collapse outright where upstream keeps the 10000 ceiling**
  (`net/routemanager/routemanager.go:148` `cgnatThreshold`, `:887`–`:891`, `:959`) — **needs port**,
  and *new at the previous revision from the audit of #438*, not from upstream. Upstream carries the decision
  into the route manager as `TailnetConfig.OneCGNAT`, a plain `bool`, and `RouteManager.cgnatThreshold()`
  returns `1` when it is set and the `cgnatThreshold` constant (`10_000`) otherwise. So the
  *disabling* attribute only declines the **forced** collapse; the ceiling still applies, and
  `wantCoarse := len(rm.cgnatPfxs) > rm.cgnatThreshold()` still installs the single `100.64.0.0/10`
  once more than 10000 distinct CGNAT peer routes exist. The merged `cgnat_threshold` maps
  `Some(false)` to `usize::MAX` — commented "unreachable so the fold never fires" — so with
  `one-cgnat?v=false` set the collapse can never happen at any peer count: a tailnet above the
  threshold programs one `/32` per peer here where a Go client programs a single `/10`. The merged
  test `host_routes_collapse_above_the_threshold_unless_control_forbids_it` pins the divergent half,
  and #438's commit body states the inverted rule as fact. This is the unbounded host route table
  #438 set out to bound, still reachable on control's say-so. Host-OS-facing. The fix is one
  expression: `Some(false)` must yield `CGNAT_THRESHOLD`, not `usize::MAX`, and the merged test must
  be re-pinned to upstream's rule rather than to the one it currently asserts.

- **The no-attribute `one-cgnat` case skips upstream's platform and CGNAT-interface gate**
  (`ipn/ipnlocal/local.go:6202`–`:6235` `shouldUseOneCGNATRoute`, `:6131`) — **needs port**, and
  *new at the previous revision from the audit of #438*. With neither `one-cgnat` attribute present, upstream
  does **not** fall through to the 10000 threshold. `shouldUseOneCGNATRoute` consults the knob first
  (an explicit `true` or `false` is terminal), then returns true for `versionOS == "plan9"`, then for
  `versionOS == "macOS"` or `"android"` probes `netmon.HasCGNATInterface()` and returns true when no
  *other* interface uses the CGNAT range — returning **false** if that probe errors — and only
  otherwise false. A true result becomes `TailnetConfig.OneCGNAT`, i.e. a threshold of 1, so the
  `/10` replaces the per-peer `/32`s from the second CGNAT route onward. The merged `Node::one_cgnat()`
  returns `None` whenever the attribute is absent and `cgnat_threshold(None)` is unconditionally
  `CGNAT_THRESHOLD`, with no platform or interface input anywhere on the path. On **macOS** — a
  platform this workspace builds and tests on — this tree therefore keeps one `/32` per peer at every
  peer count below 10000 where a Go client installs the single `/10`. Port the error direction too:
  a failed interface probe means *false*, not *true*. Read this row with the one above it; they are
  two halves of the same tri-state and are cheapest to port together, but they are independently
  testable and neither blocks the other.

- **The `disable-delta-updates` escape hatch diverts only `PeersChangedPatch`, so control still
  cannot take this node off the delta path** (`control/controlclient/map.go:278`
  `tryHandleIncrementally`, and `handleNonKeepAliveMapResponse` below it) — **needs port**, and *new
  at the previous revision from the audit of #442*. Upstream's gate is the **first statement** of
  `tryHandleIncrementally`, and a `false` return there is **not scoped to patches**:
  `handleNonKeepAliveMapResponse` falls through to `ms.netmap()` and `netmapUpdater.UpdateFullNetmap(nm)`
  for the entire response, whatever it carried — which is what the attribute's own documentation asks
  for ("treat all netmap changes as 'full' ones as tailscaled did in 1.48.x and earlier"). In the
  merged tree the check lives in `PeerTracker::apply_peer_patch_set`, which the netmap handler reaches
  only from inside `if !msg.peer_patches.is_empty()`. `PeerTracker::apply_peer_update` — which applies
  `PeerUpdate::Delta`, built in `ts_control::tokio::map_stream` from `MapResponse.PeersChanged` and
  `PeersRemoved`, this tree's **other** delta mechanism — runs unconditionally just above it and is
  untouched by the attribute. So a response carrying only `PeersChanged`/`PeersRemoved` never takes
  the full arm even with the attribute set, and one carrying both applies the delta incrementally
  *before* the patches are rebuilt as a full update. The escape hatch exists precisely for the case
  where control has decided this client's incremental path cannot be trusted; covering one of its two
  delta mechanisms does not achieve that. The gate belongs above both, at the one point the map
  response is dispatched, not inside either applier.

- **The `disable-delta-updates` full arm omits the tailnet-lock re-filter upstream runs**
  (`ipn/ipnlocal/local.go`: `tkaFilterNetmapLocked` immediately before `setNetMapLocked`;
  `tkaFilterDeltaMutsLocked`) — **needs port**, narrow, and *new at the previous revision from the audit of
  #442*. Upstream reaches the full netmap path through `tkaFilterNetmapLocked(st.NetMap)` (guarded
  only by `envknob.TKASkipSignatureCheck`), so declining the delta path also re-verifies **every**
  peer's node-key signature against the lock; the delta path gets the narrower
  `tkaFilterDeltaMutsLocked` instead, and upstream's comment says that exists precisely to match "the
  full-netmap behavior of `tkaFilterNetmapLocked`". `rebuild_netmap_with_patches` re-installs every
  retained peer through `upsert_from_control` without re-running the lock gate, so the only trust
  check on this arm is the per-patched-node one inside `apply_peer_patches`. A peer that was admitted
  earlier and whose signature has since stopped verifying is evicted by upstream on this path and
  retained here. #442's `DECISIONS` section discloses the omission and gives a defensible reason —
  this tree rewrites an expiry-flagged peer's node key via
  `ts_keys::node_public_with_bad_old_prefix`, so a whole-database re-filter would evict it and lose
  the diagnostic — which is why the audit filed this **minor** rather than as a security regression.
  It is still a divergence on the arm #442 introduced, and the disclosed reason argues for a
  *narrower* gate (exempt the expiry-flagged rewrite) rather than for no gate.

#### Read at this revision

**No port merged in the interval, so this revision audited nothing and this section is a carry.** The
audit source of change needs a merged port to read against upstream; `0c8796a..HEAD` is two
documentation commits, so there was none. What this revision read instead is the DERP frame-type
enumeration, whose results are the first three rows above, and `tailcfg.PeerChange`, which came back
clean and is written up under [What changed at this revision](#what-changed-at-this-revision).

Everything below is carried from the previous two revisions with its evidence intact, and every
verdict still holds for a reason stronger than usual: **none of the tree code any of them names changed,
because no tree code changed at all.** [`PARITY_AUDIT.json`](PARITY_AUDIT.json) audits three older
ports (#436, #438, #442); the audits of #462/#464, #440, #443, #445, #448, #456 and #459 follow. Each
verdict names the Go and the tree code it rests on, so a later revision can re-check it rather than
trust it. A section that is a carry is still worth reading before opening a row in the same
neighbourhood — that is what it is for.

- **#462, the UDP/TCP race, and #464, the mismatched-reply follow-up** — **gap: narrower than
  upstream, in three places.** See the first three rows above. What the two commits *did* port is
  faithful and is worth stating separately, because a row that narrows a port is easy to misread as a
  row that reopens it. `race_udp_and_tcp` (`ts_runtime/src/magic_dns.rs:1352`) has Go's structure:
  the UDP hop starts at once; a `ClientTransport::Udp` client gives it `UDP_RACE_TIMEOUT` alone
  (`:1212`, Go's `udpRaceTimeout`, `forwarder.go:108`) and a `ClientTransport::Tcp` client gives it
  none (Go's `timeout = 0`, `forwarder.go:740`); a failed UDP hop starts TCP at once, as
  `race.go:102` does by closing `startFallback`; a truncated one does too, as
  `truncatedResponseError` (`forwarder.go:722`) does; the first good answer wins and the loser is
  dropped. `TcpRetry::Disabled` shuts the TCP hop out of every arm — no head-start timer, no
  fallback, no truncation retry — which is what Go's `skipTCP` (`forwarder.go:677`) achieves by
  having `thenTCP` wait on the context and never dial. The anti-leak coupling the original row
  insisted on is kept: both hops ride the overlay netstack `Channel`, the TCP one via
  `channel.tcp_connect` from `0.0.0.0:0`, never a host socket. #464's fix matches Go exactly — a TCP
  message that does not echo the query is scored as a **failed** TCP hop rather than as a winner, so
  the UDP answer it beat is still relayed, which is what `send` does when it unwraps
  `truncatedResponseError` out of the joined error (`forwarder.go:751`).
- **The race's treatment of an unmatched datagram is a deliberate divergence, and it belongs on the
  list** (`net/dns/resolver/forwarder.go:785` `errTxIDMismatch`, returned at `:841`) — **not a row.**
  In Go a transaction-id mismatch on the UDP hop is an error like any other, so it closes
  `startFallback` and dials TCP. Here `UdpOutcome::Unmatched` (`magic_dns.rs:1300`) never *starts*
  the TCP hop: if the hop is already running it is still waited for, and if it is not the datagram is
  handed on for `forward_walk` to discard. The reason is stated at the call site and is sound — an
  off-path injector that can spray datagrams it could never have matched the question of would
  otherwise conscript this node into opening a TCP connection to the resolver on demand, which is a
  cheaper amplifier than the silence it replaces. Recorded here rather than opened, and added to the
  standing list of deliberate divergences, so the next revision does not re-derive the argument.

**Carried from the revision before that, with their evidence intact**, because a verdict whose
evidence is elided is a verdict the next re-derivation has to redo. Each was re-checked here only for
whether the tree code it names still exists; nothing in the interval touched any of it.

- **#440, the upstream-resolver TCP retry** — **gap: narrower than upstream.** That gap became the
  row #462 closed; see [Closed at the previous revision, kept in full](#closed-at-the-previous-revision-kept-in-full).
- **#443, the periodic STUN idle stop and `debug-always-stun`** — **faithful** on the arms it ports.
  Go's `shouldDoPeriodicReSTUNLocked` (`wgengine/magicsock/magicsock.go:3603`) stops on network-down
  or homeless, then on no peers or a zero private key, then on `idleFor > sessionActiveTimeout` unless
  `ForceBackgroundSTUN` is set. `should_do_periodic_restun` (`ts_runtime/src/direct.rs:779`) checks
  no-peers first and idle-with-override after it, in Go's order, so a peerless node stays quiet under
  the override exactly as in Go. Its doc comment records the arms it does not port and why (a zero
  key cannot occur while the prober runs; network-down and homeless wait on a `ts_netmon` OS
  backend), which is where the previous revision's closure already left them.
- **#445, `silent-disco`** — **faithful.** Go's attribute sets `heartbeatDisabled`
  (`wgengine/magicsock/endpoint.go:1701`), which keeps the heartbeat timer from starting (`:1016`),
  returns early from `heartbeat` (`:879`), and — the compensating arm — re-extends
  `trustBestAddrUntil` when data arrives on the best address (`:591`). All three are here:
  `PeerPaths::set_heartbeat_disabled` (`ts_magicsock/src/path.rs:276`), the inbound re-trust
  (`path.rs:303`), and the heartbeat exemption that drops out under it (`path.rs:526`).
- **#448, `only-tcp-443`** — **faithful** for the meaning it claims. Go reads the attribute inside
  magicsock in three places that matter here: `sendUDPStd` returns without sending
  (`wgengine/magicsock/magicsock.go:1622`), `sendUDPNetcheck` refuses with `ErrUnsupported`
  (`:1606`), and netcheck is told `OnlyTCP443` (`:1038`), which skips the STUN probe plan and the ICMP
  probes and keeps the HTTPS latency arm (`net/netcheck/netcheck.go:940`, `:1008`). Here `send_udp`
  returns without sending (`ts_magicsock/src/sock.rs:372`), `send_stun_request` refuses with
  `Error::OnlyTcp443` (`sock.rs:2148`), and the STUN sweep skips its round (`ts_runtime/src/direct.rs:853`);
  `ts_netcheck` has only an HTTPS arm, so its report is already Go's `OnlyTCP443` report. Two more Go
  reads have no target in this tree — the port mapper (`magicsock.go:686`) and the skip of the
  rebind-on-send-error path (`:921`). The last, inside `relayClientEnabled` (`:3002`), is the
  `disable-relay-client` row above, unchanged.
- **#456, method-aware c2n dispatch** — **faithful, with a scoping decision it records.** Go's
  `handleC2N` (`ipn/ipnlocal/c2n.go:133`) tries method and path, then path alone, then the prefix
  handlers, and refuses with `405 bad method` for a path it serves under another method and
  `400 unknown c2n path` otherwise. #456 ports that order, and scopes the `405` set to *this node's*
  routes: `POST /update` answers `400` here, where a Go build carrying `feature/clientupdate` answers
  `405`. That is what a Go build without the feature answers too, and a `405` would claim a feature
  this node lacks. Recorded, not opened.
- **#459, VIP-service client actions** — **faithful** on the decode half, which is all it claims;
  the *Services model extension* entry below records why having no consumer here is correct.

#### Closed at this revision

**Nothing closed at this revision, and the reason is structural rather than a lull.** A §B row closes
when this tree gains the behaviour the row describes, and this tree gained no behaviour: the interval's
two commits are this document and a correction to its backlog file. Recording the empty close is not
bookkeeping for its own sake — across this ledger's history the close counts run 3, 3, 6, 4, 6, 0, 1
and now 0, and a revision that closes nothing while opening three is the normal shape of a document
whose pin has caught up with upstream. The previous revision's close is kept below in full, because
its three successor rows are still open and a reader who finds one of them wants the closure they came
out of.

#### Closed at the previous revision, kept in full

- **The upstream DNS hop falls back to TCP only on truncation, where Go races TCP against a slow or
  failed UDP hop** — **closed by #462** (`2179cf4`, "race the upstream TCP hop against a slow or
  failed UDP hop") **and #464** (`25da017`, "a mismatched TCP reply must not sink the UDP answer").
  `race_udp_and_tcp` (`ts_runtime/src/magic_dns.rs:1352`) is the race, `UDP_RACE_TIMEOUT` (`:1212`)
  is Go's `udpRaceTimeout`, `classify_udp` (`:1300`) is `firstUDP`'s classification, and
  `ClientTransport` decides the head start as Go's `isUDPQuery` does. **Not closed cleanly:** three
  successor rows above — the EDNS clamp, the soft-error arm of the race, and the sequential walk the
  race sits inside. The one behaviour of Go's that the port deliberately declines is the third of the
  three the row named: Go does not re-ask a *UDP client's* truncated answer over TCP, and this tree
  does, because two of its UDP-labelled clients cannot make that retry themselves (the application
  netstack serves `100.100.100.100:53` over UDP only, and `query_dns` is a programmatic caller with
  no TCP path at all). That reason is written at `magic_dns.rs:1245`, it is the shape the row asked
  for — "port the fallback and record the UDP-client skip as a deliberate divergence with its reason"
  — and it is accepted here rather than carried as a row.
- **This is the first row this ledger has opened and closed in consecutive revisions**, and the
  lesson is in the three successor rows rather than in the speed. The row was written from a reading
  of `forwarder.go`, the port was written from the row, and the port is exactly as wide as the row
  was. Three behaviours that the row did not name are therefore still missing. **A row is a summary,
  and a port scoped to a summary inherits the summary's blind spots** — which is the argument for
  auditing a closure against upstream even when, especially when, the closure looks clean.

#### Closed at earlier revisions

Six rows opened two revisions before the previous one were ported before it was written. Each names the tree code that now
covers it, because a closed row still needs evidence: the next revision must be able to re-check the
claim without re-deriving the document, and a closed row reopens if the code it names is refactored
away. Two of the six closed *incompletely* and have successor rows above; that is recorded here too,
because "closed" and "closed cleanly" are not the same claim.

- **`only-tcp-443`** — **closed by #448** (`743aea8`, "stop sending UDP when control sets
  only-tcp-443"). `ts_control::Node::NODE_ATTR_ONLY_TCP_443` (`ts_control/src/node.rs:800`) reads the
  attribute off the self node, and `ts_runtime::direct` gates the UDP send path on it
  (`ts_runtime/src/direct.rs:2257`, `:2272`, `:2293` exercise both directions, including clearing the
  attribute without a restart). **Not closed cleanly:** the attribute's *other* upstream meaning —
  it is half of Go's `relayClientEnabled` predicate — is still unread here. See the
  `disable-relay-client` row above.
- **`silent-disco`** — **closed by #445** (`ee7c674`, "stop pinging peers control set silent-disco
  for"). `ts_control::Node::NODE_ATTR_SILENT_DISCO` (`node.rs:770`) plus the suppression and its
  compensating arm in `ts_runtime::peer_tracker` (`peer_tracker/mod.rs:3363`). The compensating half
  — extending best-path trust on inbound data when the heartbeat is suppressed — was the part this
  row warned was not optional, and it landed with it.
- **The periodic STUN sweep's idle stop condition, and `debug-always-stun`** — **closed by #443**
  (`1632f07`, "stop the periodic STUN sweep when the datapath goes idle").
  `ts_control::Node::NODE_ATTR_DEBUG_FORCE_BACKGROUND_STUN` (`node.rs:674`) is the override, and
  `ts_runtime/src/direct.rs:2296`/`:2462` exercise the idle arm against it. The two arms this row
  explicitly excluded — network-down and homeless — remain out of scope pending a `ts_netmon` OS
  backend, and are to be cut as their own row when that backend lands.
- **`disable-delta-updates`** — **closed by #442** (`cb2b2c0`, "let control switch this node off the
  delta netmap path"). `ts_control::Node::NODE_ATTR_DISABLE_DELTA_UPDATES` (`node.rs:744`) and the
  gate in `ts_runtime::peer_tracker` (`peer_tracker/mod.rs:3138`). **Not closed cleanly:** two
  successor rows above — the gate covers only one of this tree's two delta mechanisms, and the full
  arm omits the tailnet-lock re-filter.
- **The upstream-resolver TCP retry, and `dns-forwarder-disable-tcp-retries`** — **closed by #440**
  (`1e3687a`, "resolve names whose answer does not fit a UDP datagram").
  `ts_control::Node`'s attribute constant (`node.rs:651`) is the off switch, and the retry itself is
  in `ts_runtime::magic_dns` (`magic_dns.rs:3724` exercises it). The anti-leak coupling this row
  insisted on — the TCP hop rides the same overlay netstack channel as the UDP hop, never a host
  socket — was kept.
- **The host route set, and `one-cgnat`** — **closed by #438** (`c133b98`, "bound the host route
  table by collapsing CGNAT peer routes"). `ts_control::Node::NODE_ATTR_ONE_CGNAT_ENABLE` /
  `_DISABLE` (`node.rs:698`) and the collapse in `ts_runtime::tun_actor` (`tun_actor.rs:1915`). The
  collapse mechanics are right: `10000` as the constant, a strict `>` comparison, single-IP-in-CGNAT
  as the collapsible class, the `/10` replacing the `/32`s it covers, and the self and MagicDNS
  `/32`s kept distinct. **Not closed cleanly:** two successor rows above — the `?v=false` arm is
  inverted and the no-attribute arm skips upstream's platform gate.

#### Not applicable, from the commits in `023255e8a..e2ed43239`

No commit is new at this pin, so this list is the previous revision's, carried unchanged and
re-checked: the seven commits in `023255e8a..e2ed43239`. One of them is §A row 147; the rest are here so the next
revision does not re-derive them.

- **`ipn/ipnlocal`: exceptions to the 4via6 target filter** (`f31cfaec3`) — **not applicable.**
  Upstream added `TS_4VIA6_ALLOW_LOCAL`, an envknob of IPv4 ranges/prefixes/addresses that
  `viaTargetAllowed` consults *before* its loopback / multicast / unspecified / broadcast / CGNAT /
  link-local refusals, so a deployer can deliberately expose host-scoped addresses through a 4via6
  subnet router. This tree has no 4via6 data path to guard: 4via6 appears only in doc comments
  (`ts_control/src/map_request_builder.rs:223`, `ts_control/src/config.rs:468`, `src/config.rs:302`)
  recording that the app-connector data path — control-pushed domain routes, the 4via6 domain→route
  mapping, the per-domain DNS — is deliberately out of scope, which is the same reason §A row 143 is
  not applicable. There is no `viaTargetAllowed` here to add exceptions to. **If 4via6 is ever
  ported, port `viaTargetAllowed` with it and this envknob after it** — the guard is the sole check
  on the embedded IPv4 target, because the packet filter only ever sees the outer via address.
- **`control/controlclient`: `mapSession` no longer runs in its own goroutine** (`4b60ec876`) —
  **not applicable**, and worth one sentence of *why* because it touches a swept core package. The
  commit reverts plumbing added so that disco keys could flow into `mapSession` for TSMP-learned-key
  filtering; upstream moved that learning into the userspace engine and introduced two active disco
  keys per endpoint, so the extra goroutine and its separate entry path are no longer needed. It
  changes Go's internal concurrency structure and nothing a peer or control plane observes. This
  tree never had the structure being reverted — `ts_control`'s map poll is a stream consumed by an
  actor — and the disco-key behaviour the revert refers to is already carried by the TSMP disco-key
  rows below. Its only effect on this ledger is line numbers: it moved the `DisableDeltaUpdates`
  gate in `map.go` from `:349` to `:278`.
- **`ipn/localapi`: require local admin for `dev-set-state-store`** (`c9f5d175b`) — **not
  applicable.** A privilege check on a LocalAPI debug handler that could otherwise write state keys
  (for example `_serve/<profile-id>`) while bypassing the per-handler admin check `serve-config`
  performs. This fork's LocalAPI serves one route and has no `dev-set-state-store` handler, so there
  is no bypass to close. Recorded rather than skipped because it is a security fix in a swept
  package: if a LocalAPI debug surface is ever added here, it inherits this requirement.
- **`go.mod`: bump `wireguard-go` for a small-packet buffer pool** (`8054aa257`) — **not
  applicable**, and it is the mapping row that has no upstream path to sweep.
  `golang.zx2c4.com/wireguard`'s device is an upstream *dependency* that `ts_tunnel` re-implements,
  so this is tracked through `go.mod` bumps rather than through the loop — which is exactly how it
  surfaced. The change reduces packet slab memory retained in per-peer staged queues. `ts_tunnel`
  does not stage per-peer packet queues in the shape being fixed, so there is no equivalent
  retention here. Adjacent but distinct from this repository's own memory trade-off, which is
  `Config::tcp_buffer_size` in the netstacks — see [`AGENTS.md`](AGENTS.md); do not conflate them.
- **`cmd/cigocacher`: stop logging the default cache dir** (`91bd4f99d`) and **`go.toolchain.rev`
  bump** (`e2ed43239`) — **not applicable.** Upstream CI tooling and toolchain pinning; neither is a
  client behaviour and `cmd/` is out of scope.

#### Not applicable, from packages first swept at the previous revision

- **`sessionrecording`'s seven commits in the window** — **not applicable**, all seven, and recorded
  here so the next revision does not re-derive them. The package entered the loop at the **previous**
  revision, the fifth and most embarrassing time the sweep list was found short (see
  [Re-deriving this ledger](#re-deriving-this-ledger) for why it was missing). Re-run at this pin the
  loop returns the same seven — upstream touched the package none of the seven times in the interval
  — so these assessments carry unchanged. `08eae9aff`
  (2025-10-10) adds `Event.Destination` and the `Destination` struct to `sessionrecording/event.go`:
  that is the **Kubernetes API-server proxy's** audit-event stream, not the SSH cast stream, and this
  fork has no API-server proxy. `cd2a3425c` (2025-10-08) introduced that same event surface and is
  out for the same reason. `899625464` (2025-10-29) fixes a Go-internal transport slip —
  `DialTLSContext` where `DialContext` was meant, so the passed-in dialer was being ignored in favour
  of `net.Dialer`; this fork dials the recorder over the tailnet explicitly and has no
  `http.Transport` to mis-wire. `53ef7f92c` (2026-06-19) calls `hc.CloseIdleConnections()` after the
  upload and sets a 30 s `IdleConnTimeout` on both recorder clients, to stop the SSH server
  exhausting ports by pooling idle connections to the recorder; this fork's recorder client
  ([`src/ssh/recording.rs`](src/ssh/recording.rs)) holds no connection pool to leak — the upload owns
  its connection and closes it — so there is no idle connection to reap. The remaining three
  (`7f3bbc986`, `5ef3713c9`, `3ec5be3f5`) are a `net/netutil` helper, a `cmd/vet` analyzer and the
  `AUTHORS` file removal, and touch this package only incidentally.
  The one thing worth re-checking at every future revision is **not** in those commits: `CastHeader`
  in `sessionrecording/header.go` is the wire format this fork writes to a real tsrecorder, it is
  unchanged in the window, and it is the reason the package needs to be in the loop at all. A field
  added to it is a field a recorder will expect.

- **A wrapped (tailnet-lock) auth key is sent to control verbatim, and no node-key signature is ever
  sent** (`tka/sig.go`: `DecodeWrappedAuthkey`, `SignByCredential`; `control/controlclient/direct.go`:
  the `authKey, isWrapped, wrappedSig, wrappedKey := tka.DecodeWrappedAuthkey(c.authKey, c.logf)`
  line in `doLogin`, and `RegisterRequest.NodeKeySignature`) — **needs port**, and it is the row with
  the sharpest failure of the eight: on a tailnet whose lock is on, there is no way for this node to
  arrive signed.
  Upstream's pre-auth keys come in two forms. A plain key is `tskey-auth-…`. A key issued for a
  tailnet whose lock is on is *wrapped*: the same key, then the literal separator `--TL`, then a
  base64 (raw-std, unpadded) `SigCredential` node-key signature, a `-`, and a base64 ed25519 private
  key whose signing authority that credential delegates. `DecodeWrappedAuthkey` cuts the string at
  `--TL` and returns the two halves; every decode failure inside it returns the *whole* string with
  `isWrapped` false, so a malformed suffix degrades to "this is a plain key" rather than to an error.
  `doLogin` then does two things with the result that this tree does neither of: it registers with
  the **stripped** key — the part before `--TL`, which is what control is expecting to see — and,
  when the key was wrapped, it calls `tka.SignByCredential(wrappedKey, wrappedSig, tryingNewKey.Public())`
  to build a `SigRotation` whose `Nested` is the credential and whose signature is made by the
  delegated private key, and attaches it as `RegisterRequest.NodeKeySignature`. That signature is
  what makes the new node key trusted under the lock from its first contact, with no human running
  `tailscale lock sign`.
  Here, the auth key reaches the wire untouched: `ts_control::tokio::register` takes
  `auth_key: Option<&str>` and sets `auth: auth_key.map(RegisterAuth::from)`, and `RegisterAuth`
  ([`ts_control_serde/src/register.rs`](ts_control_serde/src/register.rs)) is a one-field borrow of
  that same `&str`. The literal `--TL` appears nowhere in the tree. `RegisterRequest.node_key_signature`
  is modelled — with Go's own comment on it — and is never set by any caller. So two things go wrong
  at once for an embedder handed a wrapped key: the auth key sent to control is not the auth key
  control issued, and even if control tolerated the suffix the node would register unsigned, which on
  a locked tailnet is a node its peers will refuse. The peer-side half of the lock is fully
  implemented here — `ts_tka`'s `verify_signature` accepts `Credential`-rooted chains and skips the
  `pubkey == node_key` bind for a `Credential` leaf, because `SigCredential` leaves `Pubkey` unused
  and binding it rejects legitimate credential-provisioned peers — so what is missing is specifically
  the *self*-signing half, and `ts_tka` already has the pieces:
  `NodeKeySignature::sign_rotation` builds a rotation wrap, and `ts_runtime`'s `tka_sign` already
  signs and submits a `Direct` signature over a live Noise channel. What is absent is the decode of
  the `--TL` suffix and a rotation wrap whose nested signature is a *supplied* credential rather than
  an inner `Direct` this tree constructs.
  Bring Go's refusals with it, because they are the whole safety of the feature: a key with no `--TL`
  is a plain key; a suffix with no `-` delimiter, a suffix half that is not valid unpadded raw-std
  base64, or a signature blob that does not deserialize all degrade to "plain key" and register
  unwrapped rather than failing; and `SignByCredential` refuses outright when the wrapped signature is
  not of kind `SigCredential` (`wrapped signature must be a credential, got %v`), which is the usage
  refusal to port rather than to infer.

- **The register response's two instructions — re-sign this node key, and regenerate the expired one
  — are both ignored** (`control/controlclient/direct.go`: `doLoginOrRegen`, the
  `if len(resp.NodeKeySignature) > 0` and `if resp.NodeKeyExpired` arms of `doLogin`, and the
  `tka.ResignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature)` call;
  `tka/sig.go`: `ResignNKS`, `maybeTrimRotationSignatureChain`) — **needs port**, and it is the row
  most likely to present as a node that simply stops working one day rather than as a node that never
  worked.
  Upstream's registration is a two-shot loop, and the shape is `doLoginOrRegen`: call `doLogin`; if it
  comes back with `mustRegen`, set `opt.Regen` and `opt.OldNodeKeySignature` from what the first call
  returned and call it exactly once more. Two things in the response set `mustRegen`. **`NodeKeySignature`
  non-empty** means "here is the signature your *old* node key had; re-sign it for the new one" —
  `doLogin` returns it, and on the second pass `tka.ResignNKS` unserializes it, returns it verbatim if
  it already covers the key being registered, otherwise wraps it in a fresh `SigRotation` signed by
  this node's network-lock private key, and the result goes out as `RegisterRequest.NodeKeySignature`.
  `maybeTrimRotationSignatureChain` is the bound that makes the wrap safe to repeat: a rotation chain
  may name at most 15 previous node keys, because CBOR nesting is capped at 16, and past that the
  chain is rebuilt from the original `Direct` leaf forward. **`NodeKeyExpired`** means the key just
  offered is expired; Go logs it without PII and returns `mustRegen` so the second pass generates a
  fresh node key (moving the current one to `OldPrivateNodeKey`), and it treats `regen=true` *with*
  `NodeKeyExpired` as a contradiction and errors — `weird: regen=true but server says NodeKeyExpired`.
  Here, `classify_register_response` ([`ts_control/src/tokio/register.rs`](ts_control/src/tokio/register.rs))
  reads exactly three things: `machine_authorized`, `auth_url` and `error`. `node_key_signature` and
  `node_key_expired` are both modelled on `RegisterResponse` and are read by nothing in the tree, so
  each of control's two instructions lands as an unauthorized registration with no auth URL —
  `MachineNotAuthorized(None)` — and the caller retries the same key with the same missing signature.
  The node-key rotation machinery it would need is already here: `ts_keys::NodeState` carries
  `old_node_key` and the register path already logs `re-registering with OldNodeKey set`, and
  `network_lock_keys` is persisted and its public half already goes out as `RegisterRequest.nl_key` —
  which is precisely the declaration on the strength of which control sends the signature back.
  The port is the loop, not the crypto: classify the two fields into the existing typed error set,
  re-sign or regenerate, and retry **once**. Go's single retry is the behaviour to copy, not an
  unbounded loop. The refusals to carry: the signature is used verbatim when it already covers the
  key being registered rather than being needlessly re-wrapped; a chain at the 15-key limit is
  trimmed rather than extended; a re-sign failure is logged and registration proceeds *without* a
  signature (Go's `ResignNKS` error path does not abort the register); and `regen` together with
  `NodeKeyExpired` is an error, not a third attempt.

- **MagicDNS `AAAA` answers are gated on a local flag, not on the `magicdns-aaaa` node capability**
  (`ipn/ipnlocal/node_backend.go`: `magicDNSAddrs`, `magicDNSHostAddrs`, and the
  `nm.AllCaps.Contains(nodecap.MagicDNSPeerAAAA)` test in `dnsConfigForNetmap`;
  `tailcfg/nodecap/nodecap.go`: `MagicDNSPeerAAAA`; capability version 116) — **needs port**, and it
  is the one row of the eight that diverges in *both* directions: this node can answer `AAAA` where Go
  would not, and refuse it where Go would answer.
  Upstream decides a peer's MagicDNS addresses in one function, `magicDNSAddrs`, from the peer's
  addresses plus two flags. The default rule is deliberately conservative and is commented as such:
  if the peer has an IPv4 address at all, its IPv6 addresses are dropped from the answer, "as we
  don't guarantee that the peer node actually can speak IPv6 correctly". Two things lift it.
  `selfV6Only` — *this* node has v6 addresses and no v4 — returns only the peer's v6, because there
  is nothing else this node could reach. And `wantAAAA`, set when the **self node** holds the
  `magicdns-aaaa` capability, keeps the peer's v6 alongside its v4. Capability version 116
  (2025-05-05, "Client serves MagicDNS `AAAA` if `NodeAttrMagicDNSPeerAAAA` set on self node") is the
  declaration that the client will do this, and it sits well below the 125 this node declares, so
  control may set the attribute and expect `AAAA` to start being served.
  Here the whole decision is one embedder-set boolean. `ts_runtime::magic_dns` answers an `AAAA`
  question for a tailnet name from `DnsView::enable_ipv6`, which is `ts_control::Config::enable_ipv6`
  carried through `ts_runtime::env`: with it off (the default) a known peer's `AAAA` returns `NoError`
  with an empty answer, and with it on the peer's overlay v6 address is returned. Control's
  capability map is not consulted; the string `magicdns-aaaa` appears nowhere in the tree. The two
  divergences fall straight out. With `enable_ipv6` on and the attribute *unset*, this node hands out
  a peer's `AAAA` beside its `A` — which is what Go does only when control asked for it, and Go's
  comment says why it otherwise does not. With `enable_ipv6` off and the attribute *set*, control has
  asked for `AAAA` and gets `NODATA`.
  The port is to make the answer the conjunction Go makes it: keep the local gate, because it is this
  fork's own anti-leak decision and is load-bearing (with IPv6 off the node opens no IPv6 socket, so
  an `AAAA` it hands out is an address it cannot then reach), and add the capability as the second
  condition, with Go's default suppression — drop a peer's v6 when that peer also has a v4 — applying
  when the capability is absent. The self-v6-only case can be assessed and recorded rather than
  implemented: a node with only v6 addresses is not a configuration this fork produces today. Assert
  the negatives: a peer with only a v6 address is answered under the *current* rules already and must
  not regress; an `AAAA` for a tailnet name that resolves to nothing is still `NXDOMAIN` and is never
  forwarded upstream (the existing anti-leak test); and the capability is read from the **self** node,
  not from the peer being resolved.

- **The DERP home region is chosen without control's per-region scores**
  (`net/netcheck/netcheck.go`: `addReportHistoryAndSetPreferredDERP`; `tailcfg/derpmap.go`:
  `DERPHomeParams.RegionScore`) — **needs port**, and it is narrow, cheap, and the clearest example in
  this ledger of a gap that only a top-to-bottom read of an already-ported function can find.
  Upstream's home-DERP choice is latency plus history plus stickiness, and this tree ports all three
  faithfully — `ts_runtime::control_runner::select_home_region` reproduces Go's asymmetric comparison
  (the candidate by smoothed latency, the incumbent by this cycle's raw latency), its 10 ms absolute
  threshold, and even its integer `oldRegionCurLatency/3*2` in preference to the float form, with the
  divergence at the exact two-thirds boundary written out in the comment. What it does not do is the
  step Go takes *first*. Before any comparison, Go fetches `dm.HomeParams().RegionScore()` and scales
  every region's latency by its score — both the smoothed `bestRecent` map and, separately, each
  region's latency in the current report, because the reports themselves are deliberately not mutated
  in case the scores change. A score in `(0, 1)` makes a region proportionally more preferred, a score
  above 1 penalises it, an absent region is 1.0, and a zero or negative score is ignored — Go's
  `if score := scores.Get(regionID); score > 0`. A nil map means "no change from the previous value";
  an empty non-nil map resets every score to 1.0.
  Here, `DerpMap::home_params` and `HomeParams::region_score`
  ([`ts_control_serde/src/derp_map.rs`](ts_control_serde/src/derp_map.rs)) are modelled and are read
  by nothing: `ts_netcheck::RegionResult` carries an id, a latency, a latency-map key and the
  connected remote, and `select_home_region` sees only that. So control's only lever for steering a
  client off a region it is deliberately draining does nothing to this node, which keeps choosing on
  raw latency and reporting the result in `NetInfo.PreferredDERP` — the field control reads back to
  find out where to route the node's traffic.
  The port is to scale inside the existing pure function so it stays unit-testable: pass the score map
  in, apply it to both the smoothed and the raw sides exactly where Go applies it, and leave the
  hysteresis arithmetic untouched. The cases to pin are Go's guards, not the happy path: a score of 0
  or below is ignored rather than treated as "infinitely preferred", a region absent from the map is
  unscaled, an empty map is a reset and a nil map is not, and a score high enough to move the choice
  must still pass the same stickiness thresholds — scoring changes the inputs to the comparison, never
  the comparison.

- **Only `c2n` ping requests are answered, and a repeated one is answered again**
  (`control/controlclient/direct.go`: `answerPing`, `answerHeadPing`, `doPingerPing`,
  `isUniquePingRequest`, and the `if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr)`
  guard in the map-response loop; `tailcfg`: `PingRequest.URLIsNoise`, capability version 38) —
  **needs port**, and the cheap half of it is the half worth doing first.
  Upstream's `PingRequest` is control asking the node to do something and report back by hitting
  `pr.URL`, and `answerPing` has four arms. `Types == ""` is a plain **HEAD ping**: `answerHeadPing`
  issues an HTTP `HEAD` to `pr.URL` under a 15-second timeout and logs the round-trip; it is how
  control measures that a node is awake and can reach a URL. `Types == "c2n"` is the control-to-node
  HTTP request this tree implements. Anything else is a comma-separated list of `PingType`s —
  `disco`, `TSMP`, `ICMP`, `peerapi` — each dispatched to `doPingerPing`, with an explicit
  `unsupported ping request type: %q` for a name outside that set. Two rules sit around the switch.
  `useNoise := pr.URLIsNoise || pr.Types == "c2n"` chooses the transport, and a `c2n` request that
  did *not* come out as Noise is **refused** rather than answered — `refusing to answer c2n ping
  without noise` — unless a debug envknob says otherwise. And the request is only dispatched at all
  when `isUniquePingRequest` says its `URL` differs from the last one seen: a `PingRequest` with an
  empty URL is bogus and dropped, and a repeat of the previous URL is dropped too, so a re-sent or
  replayed map response does not produce a second answer.
  `ts_control::handle_ping` ([`ts_control/src/tokio/ping.rs`](ts_control/src/tokio/ping.rs)) walks
  `ping_request.types` and `continue`s past everything that is not `PingType::C2N` with
  `ignoring unsupported ping type`. An empty `types` — Go's HEAD ping — is not an unsupported type but
  an empty loop, so it is dropped with no log at all, which is the least debuggable of the two
  outcomes. There is no dedup: every map response carrying a `PingRequest` is answered, however many
  times control sends the same one. `url_is_noise` is modelled on the request and read by nothing,
  which is harmless *today* only because the one arm implemented is the one Go forces onto Noise
  regardless. And the response is posted to `control_url.join(ping_request.url.path())` — the path
  only, rebased onto the control URL — where Go posts to `pr.URL` as given; that is a deliberate-looking
  narrowing this fork should keep, but it currently keeps it by accident and drops the query string
  with it, so it belongs in a comment either way.
  The port splits cleanly. The HEAD arm and the dedup are small, self-contained and need no new
  subsystem. The `doPingerPing` arms need a pinger this `Device` does not expose — and the `TSMP` one
  is the send half of the *A TSMP ping gets no pong* row below, which is the reason to read the two
  rows together and to decide them in that order. Carry the refusals: an empty `pr.URL` is bogus and
  answered with nothing, a repeated URL is dropped, an unknown type name is logged and skipped rather
  than failing the whole request, and a `c2n` request is answered only over Noise.

- **The map request never carries `TKAHead`, so control is told this node has no lock state**
  (`control/controlclient/direct.go`: `SetTKAHead` and the `TKAHead: tkaHead` field of the built
  `MapRequest`; `control/controlclient/auto.go`: `Auto.SetTKAHead`) — **needs port**, narrow, and
  recorded with its own bound: nothing in this tree's own tailnet-lock sync depends on it.
  Upstream keeps the head hash of the local tailnet-lock authority on the `Direct` client and stamps
  it into **every** map request. `SetTKAHead` reports whether the value actually changed, and `Auto`
  uses that to force a fresh map request when it does, so control learns of a local head advance
  promptly rather than on the next unrelated update. It is the mirror of `MapResponse`'s `TKAInfo`:
  control says where it thinks the chain is, the client says where it actually is.
  Here, `MapRequest::tka_head` ([`ts_control_serde/src/netmap.rs`](ts_control_serde/src/netmap.rs))
  is modelled as `&'a str` with Go's own description, and `MapRequestBuilder`
  ([`ts_control/src/map_request_builder.rs`](ts_control/src/map_request_builder.rs)) has no setter for
  it, so it takes its `Default` and every map request this node has ever sent carries an empty head.
  The bound worth stating, so the row is not read as worse than it is: this tree's sync is driven
  entirely from the *response* side — `ts_runtime::control_runner::maybe_sync_tka` compares control's
  `TKAInfo.Head` with the local `Authority`'s head and syncs when they differ, which is Go's
  `tkaSyncIfNeeded` — so an empty request-side head costs correctness nothing here. What it costs is
  honesty on the wire: a control plane that reads `TKAHead` to decide what to send, or to notice a
  node whose chain has forked or fallen behind, sees a node that is permanently at genesis, which is
  exactly the "no peer or control plane can distinguish this from a Go client" line this repository
  is measured against. The port is a setter on the builder fed from the synced authority's head in
  its base32 form (`Authority::head` → `AumHash::to_base32`, both already used by the sync path),
  plus Go's changed-or-not return so a head advance triggers a map request instead of waiting for one.
  The negative cases: no synced authority means the empty string, not a zero-hash; and an unchanged
  head must not force a request, or a node with a lock sends map requests in a loop.

- **ACL rules whose source is a capability (`cap:…`) never match**
  (`wgengine/filter/filtertype/filtertype.go`: `Match.SrcCaps`; `wgengine/filter/filter.go`:
  `f.srcIPHasCap`; `ipn/ipnlocal/local.go`: `srcIPHasCapForFilter`; capability versions 100 and 109)
  — **needs port**, and it is the cheapest of the previous revision's five rows to close, because
  every layer but the caller is already right.
  Upstream lets a policy name its source by *capability* instead of by prefix. `tailcfg.FilterRule`
  carries such a source as the string `cap:<name>`; `MatchesFromFilterRules` splits it out into
  `Match.SrcCaps`; and `runIn4`/`runIn6` pass `f.srcIPHasCap` into every `matches4.match` and
  `matchIPsOnly` call so a packet whose source peer holds the named capability is accepted even
  though no `Srcs` prefix covers it. The test function is `LocalBackend.srcIPHasCapForFilter`, and
  note the refusal it carries: `return !n.UnsignedPeerAPIOnly() && n.HasCap(cap)` — an unsigned peer
  is denied every capability, which is the same rule as the *Peer capabilities are not withheld from
  `UnsignedPeerAPIOnly` peers* row below and must travel with this port rather than after it.
  Upstream dates this precisely: capver 100 (2024-06-18) is "initial support", capver **109**
  (2024-11-18) is "client supports `filtertype.Match.SrcCaps`", and this node declares **125**. So
  control is entitled to compile a `cap:`-sourced rule for us and to expect it honoured.
  Everything here is ported except the last step. `ts_packetfilter_serde`'s `SrcIp` parses the
  `cap:` form into `SrcIp::NodeCap` (its own doc says "the type name is a misnomer: the source may
  not be an IP at all"); `ts_packetfilter::rule` matches a rule's capability sources against a
  supplied set; and the `Filter` trait threads a `CapIter` through `match_for`/`matches`/`can_access`
  from top to bottom. Then both call sites hand it nothing.
  `ts_dataplane::inbound_filter_verdict` reads `// TODO(npry): wire in nodecaps` and then
  `let caps = [];`, and `ts_runtime::peerapi_doh::dns_source_allowed` — the ACL arm of the peerAPI
  DNS gate #423 landed — does the same and says why in place. So a `cap:`-sourced rule is decoded,
  compiled, and can never match: this node drops a packet a Go client accepts, and refuses a peerAPI
  DNS query a Go client answers. It fails closed, which is why it has cost nothing visible; it is
  still a divergence a control plane can trigger at will.
  The port is the capability test itself: resolve the source address to a peer
  (`PeerTracker::peer_by_tailnet_ip`, which the DoH gate already calls) and pass that peer's node
  capabilities into `can_access` at both sites. The cases to pin are the ones that keep it from
  widening the ACL: a source that resolves to no peer supplies no capabilities and is still judged
  on prefixes alone; an `UnsignedPeerAPIOnly` peer supplies none either, per Go's own line; the
  empty capability name matches nothing (Go guards `cap == ""` explicitly); and a rule with *both*
  `Srcs` and `SrcCaps` still accepts on the prefix when the capability is absent. Both call sites
  move together or the two answers disagree — the dataplane would admit a packet the DoH gate
  refuses, from the same peer, under the same policy.

- **A jailed peer gets the ordinary ACL instead of the shields-up filter**
  (`net/routemanager/routemanager.go`: `PeerRoute.Jailed`; `net/tstun/wrap.go`:
  `inboundPacketIsJailed`/`outboundPacketIsJailed`; `ipn/ipnlocal/local.go`: `SetJailedFilter`;
  capability version 81) — **needs port**, and it is the one of the previous revision's rows where
  the missing behaviour
  removes a restriction rather than adding one.
  Control marks a peer `IsJailed` when that peer "should not be allowed to initiate connections" to
  us — upstream's canonical case is a Mullvad-style exit node, a peer this node dials out through
  and which has no business dialling back in. Upstream implements it as a *second, separate filter*.
  `LocalBackend.updateFilterLocked` builds it unconditionally, on every netmap, as
  `filter.NewShieldsUpFilter(localNets, logNets, oldJailedFilter, b.logf)` — a filter with no rules
  at all, sharing flow state with its predecessor — and installs it with `e.SetJailedFilter`. Then
  `tstun.Wrapper` *chooses* between the two per packet: `filterPacketInboundFromWireGuard` uses the
  jailed filter when `pc.inboundPacketIsJailed(p)`, and `filterPacketOutboundToWireGuard` uses it
  when `pc.outboundPacketIsJailed(p)`. Because a shields-up filter has no rules, nothing a jailed
  peer *initiates* is admitted, while the reply-admission carve-outs (TCP non-SYN, ICMP responses,
  the UDP/SCTP reverse-flow cache) still run against the jailed filter's own state — so a connection
  *we* opened to that peer keeps working in both directions. That is the whole point: one-way, not
  no-way. Capver 81 (2024-05-06) declares the client understands `Node.IsJailed`, and this node
  declares 125.
  Here, `is_jailed` is decoded and dropped. `ts_control_serde::Node::is_jailed` exists with Go's own
  doc comment on it; the string appears nowhere else in the tree; `ts_control::Node` does not carry
  it; and `ts_dataplane::filter_inbound_from_peer` takes exactly one `filter` for every peer in the
  batch. So a jailed exit node can open a TCP connection to any port this node's ACL happens to
  leave open — which, for a node that has *selected* that peer as its exit node, is precisely the
  peer with the most reach and the least reason to have it.
  The mechanism is already here, which is what makes the port tractable: `ts_packetfilter`'s
  `ShieldsUpFilter` (`ts_runtime::packetfilter`) is this fork's `NewShieldsUpFilter`, built today
  from the *local* `Env::block_incoming` knob rather than per peer. The port is to build the second
  filter alongside the first and select between them per source peer, the way Go selects per packet,
  plus carrying `is_jailed` onto `ts_control::Node`. The negative cases are what make it safe:
  a reply to a flow *this* node opened to the jailed peer must still be admitted (or selecting a
  jailed exit node breaks the exit node), an unjailed peer must be unaffected, and a peer that
  becomes unjailed in a later netmap must stop being filtered — the jailed flag is per netmap, not
  sticky. Worth reading alongside the shields-up reason on the TSMP reject path, which already
  distinguishes a shields-up deny from an ACL deny; a jailed peer's refusal should carry the same
  reason a Go node's does.

- **Per-peer masquerade addresses are decoded and never applied**
  (`net/routemanager/routemanager.go`: `PeerRoute.MasqAddr4`/`MasqAddr6`; `net/tstun/wrap.go`:
  `peerConfigTable.snat`/`dnat`/`selectSrcIP`/`mapDstIP`; `ipn/ipnlocal/peerapi.go`:
  `isAddressValid`, `5186c3f41` at this pin; capability versions 64 and 87) — **needs port**, and it
  is the row whose failure mode is hardest to diagnose from this side.
  When two tailnets are shared, control can tell this node "peer P knows you as address M" by
  setting `SelfNodeV4MasqAddrForThisPeer` (or the v6 field) on P in our netmap. Upstream honours it
  as symmetric NAT in the data plane: `selectSrcIP` rewrites the source of a packet *we* originate
  to `M` when the destination is P's prefix (and refuses to do so when the source is not our own
  native address, so a subnet-routed packet is left alone), and `mapDstIP` rewrites the destination
  of a packet *from* P back from `M` to our native address. Both go through `checksum.UpdateSrcAddr`
  / `UpdateDstAddr`, and both are documented as never changing packet length or address family.
  L7 follows the same rule: `peerAPIHandler.isAddressValid` accepts a `Host` naming `M`. That last
  function is the only part of this upstream touched in the new interval — `5186c3f41` fixed it to
  require a masquerade match only for the *family* the masquerade applies to, because a v6-only
  masquerade pair still leaves the peer dialling our native v4 peerAPI URL and every fresh
  connection was getting a 403.
  Nothing here applies any of it. `ts_control_serde::Node` models both fields, with Go's own comment;
  neither reaches `ts_control::Node`; and there is no SNAT or DNAT anywhere in `ts_dataplane` — the
  outbound path runs the capture tee, the host-injected-TSMP drop, the flow-state record and the
  routing, and never touches an address. `ts_runtime::peerapi`'s `is_self_address` records the
  consequence honestly and reaches the wrong conclusion from it: its doc says masquerade "is not
  implemented at all and there is no current divergence". The divergence is real, because the
  behaviour is *control-driven and peer-side*. Capver 64 (2023-03-16) and 87 (2024-08-03, "…now
  works") both sit below the 125 this node declares, so control may configure a masquerade for a
  shared peer on the strength of that declaration alone. When it does, the peer sends to `M`, we
  receive a packet addressed to an IP we do not own and the netstack drops it; we reply from our
  native address, which is outside the peer's allowed IPs for us, and the peer drops that. The node
  is simply unreachable from that peer, with nothing in either log naming a cause.
  Two ways to close it, and the choice is the row's real content. **Implement it**: carry both
  fields onto `ts_control::Node`, add the two rewrites to `ts_dataplane`'s inbound and outbound
  paths with incremental checksum updates, and teach `is_self_address` the masquerade address
  per family (Go's shape *after* `5186c3f41`, not before — the pre-fix shape 403s a mixed-family
  peer). **Or refuse it**: hold the declared capability version below 64, which this tree cannot do,
  because #335 established a *floor* of 121 for the peer-relay client half. That leaves implementing
  it, or accepting a knowingly false declaration — which is the kind of thing this ledger exists to
  stop happening silently. Test the refusals with the port: a subnet-routed packet is not SNATed
  (Go returns early when the source is not our native address), a family with no masquerade
  configured is untouched, and the checksum is updated rather than recomputed from scratch.

- **A TSMP ping gets no pong**
  (`net/tstun/wrap.go`: `filterPacketInboundFromWireGuard`'s `p.AsTSMPPing()` arm and
  `injectOutboundPong`; `net/packet/tsmp.go`: `TSMPPingRequest`, `TSMPPongReply`;
  `wgengine/userspace.go`: `OnTSMPPongReceived`) — **needs port**, and it is the carried row a real Go
  peer can see without any help from control.
  Upstream answers a TSMP ping in the wrapper, before any filtering: `AsTSMPPing` matches, the node
  notes activity, `injectOutboundPong` builds a `TSMPPongReply` carrying the request's 8-byte
  opaque data and this node's peerAPI port, reverses the IP header with `ToResponse` and injects it
  outbound, and the ping itself returns `filter.DropSilently` so it never reaches the local stack.
  The other direction is symmetric: an arriving pong is handed to `OnTSMPPongReceived`, which
  `userspaceEngine` dispatches through a `pongCallback` map keyed on those same 8 bytes, and that is
  how `tailscale ping` reports an end-to-end round trip that a disco ping cannot measure — disco
  ping proves a *path*, TSMP ping proves the *node* is alive and processing packets.
  This tree names the two type bytes and stops: `ts_packet::tsmp`'s `TSMP_TYPE_PING` and
  `TSMP_TYPE_PONG` each carry the doc comment "Not parsed here". `ts_dataplane`'s inbound TSMP arm
  consumes the disco advertisement and (since #421) the rejected-connection header, and lets every
  other TSMP body through to the ACL step, which accepts it and hands it to `smoltcp` — which has no
  handler for IP protocol 99. So a Go peer running `tailscale ping --tsmp` against this node waits
  out its timeout while the node sits there working perfectly. It is the same class of silence #421
  removed from the ACL-drop path, one message type over.
  The send half is the port worth doing first and it is small: `ts_packet::tsmp` already marshals two
  TSMP bodies against Go's own layout, so a third is the same shape, and `ts_dataplane` already has
  a route for injecting a TSMP message back to a peer — `harvest.rejects_to_send`, built for the
  reject. The pong's `PeerAPIPort` field should be filled from the same `RejectConfig::peerapi_port`
  the reject path already carries, since that is exactly Go's `t.PeerAPIPort` and it is already
  plumbed. The receive half (matching a pong against an outstanding request) needs a caller here to
  be worth anything — `Device` has no `ping` surface today — so it can be a separate decision; say
  which way you went. Refusals to bring with it: a ping whose body is short is not a ping (Go's
  `AsTSMPPing` length check), a ping is dropped rather than delivered even when a pong could not be
  built, and a pong is never generated for a non-IPv4/IPv6 header (Go's `default: return`).

- **The peerAPI has no packet-level carve-out, so it is reachable only through the ACL**
  (`net/tstun/wrap.go`: "Let peerapi through the filter; its ACLs are handled at L7, not at the
  packet level") — **needs a decision more than a port**, and it is the other half of the design
  #423 ported one half of.
  Upstream pairs a deliberate hole in the packet filter with an L7 check inside each handler. The
  hole: after `RunIn` returns a non-`Accept` outcome, `filterPacketInboundFromWireGuard` flips it
  back to `Accept` when the packet is a TCP SYN whose destination port is this node's peerAPI port.
  The check: every peerAPI handler that needs an access decision makes it itself — Taildrop against
  the file-sharing capability, the DNS proxy against `isPeerAPIDNSAllowed`. The two are one design,
  and upstream's reason for it is that a tailnet ACL is written about *ports and services*, not
  about the daemon's own ephemeral peerAPI port, so requiring the ACL to name that port would make
  Taildrop depend on a rule nobody writes.
  This tree now has the L7 half and not the packet half. #423 landed the DNS source gate, and
  `ts_runtime::peerapi`'s `gate_taildrop` is the file-sharing check; but `ts_dataplane` admits a
  peerAPI SYN only if some ordinary rule covers it. `tsmp_reject_for_drop` documents the seam
  exactly — it suppresses the TSMP reject for a peerAPI-port SYN "by suppressing the message here
  rather than by admitting the packet", so a peer whose ACL does not cover our peerAPI port is
  dropped *and* told nothing, where Go would have admitted it and then made an L7 decision.
  Which way to go is a genuine choice and it should be made deliberately rather than by omission.
  Adding the carve-out is parity, and it is what makes Taildrop from a Go peer work under an
  ordinary ACL; it also widens what reaches the peerAPI to every peer, which is safe only because
  the L7 gates are now in place — and they were not, before #423. Leaving it out is stricter, and
  the fork has been running that way; the cost is that Taildrop *to* this node silently fails on a
  tailnet where it works for every Go client, and the failure looks like a network problem. The
  reading this ledger recommends is to add it, now that both L7 gates exist, and to gate it exactly
  as Go does — TCP, SYN only, destination port equal to the peerAPI port, and only when a peerAPI is
  actually running — so the hole is no wider than upstream's. Whichever way it goes, the decision
  belongs in the code as a comment, because the current state reads as an oversight and is not.

- **TSMP rejected-connection messages are neither sent nor understood**
  (`net/tstun/wrap.go`, `wgengine/pendopen.go`, `net/packet/tsmp.go`) — **already covered**,
  *changed from "needs port"*: #421 landed both halves after the previous revision was written, and
  #428 corrected the receive half's log level.
  Upstream's send half injects a `packet.TailscaleRejectedHeader` (TSMP type `!`) back to the peer
  when `filterPacketInboundFromWireGuard` drops an IPv4 TCP SYN on the ACL, carrying the four-tuple
  and a reason — `RejectedDueToACLs`, or `RejectedDueToShieldsUp` when the filter has shields up;
  its receive half parses one in `trackOpenPreFilterIn`, matches it against the pending-open flow
  table, and drops it silently rather than delivering it.
  `ts_packet::tsmp` now carries `TailscaleRejectedHeader` with both a marshaller and a parser, and
  `RejectReason` spells out all four of Go's reasons plus the zero. `ts_dataplane`'s
  `tsmp_reject_for_drop` is the send decision, and it ports Go's guards refusal for refusal:
  nothing for a non-IPv4 drop, nothing for a non-SYN, nothing when `disableTSMPRejected` is set, and
  nothing for a peerAPI-port SYN — that last one reaching Go's outcome by suppressing the message
  rather than by admitting the packet, which is the seam the new peerAPI carve-out row above is
  about. The reason is `SHIELDS_UP` or `ACLS` off `filter.shields_up()`, and `maybe_broken` stays
  false because neither reason is one of Go's non-terminal ones. The receive half consumes an
  inbound reject instead of delivering it to `smoltcp` and records it on
  `InboundHarvest::rejected_flows` for the embedder. #428 is worth keeping in view because it is a
  fork-local consequence rather than a Go one: this tree has no pending-open flow table to match a
  reject against, so Go's `open-conn-track: flow … rejected due to …` line had nothing to filter it
  and any authenticated peer could drive an operator's default-level log at link speed. It now logs
  at `debug!`, in step with the sibling TSMP branches, and the embedder — the layer that *can* do
  Go's match — is the layer that decides a rejection is worth an `info!`.
  The capver-146 aggregator that counts these events is separately out of scope — see §A and the
  *no counterpart here* list.

- **The peerAPI DNS proxy answers any peer that can reach it**
  (`ipn/ipnlocal/peerapi.go`: `isPeerAPIDNSAllowed`, `a4c790224`) — **already covered**, *changed
  from "needs port"*: #423, with #425 for a case the first pass got wrong.
  Upstream gates `handleDNSQuery` before it resolves anything and answers `403 DNS access denied`
  when the gate fails: a peer that is untagged and owned by the same user is allowed outright,
  otherwise the node must be `OfferingExitNode() || OfferingAppConnector()` **and** the peer must
  pass `filter.CheckTCP(remoteIP, 0.0.0.0-or-2000::, 53) == Accept`.
  `ts_runtime::peerapi_doh::dns_source_allowed` is now that gate, evaluated once per connection
  ahead of any resolution. It resolves the connection's source to a known tailnet peer, refuses when
  it is not one, and then asks the live packet filter whether a TCP SYN from that source to an
  off-tailnet address on port 53 would be accepted — `internet_probe_dst` picking the family the way
  Go picks between `0.0.0.0` and `2000::`. Go's self arm is deliberately not ported and the function
  says why: this fork carries no owner notion on the peerAPI path, and the arm only ever *widens*
  upstream's answer, so skipping it is the strictly safer of the two readings. #425 is the
  correction worth recording: a filter that has not been compiled yet is Go's `f == nil` refusal,
  but the first pass reached the same `false` through a path that also refused *after* a filter was
  dropped and rebuilt, which left the gate refusing every peer until the next netmap. The refusal
  now distinguishes "no filter yet" from "filter says no", and both are tested. The one thing the
  gate deliberately does **not** touch is the divergence already recorded in `peerapi_doh`'s module
  docs — this server answers some names authoritatively where Go forwards. That is about *which
  names*; this row was about *which peers*. Note also what the gate still cannot express: it hands
  `can_access` an empty capability set, so a policy that grants internet access by `cap:` rather
  than by prefix refuses here. That is the new `SrcCaps` row above, and closing it closes this
  residue with it.

- **The inbound ACL admits no replies: TCP non-SYN and ICMP responses are dropped**
  (`wgengine/filter/filter.go`: `runIn4`/`runIn6`) — **already covered**, *changed from "needs
  port"*: #419, with #430 for a fragment case it exposed.
  Go accepts an inbound TCP segment that is not a SYN unconditionally ("we want to allow return
  packets on those connections … a new incoming session can't be initiated without first sending a
  SYN") and an ICMP echo response or ICMP error unconditionally ("ICMP responses are allowed"),
  both ahead of any rule match.
  `ts_dataplane::inbound_filter_verdict` now runs both carve-outs in Go's order, between the
  reverse-flow cache and `can_access`, each quoting the upstream comment it ports.
  `ts_packetfilter::PacketInfo` gained an `l4` member carrying the decoded TCP flags and ICMP type,
  which is what made the non-SYN case representable at all — and `PacketInfo::is_tcp_non_syn`
  demands a *decoded* flags byte rather than treating "no flags available" as "not a SYN", so the
  carve-out cannot be reached by a packet whose L4 header was not parsed. The negative case that
  gives the change its value is asserted: an inbound TCP **SYN** with no matching rule is still
  dropped. The third class — ICMP that is neither a response nor an error still matching IPs-only —
  was already right in `ts_packetfilter::rule` and was left alone.
  #430 is the fork-local consequence, and it is the kind this ledger exists to record: making the
  verdict depend on a decoded L4 header meant the *head fragment* of a fragmented reply, which
  carries the L4 header, was being classified before reassembly and dropped. The fragment
  classification and the reply carve-outs now compose in the right order, with the pre-existing
  RFC 1858-style fragment refusals unchanged.

- **Outbound UDP flows are not tracked, so their replies need an explicit rule**
  (`wgengine/filter/filter.go`, `net/tstun/wrap.go`: `e0677ccc7`) — **already covered**, *changed
  from "needs port"*: #418.
  Go's `Filter` carries a 512-entry `flowtrack` LRU that `RunOut`'s `UpdateOutboundFlowState` fills
  with the *reversed* tuple for UDP and SCTP, consulted first by `runIn4`/`runIn6` for those
  protocols and returning `Accept, "cached"` on a hit; `e0677ccc7` extended the fill to netstack's
  injected path, which is the only path this fork has.
  `ts_dataplane::flowtrack` is now that cache, bounded at Go's own 512 rather than at an invented
  number, filled by `process_outbound` for every outbound UDP and SCTP packet and consulted at the
  top of `inbound_filter_verdict` — ahead of the stateless carve-outs, in Go's order, with the
  upstream code quoted beside it. The comment that matters for the next reader is on the miss path:
  "the cache only ever admits, it never denies", so a miss falls straight through to the rule match
  rather than short-circuiting to a drop. The negative cases are asserted: an inbound datagram with
  no tracked flow and no rule is still dropped, an entry does not admit a datagram from a different
  source address or port, and the cache is bounded so a peer cannot grow it without limit.

- **Exit-node suggestion ranks on the latest report, not on recent per-region latency**
  (`ipn/ipnlocal/local.go`, `net/netcheck/netcheck.go`: `f442cda99`, `e9e209673`) — **already
  covered**, *changed from "needs port"*: #417.
  Upstream's `suggestExitNodeUsingDERP` takes `preferredDERP` plus a `regionLatency` map from
  `netcheck.Client.RecentRegionLatency()` — the lowest latency seen per region across a retained
  history whose window is tied to the full-report interval — rather than the single most recent
  report, because an incremental report made a distant exit node fall back to a random pick.
  `ts_runtime::exit_node_suggest::suggest_exit_node` now takes `(preferred_region,
  region_latency)`, and `ControlRunner::recent_region_latency` is this fork's
  `RecentRegionLatency()`: a per-region best-latency history kept beside the measurer, with its
  retention window tied to the re-measure cadence the way upstream ties its to
  `fullReportInterval`. `min_latency_derp_region` is unchanged as a port of `minLatencyDERPRegion`
  and now reads the history instead of one report, and the two injected selectors — ports of Go's
  `selectRegionFunc`/`selectNodeFunc` — are kept, because they are what makes the algorithm
  testable without a DERP map. The reason this mattered more here than upstream is unchanged and
  worth keeping written down: `ts_netcheck::Config` defaults to `complete_threshold: 3`, so a report
  against a real DERP map names a handful of regions *every* time, not merely on incremental runs.

- **MagicDNS does not resolve subdomains for peers carrying `dns-subdomain-resolve`**
  (`net/dns/resolver/tsdns.go`, `tailcfg/nodecap/nodecap.go`: `f48cd4666`) — **already covered**,
  *changed from "needs port"*: #415.
  Upstream's resolver keeps a `SubdomainHosts` set beside its `Hosts` map and, on a miss, walks the
  queried name's parents looking for one that names a subdomain host, so for a node named `machine`
  both `my.machine` and `be.my.machine` resolve to it at any depth.
  `ts_runtime::magic_dns` now carries the attribute onto the DNS view and does the parent walk, and
  the cases that keep it from becoming a wildcard are pinned: a node *without*
  `dns-subdomain-resolve` still answers `NXDOMAIN` for its subdomains, an exact name still beats a
  parent match, the walk stops at the tailnet zone rather than climbing out of it, and the
  multi-label case resolves. The peerAPI DoH server shares `decide`, so the widening reaches that
  path too — `peerapi_doh`'s module docs now say so explicitly rather than leaving it to be
  discovered, which is the right treatment for a behaviour that widens what a *peer* can ask this
  node about.

- **TSMP disco-key advertisement** (`net/packet`, `net/tstun`, `wgengine/magicsock`,
  `control/controlclient`: `c54d24369`, `c870d3811`, `bf467727f`, `82a381e54`, `014d5bd9e`,
  `3799eaf26`, `fb27d87e0`) —
  peers advertise their disco key in a TSMP message around the WireGuard handshake, and learn a
  peer's disco key from it without restarting WireGuard. It is the one item here a real Go peer will
  *send us* unprompted. **Both halves remain covered.** Receive: `ts_packet::tsmp` decodes the
  advertisement (Go `Parsed.AsTSMPDiscoAdvertisement`), `ts_dataplane::filter_inbound_from_peer`
  consumes it ahead of the ACL and drops it rather than delivering it to the local stack (Go
  `tstun.filterPacketInboundFromWireGuard` returning `filter.DropSilently`), a zero advertised key
  is ignored (Go's `!discoKeyAdvert.Key.IsZero()` guard, `82a381e54`), and
  `PeerTracker::learn_disco_key` applies it to the peer. Send: `ts_packet::tsmp` marshals it against
  Go's own `TestTSMPDiscoKeyAdvertisementMarshal` vectors, `ts_tunnel` reports the two moments
  `wireguard-go` calls `SendPriorityMessage` (`device/receive.go`) and carries its refusals (empty or
  oversize is dropped, not truncated; a peer with no live keypair sends nothing), and
  `ts_dataplane::DiscoAdvertisementState::advertisement_for` decides the content, refusal for
  refusal against Go `magicsock.Conn.PriorityMessageForPeer` — including the one a wireguard-only
  peer depends on (`ep.isWireguardOnly` ⇒ send nothing), which matters because that peer is a plain
  `wireguard-go` or kernel endpoint that would receive an IP-proto-99 packet it has no idea what to
  do with. Row 144 above is the capability-version view of the same work.
  **The three rows an earlier revision opened off the back of this one have all closed**; they are
  the next three bullets, and each records what closed it.

- **A peer's *inactive* known disco key is accepted on ingress, and becomes the active key**
  (`wgengine/magicsock/endpoint.go`, `wgengine/magicsock/magicsock.go`: `da1fc4fc8`, `e1d17a6b9`) —
  **already covered**, *changed from "needs port"*: #372 landed it after an earlier revision was
  written.
  Upstream resolves every inbound disco comparison in `Conn.handleDiscoMessage` and
  `unambiguousNodeKeyOfPingLocked` through `endpoint.checkAndUpdateDiscoKey`, which returns true for
  **either** slot's key and, when the key seen is the currently-inactive one, compare-and-swaps
  `tsmpActive` so that key becomes the active one and calls `changedActiveDiscoLocked`.
  Here `PeerDb` now carries a second index — `inactive_disco_idx`, the peer's other known key — and
  `PeerDb::peer_by_known_disco_key` resolves an inbound frame's sender against both, so a peer
  mid-rotation that is still sending under the key control gave us is understood instead of dropped.
  `EndpointDisco::check_and_update` is the switch itself, cited to
  `endpoint.checkAndUpdateDiscoKey` by name, and it keeps the refusal that carries the security
  value: a key belonging to **neither** slot is refused even though the frame that carried it
  opened correctly, so a peer cannot move itself onto a key nobody told us about. The switch and the
  path invalidation it triggers travel together, on the one `peer_watch` snapshot every other
  disco-key transition already used.

- **A control disco-key update does not preempt an active TSMP-learned key** (`wgengine/magicsock`,
  `control/controlclient`, `ipn/ipnlocal`: `f53c28101`) — **already covered**, *changed from "needs
  port"*: #370.
  Upstream's `endpoint.updateDiscoKey` sets `epDisco.tsmpActive = old.tsmpActive || key.IsZero()`:
  control's new key is still *recorded*, but a TSMP-learned key that is already active stays active,
  and the switch back to control's key happens only when disco is actually received under it.
  `EndpointDisco::update_from_control` is now that line — `self.tsmp_active = self.tsmp_active ||
  key.is_none()` — and the doc comment this ledger called "the single most misleading line in the
  tree on this subject" at an earlier revision has been rewritten to say what the code now does.
  The ordering hazard an earlier revision flagged was respected: #372 landed the ingress half
  first, so a peer whose key control has legitimately rotated is not stranded by the stickiness.
  The `ScheduleHandshakeOnUserSend` half of `f53c28101` still has no target here — `ts_tunnel` is
  this fork's own WireGuard implementation and has no such callback — but the behaviour it replaced
  (tear down and re-establish on every disco-key change) is not what this tree does either, so
  nothing regresses by leaving it.

- **The trusted direct path is invalidated when a peer's disco key changes**
  (`wgengine/magicsock/endpoint.go`: `65d222674`, `e1d17a6b9`'s `changedActiveDiscoLocked`) —
  **already covered**, *changed from "needs port"*: #369.
  Upstream sets `trustBestAddrUntil = 0` and calls `invalidateDiscoPathLocked()` on *every* disco-key
  transition, keeping `bestAddr` so data keeps flowing while a fresh path is confirmed.
  An earlier revision found the right primitive here with no caller on this path — it had
  `MagicSock::rebind` and nothing else. #369 added the missing one, and gave it its own method rather
  than reusing the rebind case, because the two are not the same invalidation:
  `ts_magicsock::path::PeerPaths::invalidate_disco_path` clears the trust window, the in-flight
  probes (their tx ids went to the old key, so a matching pong must not re-confirm across the
  rotation) and every candidate's measurement and ping timestamps, while **keeping** `best` and the
  candidate set, which is exactly Go's "keep bestAddr so that we can still send data while we find a
  new path". `invalidate_best`, the rebind case, still clears the best address outright, because
  there the *local* NAT mapping changed and the address is stale as an address.

  **Narrowed twice at this revision, and the row's own wording was part of what was wrong.** #369
  left the re-probe to the periodic sweep, so this row (and the function's doc) claimed traffic rode
  DERP "for the one round trip it takes to re-confirm" when nothing had asked for that round trip —
  it was up to a full `PING_INTERVAL` (2 s) short of true. #451 (`588ea36`) pings inside the netmap
  handler instead, and #453 (`4060be0`) fixed the arm of `changed_active_disco` that answered "no
  re-probe needed" for the case that needs it most: where path state already exists under the peer's
  *new* disco key, which is normally an unconfirmed entry built from an inbound disco ping rather
  than a confirmed one. Both are closer to upstream without reaching it: Go's `addrForSendLocked`
  returns the retained-but-untrusted `bestAddr` **and** the DERP address, so a packet rides both
  while the path is re-confirmed, and this dataplane routes each peer through exactly one underlay.
  That residual difference is a deliberate divergence of the overlay-router design, not an open row.
  Recorded here because the row read as settled for two revisions while its central timing claim was
  not checked against the code.
  The path in is `ts_runtime::direct::disco_key_rotations`, which diffs consecutive peer snapshots
  and calls `MagicSock::changed_active_disco` — this fork's `changedActiveDiscoLocked` — so a
  control-side change, a TSMP advertisement and an active-slot switch on receive all reach the same
  invalidation, which is what stops the node coasting for a full `TRUST_DURATION` (6.5 s) on a path
  confirmed by a pong signed under a key the peer has since replaced. One fork-local limit is worth
  recording rather than leaving to be rediscovered: `PeerPaths::best_addr` gates on trust, so this
  node does not keep *sending* to the retained best the way Go's `addrForSendLocked` dual-sends to an
  untrusted `bestAddr`. The retained address is what the immediate re-probe targets, and traffic
  rides DERP for the one round trip it takes to re-confirm.

- **Peer capabilities are not withheld from `UnsignedPeerAPIOnly` peers**
  (`ipn/ipnlocal/node_backend.go`, `wgengine/magicsock/magicsock.go`, `control/controlclient/map.go`:
  `0eb38dc2e`) — **half covered, half still needs port**, *changed from "needs port"*.
  Upstream refuses such peers three things, all unconditionally and none of them dependent on
  tailnet lock being enabled, because the point is that an unsigned peer is by definition outside
  the lock's coverage: `upgradeNode` clamps their `AllowedIPs` back to their own `Addresses`;
  `nodeBackend.peerCapsLocked` / `PeerCapsForIP` / `PeerCapsForService` return nil for them; and
  `magicsock.nodeHasCap` refuses them the relay-allocation and relay-target capabilities.
  The route clamp landed here at #365: `ts_control::Node` now carries `unsigned_peer_api_only`, and
  the `From<ts_control_serde::Node>` impl clamps `accepted_routes` back to the node's own addresses
  whenever it is set, with a regression test that gives an unsigned and a signed peer the *same*
  advertised route so it cannot pass by dropping routes generally. That closes the case where
  control could hand an unsigned peer `0.0.0.0/0` and have this node route to it.
  The capability half is untouched. There is still no per-peer capability map anywhere in the domain
  model — `ts_control::Node` carries only the node-attribute `cap_map`, and
  `ts_runtime/src/peerapi.rs` still records threading `PeerCapMap` in as an open limitation — and
  `ts_magicsock`'s relay module still accepts a `CallMeMaybeVia` with no capability gate on the peer
  at all. Admission under an *active* lock remains the separate, deliberate divergence
  [`docs/PARITY_ROADMAP.md`](docs/PARITY_ROADMAP.md) records: this tree drops unsigned peers
  outright there, which is stricter than Go.

- **MagicDNS negative answers carry an SOA, and positive answers expire in 5 seconds**
  (`net/dns/resolver/tsdns.go`: `0bbe6394d`) — **already covered**, *changed from "needs port"*:
  #367. Upstream attaches the zone's SOA to the authority section of every NXDOMAIN and NODATA
  response it is authoritative for, advertising a 10-second negative-caching TTL (RFC 2308), and
  dropped the positive-answer TTL from 600 seconds to 5; the motivating bug is a macOS
  `mDNSResponder` that caches an SOA-less negative answer on its own schedule, so a name queried
  shortly *before* a node was renamed to it does not start resolving until something flushes the
  cache. `ts_dns_wire`'s `ANSWER_TTL` is now `5`, `NEGATIVE_TTL` is `10`, and `encode_response`
  emits an authority section carrying a single SOA whose MNAME and RNAME both repeat the zone name
  and whose REFRESH/RETRY/EXPIRE/MINIMUM all repeat the negative TTL — the same placeholder shape as
  Go's `marshalSOA`, for the same reason: nothing consumes those fields, only the TTLs mean
  anything. The interaction an earlier revision warned about was handled rather than dodged: an
  authority record makes a previously-fitting negative answer larger, and the truncation path is
  tested with the SOA present.

- **Host-injected TSMP is dropped on the TUN-to-WireGuard path** (`net/tstun/wrap.go`:
  `9175fe267`) — **already covered**, *changed from "needs port"*: #360.
  Upstream's `filterPacketOutboundToWireGuard` drops any packet the host writes into the TUN whose
  `IPProto` is TSMP, counting `tstun_out_to_wg_drop_tsmp`, on the rule that "TSMP traffic should only
  originate from tailscaled, not from the host itself". `ts_dataplane::process_outbound` now makes
  that check at the top of the outbound path, before routing. The negative case that gives the fix
  its value is asserted: this node's *own* advertisements are injected below that point via the
  priority-message path, so they still go out — without that test the fix would silently disable
  capability version 144.

- **A TKA `SyncOffer` offers every checkpoint ancestor** (`tka/sync.go`, `tka/limits.go`:
  `f6fa29463`) — **already covered**, *changed from "needs port"*: #363.
  Upstream replaced the exponential ancestor sampling (`ancestorsSkipStart = 4`,
  `ancestorsSkipShift = 2`) with "offer every ancestor whose `MessageKind` is `AUMCheckpoint`", and
  raised `maxSyncHeadIntersectionIter` from 400 to 1000, because nodes compact aggressively and an
  exponentially sampled offer can be *disjoint* from what the node kept — leaving it unable to find
  a common ancestor and stuck in a poll-and-fail loop with a permanently stale view of the tailnet.
  `ts_tka::Authority::sync_offer` now walks parents and pushes checkpoints, with the cap at 1000.
  The trap an earlier revision named was checked: `missing_aums` consumes the same offer, and both
  arms of `intersection` were re-tested against the checkpoints-only ancestor shape.

- **A peer removal evicts index entries another peer has since claimed**
  (`ipn/ipnlocal/node_backend.go`: `2ae2808b6`) — **already covered**, *changed from "needs port"*:
  #412 landed it after the revision that opened the row was written.
  Upstream's `nodeBackend` used to evict its index entries (`nodeByAddr`, `nodeByKey`,
  `nodeByWGString`, `nodeByStableID`, `nodeByName`) from a node's last-known value without checking
  that the entry still pointed at that node, and made every eviction conditional through a
  `deleteIfOwned` helper. The exposure was never theoretical here and was worse than the ordering
  upstream had to defend against: `PeerTracker::apply_peer_update` applies a `Delta`'s upserts first
  and its removals second, so the intra-batch ordering that broke Go is the ordering this tree always
  uses, and a peer inheriting a departing peer's tailnet IP was installed in `ip_idx` and then evicted
  by the departing peer's removal.
  `ts_runtime/src/peer_tracker/peer_db.rs` now carries `delete_if_owned` and `delete_ip_if_owned`, and
  every retraction in `IndexState::remove` goes through one of them — `nk_idx`, `stableid_idx`,
  `control_idx`, both `ip_idx` entries, **both** halves of `name_idx` (the fqdn half was unguarded too,
  which an earlier revision's write-up missed) and `disco_idx`. The IP helper matches on the exact
  prefix (`lookup_prefix_exact`), never a longest-prefix match, because a peer owns the row for its own
  address and not for whatever covering route answers a lookup. The five hand-rolled guards already on
  the upsert path call the same helper, so there is one definition of the rule rather than six
  spellings of it — which is how the removal path came to be missing it. `route_idx` is deliberately
  still unguarded: it is a multimap keyed by route holding a vec of peer ids, so a successor's claim
  does not displace the departing peer's row and the removal already filters by id. The orderings that
  actually differ are tested, and so is the positive case, so the fix cannot pass by never evicting
  anything.

- **Expired peers are neither flagged nor re-evaluated when their keys expire**
  (`ipn/ipnlocal/expiry.go`, `ipn/ipnlocal/local.go`: `0640312e5`, and the `expiryManager` the commit
  repairs) — **already covered**, *changed from "needs port"*: #408, with #410 for two follow-ups.
  Upstream's `expiryManager` does three things this tree did none of: `flagExpiredPeers` marks a peer
  whose `KeyExpiry` has passed, clears its `Endpoints` and `HomeDERP` and breaks its node key with
  `key.NodePublicWithBadOldPrefix`; `nextPeerExpiry` finds the soonest future expiry across peers and
  self so a timer can re-evaluate on time rather than on netmap arrival; and `onControlTime` corrects
  every expiry comparison for the local-to-control clock delta, ignoring a delta-adjusted "now" before
  a hardcoded epoch so a control server sending a wildly past `ControlTime` cannot expire the tailnet.
  `ts_control::ExpiryManager` ([`ts_control/src/expiry.rs`](ts_control/src/expiry.rs)) is all three,
  function for function, and `ts_runtime::status::StatusNode` now carries the flag for a watcher to
  read. The peer is **flagged, not dropped** — that is the point: it keeps its identity, so `whois`,
  `status` and a refused peerAPI dial can each say *why* it is unreachable rather than "no such peer",
  which is what Go's "peer's node key has expired" refusal reads as.
  Two fork-local consequences were found and fixed in #410 rather than left to be rediscovered, and
  both are worth keeping in view because they are consequences of this tree's shape, not Go's. Upstream
  re-derives its netmap from `controlclient`'s pristine peer store on every pass, so a peer whose key
  control later extends comes back with its endpoints and DERP home restated; **here the peer db *is*
  the store**, so flagging destroyed the only copy and a recovered peer came back unroutable. The memo
  this fork already kept for the pristine node key (`previously_expired`, Go's `previouslyExpired` set)
  now keeps the endpoints and DERP region alongside it, and the restore is non-clobbering, so an update
  that extends the expiry *and* restates endpoints keeps control's word. Separately, `Device::send_file`
  read `expired` and `peerapi_addr` off a caller-supplied `NodeInfo` snapshot that could be arbitrarily
  old; it now re-reads the live record at the decision point.

- **A REFUSED or SERVFAIL from the first upstream ends the forward**
  (`net/dns/resolver/forwarder.go`: `0b4c0f208`) — **already covered**, *changed from "needs port"*:
  #406. Upstream treats both response codes as *soft* while a query is outstanding against more than
  one upstream — a broken resolver answering REFUSED quickly must not beat a healthy resolver that is
  still working — and returns an upstream's own SERVFAIL bytes verbatim rather than a locally
  synthesized packet, because the upstream's answer may carry RFC 8914 extended DNS error information.
  `ts_runtime::magic_dns` no longer ends the walk on either code: the first such response is
  remembered, the walk goes on, and it is relayed only once the upstream list is exhausted — verbatim,
  and in preference to the caller's synthesized SERVFAIL, so an extended error survives. Every other
  RCODE, NXDOMAIN included, is a real answer and still ends the walk where it is found. The anti-
  poisoning check an earlier revision insisted on is unchanged and still runs first: a datagram from
  an address we did not query, or one that does not echo our transaction id and question, is discarded
  before it can be relayed *or* remembered as the soft error, so an off-path injector cannot plant the
  response a fully-refused forward ends up returning. The socket work (`ask_upstream`) and the decision
  of which response the client gets (`forward_walk`) are now separate functions, which is what makes
  the walk testable without a network. **One sentence of this row is wrong and is corrected at this
  revision**, without changing its verdict: "the walk goes on" describes this tree, not upstream.
  Go does not walk — `forwardWithDestChan` starts every resolver at once (`forwarder.go:1282`) and
  the soft-error tally is what runs when all of them have failed. The row's own claim, that both
  codes are soft and that an upstream's bytes are relayed verbatim, is still met. The shape
  difference is the third row under [Rows](#rows).

- **`UserProfile.Groups` is not modelled** (`tailcfg`: `6a19995f1`) — **already covered**, *changed
  from "needs port"*: #404. Upstream reintroduced `UserProfile.Groups`, "a subset of SCIM groups (e.g.
  `engineering@example.com`) or group names in the tailnet policy document (e.g. `group:eng`) that
  contain this user and that the coordination server was configured to report to this node", carried in
  `MapResponse.UserProfiles` and surfaced through `WhoIs`. It is now modelled the whole way:
  `ts_control_serde::UserProfile` gains `groups: Vec<Cow<'a, str>>` (`Cow`, not `&str`, because Go
  HTML-escapes `&` on marshal and a borrowed slice would fail the decode of the whole profile on a group
  named `R&D`), `ts_control::UserProfile` carries it into the domain, and
  `ts_runtime::status::WhoIs` now holds `user_profile: Option<UserProfile>` instead of a flattened
  label — `WhoIs::user()` gives back exactly the old string and `WhoIs::user_groups()` the groups. The
  decision an earlier revision left open (widen `WhoIs` or add a narrow accessor) went the way it
  recommended: widen, because that is the shape that absorbs the next `UserProfile` field without
  another decision. The absent case is asserted as well as the present one — an omitted key, an
  explicit `[]` and a wire `null` all decode to an empty list and none of them fails the profile.

- **IPv6 fragment extension-header handling in the filter** (`net/packet`, `wgengine/filter`:
  `4c4ec3d46`, `26b2ed0a6`) — **already covered**, unchanged at this revision and re-checked. #342 gave
  `ts_dataplane` the IPv6 half of the RFC 1858-style classification it had only for IPv4, #343
  extended it to a Fragment header hidden behind a chained extension header, and #345 rewrote the
  tests so each extension header has its own control and its drop cannot pass vacuously. #398 added
  one more pin at an earlier revision (the pre-rule drop of a proto-0 first IPv6 fragment), and #390
  stopped a prepended IPv6 header choosing which rule matches.

- **Quad-100 traffic is absorbed locally regardless of port and protocol** (`wgengine/netstack`:
  `1b4091161`) — **already covered**, unchanged at this revision. `ts_runtime::tun_actor::classify_service_ip`
  returns `ServiceIpPacket::Absorbed` for **every** packet destined to `100.100.100.100` that is not
  the UDP/53 query it serves, and an unserved quad-100 TCP port is answered with a RST built by
  `build_tcp_reset` (RFC 9293 §3.10.7 CLOSED-state rules) rather than dropped into a retransmit
  loop — upstream's `hittingServiceIP` case in `acceptTCP`.

- **The DNS forwarder sets TC against the *client's* size limit** (`net/dns/resolver`:
  `8cac8b117`) — **already covered**, and refined four times more at an earlier revision. #339 added
  `set_tc_if_over_client_limit`; #395 then corrected *which* answers it applies to, so this node
  sets TC on the same answers a Go node does and not on others; #380 stopped a 4096-byte answer Go
  marks truncated being relayed as though it fit; #387 answered the TCP retry a truncated answer
  forces; and #397 stopped an oversized delegated DoH answer killing the TCP client. The
  pre-existing `MAX_UPSTREAM_RESPONSE` (4096) relay cap stays the separate bound it always was.

- **Peer relay** (`disco` 0x04–0x09, `net/udprelay`, `feature/relayserver`; capver 120/121, i.e.
  *behind* the declared 125) — **ported (client half)**, unchanged in scope at this revision. All
  nine disco message types have a codec (`ts_disco_protocol`'s relay module, checked against Go's own
  `disco_test.go` vectors), and `ts_magicsock` runs the client side end to end: an inbound
  `CallMeMaybeVia` starts the 3-way bind handshake with the named relay server, and a relayed
  ping/pong confirms a Geneve-framed path that carries WireGuard data instead of falling back to
  DERP. Direct paths still take priority over relay ones. #400 hardened it here at an earlier revision: a
  relay server that *refuses* the handshake no longer fails silently. Not ported, and out of scope
  for an embedded client: **serving** as a relay (`net/udprelay.Server`, `feature/relayserver`) and
  *requesting* an allocation of our own. Worth recording alongside: #335 gave the declared capability
  version a **floor** because of this row — a Go peer decides whether to offer us a relay path with
  `magicsock.capVerIsRelayCapable(version)`, which is exactly `version >= 121`, so declaring less
  would silently disable the client half that is ported and working. The declaration is bracketed
  from below as well as above — floor 121, ceiling under 126 — with a ported predicate and a test
  behind the floor.

- **c2n endpoints behind the declared capability version** — capver 127 (`/debug/netmap`), 128
  (`/debug/health`) and row 138 (`/debug/tka/log`) share one responder
  (`ts_control/src/tokio/ping.rs`), which serves `/echo`, `GET /vip-services` and the
  `/remoteapi/localapi/*` prefix of row 142. The three debug endpoints are **resolved by holding
  `CapabilityVersion::CURRENT`** below them. Porting them was rejected on evidence, not preference:
  each needs a subsystem this tree does not have. `handleC2NDebugNetMap` marshals a whole
  `netmap.NetworkMap` (there is no netmap aggregate here — the netmap arrives as `StateUpdate` deltas
  accumulated by the runtime's peer tracker, which the responder cannot see, and control unmarshals
  the body back into Go's struct, so any field we could not fill would read as a zero value rather
  than as "unknown"); `handleC2NDebugHealth` marshals `health.Tracker.CurrentState()` and this fork
  has no health subsystem; and `handleC2NDebugTKALog` serves the AUM chain, which lives in
  `ts_runtime` because `ts_control` deliberately does not depend on `ts_tka`. All three take Go's own
  `400`/`unknown c2n path` fallthrough (`handleC2N`, `ipn/ipnlocal/c2n.go`), asserted by test. The
  declaration is **125**, not 126: capver 126 (seamless key renewal) is not implemented here either —
  this tree's expiry recovery is a node-key rotation plus a full re-register
  (`ts_control::Config::reauth_on_expiry`), which is upstream's *non*-seamless path. 125 is also the
  capability version Tailscale `v1.88.0` declares, so it pairs with a real release for the
  `IPNVersion` in `ts_control::hostinfo`. **The declaration is what gates row 142.** A capability
  version is a contiguous claim, not a set: to declare 142 a node must implement everything from 126
  up, so the c2n LocalAPI proxy sits behind 126, 127, 128, 130
  (`key.HardwareAttestationPublic` / `…KeySignature` in `MapRequest`, no counterpart here) and 138.
  Control will not send `/remoteapi/localapi/*` to a node declaring 125, so that handler is correct,
  tested and dormant, and will stay dormant until that whole run is closed. (129 — a sleep/wake
  deadlock fix in Go's own peer-relay code — is a bug fix in an implementation this tree does not
  share, so it costs nothing.)

- **Services model extension** (`tailcfg`: `1cd8bcc82`, `6cd185bf3`, `fc9b18f50`) — **already
  covered**, *changed from "needs port" at this revision*: #459 (`2194f0d`) ported the consuming half.
  `ts_control_serde::service_vip` now carries `ServiceDetails` (decoded from the `services/<id>` cap
  values under `NODE_ATTR_PREFIX_SERVICES`), `ServiceAction`, `ServiceActionType` with the thirteen
  well-known slugs and the three action-attribute keys, and `ts_control::Node::visible_services()`
  surfaces them off the domain node's cap map. The forward-compatibility shape is the part worth
  re-checking rather than the field list: `ServiceActionType` is a transparent `&str` newtype rather
  than an enum, and action attribute values stay raw JSON, because Go's contract is that a client
  **ignores an action type it does not recognise** rather than failing the netmap — an enum would
  turn a newer control plane into a decode failure that takes the whole map response with it.
  Unparseable values are skipped per value, not per node. This closes the decode side only, which is
  the whole of what a headless library owes here: upstream's *consumers* of these actions are the
  desktop and mobile GUIs, which choose an application to launch per port, and this fork has no such
  surface — so `visible_services()` having no caller in this tree is correct rather than a follow-on
  gap. The `services/` string is one of the sixteen genuine node-attribute reads counted above.

- **`Node.IsRouter` / `PeerStatus.IsRouter`** (`8d830599b`) — **already covered**, and corrected
  again at an earlier revision. Upstream added no wire field: both are *derived predicates* — "does this
  node route addresses besides its own" — spelled as methods so IPN-bus watchers can classify
  routers out of the netmap they already hold. Mirrored here as `ts_control::Node::is_router` (over
  `accepted_routes` vs `addresses`) and `ts_runtime::status::StatusNode::is_router` (over
  `allowed_routes` vs `ipv4`/`ipv6`), cross-checked against each other the way upstream's
  `TestNodeIsRouter` cross-checks its two definitions. #337 fixed the domain predicate (it tested each
  accepted route against the *identity* projection — the first prefix of each family — rather than
  against control's whole `Node.Addresses` list), #340 fixed two test fixtures added alongside it,
  and **#402 fixed the same narrowing where it had survived**: the status
  projection. `StatusNode::is_router` was still asking its question of the `ipv4`/`ipv6`
  first-of-family pair, so a peer control assigned two single-IP prefixes of one family, advertising
  both, was reported as a router to embedders while Go's `PeerStatus.IsRouter` — which tests each
  `AllowedIPs` prefix against the whole `TailscaleIPs` slice — says it is not. `StatusNode` now
  carries `tailscale_ips` beside the identity pair, because the predicate cannot recover addresses
  the projection dropped. Control does not assign that shape today, which is why the divergence had
  cost nothing; the surface it was wrong on is the one embedders actually read.

- **DERP `ClientInfo.AppName`** (`246c82a65`, `75519889f`) — clients may advertise an opaque app
  name (≤32 bytes printable ASCII) which servers relay to watchers and can ban on. **Not
  applicable** — the field is `omitempty` and optional, and `ts_derp`'s `ClientInfoPayload` omits it,
  which is what a Go client without the option does. The related `FramePeerPresent` extension (flags
  byte + app-name suffix) is mesh-only: `ts_derp` classifies `PeerPresent` as privileged and a leaf
  client never subscribes, so the fixed-size parser is not an interop risk.

- **`NodeAttrClientSideReachabilityRouteCheck` + `net/routecheck`** (`2fbd30824`) — client-side
  route reachability checking. **Not applicable** — no counterpart subsystem; the attribute is
  ignored, which is the correct behaviour for a client that does not implement it.

- **Upstream's `encoding/json/v2` compatibility fixes** (`82cfea90c`) — upstream adjusted JSON
  serialization for Go 1.27's finalized `encoding/json/v2`. **Needs an audit, not a port**, and the
  audit is still not done: `ts_control_serde` hand-mirrors Go's PascalCase/`omitempty`/`omitzero`
  choices field by field, so any tag semantics upstream changed must be re-checked against the wire.
  Nothing observed to have broken. `b3c719581` bumped upstream's toolchain to Go 1.27.1 before the
  previous pin, so the v2 encoder is what upstream actually ships rather than what it was preparing
  for — which raises, not lowers, the value of doing the audit. The `UserProfile.Groups` row above is
  a reminder of the cheaper half of the same job: a field-by-field re-read of `tailcfg` against
  `ts_control_serde` finds omissions that no amount of tag-semantics reasoning will.

- **`NetInfo.HairPinning` was deleted upstream and is still modelled here** (`tailcfg`:
  `de733c595`, 2025-11-09, "tailcfg: kill off rest of HairPinning symbols") — **needs port**, where
  the port is a deletion, and it is the only row at this revision that comes from a commit inside the
  window — a commit a plain reading of the sweep should have produced at one of the previous six
  revisions.
  Upstream stopped populating `NetInfo.HairPinning` in May 2024 (`9eb72bb51`, #12205) and removed the
  last symbols at `de733c595`: the struct field, its `Clone`, its view accessor and its tests. At the
  pin the string `HairPinning` does not appear anywhere in `tailscale/tailscale`. It is gone from the
  wire type, so a Go client neither sends the key nor expects it.
  `ts_control_serde::NetInfo::hair_pinning` ([`ts_control_serde/src/net_info.rs`](ts_control_serde/src/net_info.rs))
  is still declared, with the old Go doc comment on it, as `Option<bool>` under
  `skip_serializing_if = "Option::is_none"`. **There is no divergence on the wire today** and the row
  says so plainly: nothing in this tree sets the field, so it is always `None`, so it is always
  absent — which is a field-by-field match with a Go client that no longer has it. What the row is
  about is the trap and the audit. The trap is that the only thing standing between this tree and an
  identifiable not-a-Go-client tell is that nobody has written to a `pub` field; a `NetInfo` carrying
  `"HairPinning"` is a key no Go client has sent since 2024. The audit is the more useful half: this
  was found by taking every `pub` field in `ts_control_serde` and checking its wire name against every
  identifier in upstream's Go, which is the mechanical form of the "field-by-field re-read of
  `tailcfg` against `ts_control_serde`" the `encoding/json/v2` row below has been asking for. Run in
  both directions at this revision, that check returns exactly this one phantom field outbound, and
  inbound returns `Node.ComputedName`/`ComputedNameWithHost` (the row below), `Hostinfo.RemoteConfig`
  (the unported half of `feature/remoteconfig`, already recorded) and `DNSConfig.TempCorpIssue13969`
  (upstream's own named-for-deletion field). That is a cheap check with a short answer, and it is now
  part of the recipe.

- **Peer display names are the FQDN, where Go computes three names and shows the short one**
  (`tailcfg`: `Node.InitDisplayNames`, `Node.DisplayName`, `Node.ComputedName`,
  `Node.ComputedNameWithHost`; `control/controlclient/map.go` and `direct.go`, which call
  `InitDisplayNames(magicDNSSuffix)` on the self node and on every changed peer) — **needs port**,
  narrow, and **status-surface only**: no peer and no control plane observes it, so a reader
  triaging this list should rank it accordingly.
  Upstream computes the display names on the *client*, not in control — the comment on the fields says
  so ("populated from controlclient (not from control)") — and the rule in `InitDisplayNames` is
  four steps. Trim the tailnet's MagicDNS suffix off `Node.Name`, giving the base name for an ordinary
  node and the whole FQDN for a shared-in node whose name is under a different tailnet. Take
  `dnsname.SanitizeHostname(Hostinfo.Hostname)` as the host name, and discard it when it matches the
  base name case-insensitively. If the base name came out empty, promote the host name into it, or
  fall back to the node key's string form when there is no host name either. Finally build
  `ComputedNameWithHost` as `"name (host)"` when a differing host name survived, and as the bare name
  otherwise. `DisplayName(forOwner)` then picks between the two — the owner of a node sees the form
  that disambiguates it, everyone else sees the short one.
  Here, `ts_runtime::status::StatusNode::from_node` ([`ts_runtime/src/status.rs`](ts_runtime/src/status.rs))
  sets one field by one rule: `node.fqdn_opt(false).unwrap_or_else(|| node.hostname.clone())` — the
  FQDN if a tailnet component is known, else the bare hostname — and its test asserts exactly that.
  So where `tailscale status` shows `laptop`, this fork's `StatusNode::display_name` shows
  `laptop.tailnet-name.ts.net`, and where Go disambiguates two nodes as `laptop` and
  `laptop (laptop-2)` this fork shows two FQDNs. `Node.ComputedName` and `Node.ComputedNameWithHost`
  are the two `tailcfg` fields with no counterpart in `ts_control_serde`, which is the other half of
  the same fact — and note they are `json`-tagged, so control *may* send them, but Go overwrites
  whatever arrives by calling `InitDisplayNames` itself.
  The port is `InitDisplayNames` as a function over the domain `Node`, called where the netmap is
  ingested rather than at each status read, plus the owner/not-owner choice on the way out. Carry the
  refusals, because they are the whole of the function: a host name equal to the base name
  case-insensitively is dropped rather than shown in parentheses, an empty base name promotes the host
  name instead of rendering as empty, and a node with neither falls back to the node key rather than
  to an empty string.

#### Not applicable, from code read at previous revisions and re-checked here

These are what earlier revisions' reading turned up that did **not** open a needs-port row; they are
written down for the same reason a closed row is, so the next revision can re-check the judgement
instead of re-deriving it. The first is the residue of the `controlknobs` read that produced seven
rows at the previous revision, and its tally **moved at this one**, because six of those seven
rows were ported in the interval.

- **The other twenty-one `controlknobs` knobs, and the node attributes outside that struct**
  (`control/controlknobs/controlknobs.go`; `tailcfg/nodecap/nodecap.go`) — **not applicable**, each
  for a reason this fork has already settled. Reading `Knobs` top to bottom against this tree gives
  twenty-seven knobs in three groups, and the first two groups changed size at this revision. **Six
  are now honoured**, up from one: `CacheNetworkMaps` with `DisableCacheNetworkMaps` taking
  precedence (§A row 135), plus `SilentDisco` (#445), `ForceBackgroundSTUN` (#443),
  `DisableDeltaUpdates` (#442), `DisableDNSForwarderTCPRetries` (#440) and `OneCGNAT` (#438) — the
  last two of those with the caveats recorded in the rows above, which is why "honoured" here means
  "read and acted on", not "ported cleanly". **None of the twenty-seven now has a live target and
  is ignored**, which is the first time that has been true. The one switch still in that category is
  not in `Knobs` at all: `disable-relay-client`, which upstream reads directly in `magicsock`
  alongside `only-tcp-443` (the latter now read here, via #448). The remaining
  twenty-one have **no target here**:
  - already §A rows, assessed there and not repeated: `DisableHostsFileUpdates` (132),
    `ForceRegisterMagicDNSIPv4Only` (133, the one open §A row), `EmitRuntimeMetrics` (139), the four
    GRO/GSO knobs (140), `NeverGSOEqualTail` (141), `ScopeQuad100OnMacOS` (145).
  - no subsystem to switch: `DisableUPnP` and `ProbeUDPLifetime` (no portmapper, no UDP-lifetime
    probing — *roadmap*), `PeerMTUEnable` (no path-MTU discovery on this datapath),
    `LinuxForceIPTables` / `LinuxForceNfTables` (`ts_host_net` installs no firewall rules),
    `AppCStoreRoutes` (no app connector — *roadmap*), `DisableCaptivePortalDetection` (no
    captive-portal detection), `DisableSplitDNSWhenNoCustomResolvers` (an iOS battery optimisation
    with no macOS/Linux arm), `DisableLocalDNSOverrideViaNRPT` (Windows NRPT; no Windows backend).
  - Go-internal shape rather than behaviour: `DisableSkipStatusQueue` gates whether queued
    `netmap.NetworkMap` values may be skipped between `controlclient` and `LocalBackend`, which is
    a Go channel discipline this actor-per-concern runtime does not reproduce.
  - `UserDialUseRoutes` — already a row of its own below (`UserDial` happy eyeballs), *not
    applicable* for the same reason: there is no `UserDial` here to route.
  - `RandomizeClientPort` is the one that needs a sentence rather than a line, because it looks
    applicable and is not. Upstream's knob makes magicsock bind `:0` *instead of a configured fixed
    port* (`wgengine/userspace.go:836`). This fork has no configured fixed port: `rebind_socket`
    binds ephemeral and only *prefers* the previous port across a re-bind, so that an advertised
    endpoint survives ([`ts_magicsock/src/sock.rs:97`](ts_magicsock/src/sock.rs)). There is nothing
    for the attribute to override. If a fixed-port option is ever added to `Config`, this knob
    becomes live in the same commit and should be ported with it.

  Outside `Knobs`, the attributes with no counterpart here are the ones the §A table and the *no
  counterpart* list already cover: the daemon/GUI surfaces (`disable-web-client`,
  `suggest-exit-node-ui`, `tailnet-display-name`, `auto-exit-node`, `native-ipv4`), the SSH incubator
  knobs (`ssh-behavior-v1`, `ssh-behavior-v2`, `ssh-aggregator`, `ssh-env-vars` — this fork's SSH
  server runs no incubator and accepts no `SendEnv`), `log-exit-flows` (*roadmap*, netlog),
  `store-appc-routes`, `disable-relay-server` (this node never serves as a relay), the Darwin
  socket-binding caps, and the debug caps `debug-no-wg-trim` (no lazy WireGuard config here) and
  `debug-disable-subnets-if-pac` (no WPAD detection).

- **`NetInfo` is reported with four of the twelve facets Go fills**
  (`wgengine/magicsock/magicsock.go`: `updateNetInfo`) — **not applicable**, on grounds this fork has
  already settled elsewhere. (The thirteenth field this tree models is `HairPinning`, and that one is
  a row of its own above.) Go builds a whole `tailcfg.NetInfo`
  out of each netcheck report: `MappingVariesByDestIP`, `UPnP`, `PMP`, `PCP`, `HavePortMap`, the
  `DERPLatency` map keyed `"<rid>-v4"`/`"-v6"`, `WorkingIPv6`, `OSHasIPv6`, `WorkingUDP`,
  `WorkingICMPv4`, `PreferredDERP` and `FirewallMode`, and hands it to a callback that ships it on the
  next map request — de-duplicated by `BasicallyEqual`, so an unchanged report is not re-uploaded.
  `ts_control::tokio::client`'s `CarriedNetInfo` carries four of those — `preferred_derp`,
  `derp_latency`, `working_udp`, `mapping_varies_by_dest_ip` — and applies the whole of it to every
  map request, which is Go's `hi.NetInfo = c.netinfo.Clone()` invariant and is already tested here.
  The eight it does not carry each have an answer already recorded in the *no counterpart here* list
  or in this fork's design: `UPnP`, `PMP`, `PCP` and `HavePortMap` need `net/portmapper`, which is
  roadmap; `FirewallMode` describes an iptables-vs-nftables choice `ts_host_net` never makes because
  it installs no firewall rules (the same reason capability version 136 is not applicable);
  `WorkingIPv6` and `OSHasIPv6` come from a netcheck that probes v6, and this fork's `ts_netcheck` is
  HTTPS-over-IPv4 only by deliberate anti-leak design — the note at the top of
  [`ts_netcheck/src/lib.rs`](ts_netcheck/src/lib.rs) records that a STUN prober binding a second
  socket was removed rather than left dormant; and `WorkingICMPv4` needs an ICMP socket this node does
  not open. `LinkType` is the one to *not* go looking for: upstream declares it and fills it nowhere,
  so leaving it absent is parity, not a gap. The thirteenth field this tree models, `HairPinning`, is
  a different case entirely and **is** a row — upstream deleted it, and the row is above.

- **Traffic-steering exit-node suggestion** (`ipn/ipnlocal/local.go`: `suggestExitNode`,
  `suggestExitNodeUsingTrafficSteering`; `net/traffic`; `tailcfg/nodecap`: `TrafficSteering`) —
  **not applicable**, and it is the tidier twin of the `net/routecheck` row below it. Upstream's
  `suggestExitNode` is a `switch` with two arms: when the self node holds the `traffic-steering`
  capability it ranks candidates by the priority scores in `net/traffic` (an FNV-based rendezvous
  hash over peers, memoised per netmap), and its `default` arm is `suggestExitNodeUsingDERP`, the
  latency-and-region algorithm this tree ports and which the previous revision's #417 finished by
  adding the per-region latency history. The capability is not implemented here and `net/traffic` has
  no counterpart, so this node takes Go's own default arm — which is the correct behaviour for a
  client that does not advertise the capability, exactly as with `client-side-reachability` and
  `net/routecheck`. Recorded because `net/traffic` is a package upstream added inside the window and
  a reader doing the new-package check will land on it: it is read by `ipn/ipnlocal`, which *is*
  mapped, so the check's question ("does anything already swept now read from it?") answers yes and
  the follow-up question ("and does that reading change a behaviour this tree implements?") answers
  no, because it is gated behind a capability this node never claims.

#### Not applicable, or already covered, from the commits new at the previous pin

Recorded so the next re-derivation does not re-read them. **No commit is new at this revision**: the
pin did not move, so this list is the previous revision's, carried unchanged and re-checked. It
covers the fourteen upstream commits that landed between `3945b82f8` and `023255e8a`, nine of them in
mapped packages, **none of which opened a row** — the first time that was true of a whole interval,
and now the second, for the stronger reason that there was no interval.

- **`net/socks5`: nil `Server.Logf`, and a data race on the UDP client address** (`de01da564`,
  `0301c7493`, with `f48358288` for the test) — **not applicable**, both, and for the same
  structural reason. The first is a nil-func hazard: `Serve` copied `Server.Logf` into each `Conn`,
  so the UDP error paths called a nil `func` instead of falling back to `log.Printf`. The second is
  a data race: `handleUDPRequest` wrote `Conn.udpClientAddr` on the reader goroutine while
  `handleUDPResponse` read it from one goroutine per target, now guarded by `syncs.MutexValue`.
  Neither has a target here on two counts. `src/loopback.rs` is a SOCKS5 **CONNECT-only** server —
  it implements no `UDP ASSOCIATE`, so there is no UDP client address to race over — and Rust has
  neither a nil function value nor an unsynchronized shared field that compiles. Worth re-reading
  only if UDP association is ever added; the field the race was on is exactly the state such an
  implementation would need.
- **`net/tstun`: handle a zero-value `stack.GSO.MSS`** (`5208e6d7f`) — **not applicable.** A guard
  for `stack.GSO.Type != stack.GSONone` with an MSS of zero. Same ground as capability version 140:
  there is no GRO/GSO offload on this datapath (`ts_transport_tun` is single-queue), so there is no
  GSO descriptor to carry a zero MSS.
- **`wgengine/netstack`: split outbound traffic into destination queues** (`2c30be2a4`) — **not
  applicable.** gVisor's writes are routed into dedicated WireGuard, host and loopback queues, each
  drained by its own goroutine, "preparing the outbound path for batching and multiqueue WireGuard
  delivery". It is internal concurrency shape with no observable behaviour change, and it is shape
  this fork does not share: `ts_netstack_smoltcp` hands outbound packets to the runtime through a
  `Channel` rather than through a gVisor link endpoint. Recorded rather than skipped because it is
  the first move in a batching series, and a later commit in that series may well change what goes
  on the wire.
- **`wgengine/wgcfg`: size crypto and per-peer queues by CPU factor** (`960cb5046`) — **not
  applicable as a port, but it names a live constraint here.** Upstream replaced fixed queue depths
  with CPU-proportional ones "to better balance throughput vs backlogged packet memory
  consumption", cutting peak RSS by 46–77% in its own benchmarks with throughput flat or up. There
  is no `wireguard-go` queue here to size — `ts_tunnel` is this fork's own implementation — so
  there is nothing to port. It lands next to a memory constraint this fork *does* have and
  documents elsewhere: `Config::tcp_buffer_size` is allocated eagerly per socket, so a forwarder
  carrying many concurrent flows pins memory in proportion to flow count with no auto-tuning. Both
  are the same trade read from two sides; upstream's answer (scale the bound to the host) is the one
  this fork has not taken.
- **`ipn/ipnlocal`: require a peerapi masquerade `Host` match only per address family**
  (`5186c3f41`) — **not applicable as a standalone fix, and folded into the masquerade row above.**
  `isAddressValid` rejected every non-masquerade destination whenever *any* masquerade address was
  set for the peer, so a v6-only masquerade pair 403'd the peer's dials to the native v4 peerAPI
  URL. There is nothing to fix here because there is nothing to be wrong: masquerade is decoded and
  never applied. It is written up above instead, because it is the shape the port must take if
  masquerade is implemented — the pre-fix reading of `isAddressValid` is a bug this fork would
  otherwise reproduce from scratch.
- **`net/dns`: export `OSConfigurationReadWarnable`** (`970cc199f`) — **not applicable.** The
  mapped-package hunk of an otherwise `tstest/natlab` commit: a health `Warnable` is exported so a
  VM test can take the warning's wording from it. There is no health tracker here (see the *no
  counterpart here* list) and `ts_host_net` has no `openresolv` backend, which is what the test
  covers.
- **`net/dnscache`, `feature/dnsresolvecache`: only persist resolutions after TLS verification**
  (`023255e8a`) — **not applicable**, and it is the new pin only because it was HEAD when this
  revision was derived. It tightens `aa2681ac5` (assessed at the previous revision) so a resolution
  is written to the on-disk cache only once it has passed TLS certificate validation, weeding out a
  lying resolver such as a captive portal's. The two reasons the parent commit had no target here
  are unchanged: this fork resolves control through control's own `DialPlan` and the system resolver
  with no answer cache in between, and upstream links the feature into `tailscaled` and deliberately
  **not** into `tsnet`.
- **`tstest/integration`: wait for the daemon to exit after killing it** (`7dfc149c8`) — **not
  applicable.** Upstream's own integration harness, and a daemon this library does not have.
- **Dependency and toolchain bumps** (`b658bd558` x/crypto + x/mod + x/tools, `7b0d5155c`
  wireguard-go, `3d3261b66` `go.toolchain.rev`, `653a95434` go-tool-cache) — **not applicable.**
  The `wireguard-go` bump is the one to keep half an eye on, per the Package mapping note: it is an
  upstream *dependency* that `ts_tunnel` re-implements, so it is tracked through `go.mod` rather
  than through the sweep. Nothing in this bump changes the handshake or the timers.

Carried from the previous revision and re-checked at this pin, because a reader of the TSMP rows
will otherwise go looking for them:

- **The gates on upstream's *periodic* TSMP disco advertiser no longer exist** (`ee76a7d3f` "do not
  send TSMP disco when connected", `92ab4866d` raising `discoKeyAdvertisementInterval` to two minutes,
  `be2f554dd` disabling the advert when netmap caching is off, `54005752a` suppressing it when
  `bestAddr` is a peer relay, over `c76113ac7`/`2d21dd46c`/`151644f64`) — **not applicable, and the
  reason is that upstream deleted the mechanism.** `3799eaf26` moved the send onto `wireguard-go`'s
  `SetPriorityMessageOnEstablishmentFunc`, and at this pin `PriorityMessageForPeer` is the whole send
  side: a zero disco key, an unknown endpoint, an invalid self node, an `isWireguardOnly` peer, no
  self address in the destination's family, or a marshal failure each return nil, and there is no
  timer and no knob left. That is exactly the set `ts_dataplane::DiscoAdvertisementState::advertisement_for`
  ports, which is why the "refusal for refusal" claim in the TSMP row still holds — re-checked against
  `wgengine/magicsock/magicsock.go` at `023255e8a`, not carried forward on trust.
- **`net/netcheck`: mark address family sendable on STUN response** (`92ec10267`) — **not applicable.**
  A race between `runProbe` recording `IPv4CanSend`/`IPv6CanSend` after `SendPacket` returns and a fast
  STUN response being processed first, which produced a report with a valid mapping but `CanSend`
  false and made magicsock rebind. `ts_netcheck` has no `Report` type and no `CanSend` fields — it
  measures DERP-region latency over HTTPS and deliberately runs no STUN prober of its own (see the
  note in `ts_netcheck/src/lib.rs`); production reflexive discovery is the disco pong harvest on
  magicsock's one bound socket. There is no second writer to race.
- **`disco`: `UDPRelayEndpoint.AddrPorts` slice cap math** (`94381a191`) — **not applicable.** An
  operator-precedence slip in the `make(...)` *capacity* argument of the decoder. `append` corrects an
  undersized capacity by reallocating, so no decoded value and no encoded byte changes; `ts_disco_protocol`'s
  relay codec allocates from the same length arithmetic done correctly.
- **`feature/acme`: trailing dot trimmed before cert lookup, one async renewal at a time**
  (`1a14668a3`, `2d98e7524`) — **not applicable.** Both act on state this fork does not keep. There is
  no cert *store* here to key by domain — `ts_control::cert::get_certificate` is fail-closed
  `Unimplemented` and `ts_control::acme::issue_certificate` is one-shot — so there is no lookup a
  trailing dot could miss (and `is_tailnet_name` trims one anyway), and no background renewal to
  de-duplicate. Re-read them if a cert cache is ever added; the trailing-dot rule is an SNI fact
  (RFC 6066 §3) that will apply the moment there is a keyed store.
- **`net/netmon`: skip `RTM_MISS` route messages on darwin** (`2767100bc`) — **not applicable**, for
  the same structural reason as `5927c1864` in the older list: `ts_netmon` ships `ManualLinkMonitor`
  and `NoopLinkMonitor` and no OS backend, so there is no PF_ROUTE reader to filter messages in.

#### Not applicable, from older commits read at previous revisions

Re-checked against this pin and against this tree; none moved.

- **`net/packet`: ICMP Destination Unreachable generation** (`8df4816be`) — **not applicable**.
  `GenerateICMPHostUnreachable` was added for the conn25 app connector, which returns an ICMP
  unreachable when it has no IP mapping for a client (`da51072b9`). This tree has no app connector of
  either generation, so nothing would call it; `ts_packet` has no ICMP *generation* surface at all,
  only decode.
- **`tka`: constant-time comparison of the disablement secret** (`34477cf3e`) — **not applicable
  today, but it names a constraint on work that is already planned.** Upstream's
  `State.checkDisablement` moved from `bytes.Equal` to `subtle.ConstantTimeCompare`. There is nothing
  here to convert: `ts_tka` implements `disablement_value` (the Argon2i KDF, pinned byte-for-byte
  against Go's golden vectors) but no `checkDisablement`, because disablement-secret *verification*
  is not implemented — [`docs/PARITY_ROADMAP.md`](docs/PARITY_ROADMAP.md) carries it as a deferred
  item. Recorded here so that when it is implemented it is implemented constant-time, rather than
  ported from a pre-`34477cf3e` reading of upstream.
- **`net/dns/resolver`: reach netstack-only upstreams over UDP** (`e1e5325c2`, `8bebdca90`) —
  **already covered, by a stronger rule.** Upstream's `sendUDP` opened a host-stack socket and
  ignored `UseNetstackForIP`, so in userspace-networking mode a split-DNS query aimed at a *tailnet*
  resolver blackholed until the TCP fallback answered; it now dispatches through the netstack dialer.
  This fork never had the host-socket path: `ts_runtime::magic_dns`'s forwarder sends every upstream
  query over the overlay netstack `Channel`, and says why in its own doc comment — it is an anti-leak
  invariant here, not a mode.
- **`ipn/ipnlocal`, `net/dns/resolver`: no bare-name resolution when MagicDNS is disabled**
  (`1ec348784`) — **not applicable**. Upstream's `nodeByFQDNLocked` now refuses a short name when
  `nm.DNS.Proxied` is false, because `nodeByName` intentionally holds both FQDNs and short names for
  other callers. This tree is stricter and structurally so: `magic_dns::decide` returns `REFUSED` for
  *every* query when `cfg.magic_dns` is false or the node does not accept the tailnet DNS config, and
  that one read site covers the netstack responder, the peerAPI DoH server and the TUN query path.
  There is no name of any shape that resolves through MagicDNS while it is disabled.
- **`wgengine/magicsock`: only send `callMeMaybe` when disco pings were actually sent**
  (`9be21088f`, then `2690d58e4`) — **deliberate divergence, recorded rather than ported.**
  Upstream briefly let a node on a cached netmap send a `CallMeMaybe` with no peer endpoints known,
  then reverted to gating on `sentAny`. This tree's `ts_runtime::direct::run_call_me_maybe` is a
  different shape entirely: a periodic sweep over peers with no confirmed `best_addr`, gated on *our*
  side having a STUN-discovered reflexive candidate to advertise, not on having pinged the peer.
  That gate is documented in place and is the safe direction — we never relay a `CallMeMaybe` that
  carries nothing a remote peer could act on, which is what upstream's `sentAny` gate is really
  protecting against. The deliberate part on the *peer* side is the opposite of upstream's: a peer
  whose netmap carried no DERP region is still prompted, via an inferred relay region, because
  without that the WireGuard floor came up over DERP and the direct upgrade was never attempted at
  all (the fork's own issue #24). Left as-is; named so it is not re-cut as a defect.
- **`wgengine/magicsock`: invalidate the endpoint on trust timeout** (`d3ba1480f`) — **already
  covered by construction.** Upstream cleared `bestAddr` on `trustBestAddrUntil` expiry only for
  udprelay paths, so a direct UDP path could stay selected-but-untrusted and blackhole; it now clears
  for both, and `handlePongConnLocked` switches to a working alternative when the held best is
  untrusted even if `betterAddr` would not have preferred it. Here `PeerPaths::best_addr` returns
  `None` the moment `trust_until` passes, so an untrusted best is never *used* in the first place,
  and the hysteresis in `select_best` applies only while the current best is still trusted. The
  failure mode the commit fixes cannot arise. (This is a different question from the disco-key
  invalidation row above, which is about a path that is still inside its trust window but was
  confirmed under a key the peer has since replaced.)

#### Carried unchanged from the previous revisions

The rows below were re-derived against this pin and against this tree and did not move. At this
revision that is **every** row this document already carried, with nothing closing and nothing
reopening, because no Rust changed in the interval and the pin did not move — see [What changed at this
revision](#what-changed-at-this-revision). The previous revision's one close (the upstream-DNS race
row) is under [Closed at the previous revision, kept in
full](#closed-at-the-previous-revision-kept-in-full).
They are kept in full because a row whose evidence is elided is a row the next re-derivation has to
redo, and because "re-derived and unchanged" is only a checkable claim when the evidence is still on
the page.

- **DNS is still configured when router programming fails** (`wgengine`: `cfd101f9d`) — **not
  applicable: deliberate divergence, and it should stay one.** Upstream's `Reconfig` returned on
  any `router.Set` error before its DNS block ran, so a host where route programming always fails
  never learned about MagicDNS at all; upstream now records the router error, still calls
  `dns.Set`, and joins the errors. This tree does the opposite on purpose: `ts_runtime::tun_actor`
  logs `"host route programming failed; TUN idle (fail-closed)"`, tears the host state down and
  returns, so the interface never carries traffic it cannot route. Pointing the host resolver at
  `100.100.100.100` while the TUN is being torn down would point it at an address nothing answers
  on. The fail-closed invariant outranks parity here — see the quality bar's rule 5 — and the
  Linux half of the same commit (per-interface IPv6 gating in `wgengine/router/osrouter`) is
  independently not applicable: `ts_host_net` programs routes and DNS and installs no netfilter
  rules. Recording the divergence rather than porting it.
- **SSH `acceptEnv` hardening** (`ssh/tailssh`: `651049ec1`, `9d48dbd56`) — **not applicable**, and
  the reason is structural. Upstream rejects `LD_*`/`DYLD_*` in `acceptEnv` filtering and keeps
  accepted variable names and values off the incubator command line. This fork's SSH server never
  reaches that hazard: `src/ssh/shell.rs` builds the child environment with `env_clear()` plus a
  fixed six-variable allow-list (`HOME`, `USER`, `LOGNAME`, `SHELL`, `PATH`, `TERM`), so no
  client-supplied variable — dangerous or benign — is ever placed in the shell's environment, and
  there is no incubator process whose argv could carry one. The visible divergence is that the
  policy's `acceptEnv` is modelled here — `ts_control::ssh_policy`'s `SshRule::accept_env`, carried
  through onto `SshAccept::accept_env` — and then deliberately never applied. That is a scope
  decision in the safe direction, named here so it is not re-cut as a defect.
- **Digit-only SSH usernames refused** (`ssh/tailssh`: `f368a96e0`) — **not applicable**, narrowly.
  Upstream rejects a purely numeric SSH username with a banner because Go's user lookup falls back
  to resolving a numeric string as a UID, making `ssh 0@host` ambiguous with root. This tree's
  `resolve_user` calls `getpwnam` only and has no numeric-UID fallback, so a digit-only name
  matches nothing and already fails closed before a shell is spawned. The ambiguity the refusal
  exists to close cannot arise; what differs is only the message the client sees.
- **`net/netmon`'s `InterfaceIPDisappeared` predicate** (`5927c1864`) — **not applicable**.
  Upstream fixed a reversed predicate that reported addresses which had *appeared* as having
  disappeared. `ts_netmon` exposes no `ChangeDelta` equivalent — it emits a debounced link-changed
  signal and nothing that answers "which address went away" — so there is no predicate here to be
  reversed.
- **`ipn/store`: `WriteState(id, nil)` deletes the key** (`7355116c0`) — **not applicable**.
  Upstream's stores wrote a nil value into their cache map, so a later `ReadState` returned
  `(nil, nil)` instead of `ErrStateNotExist` and a reset node could not log back in. The bug needs a
  nil/absent ambiguity to exist. `StateStore::write_state` (`src/tsnet.rs`) takes `&[u8]`, which has
  no nil, and `read_state` returns `Option<Vec<u8>>`, which distinguishes absent from present; the
  single call site writes a serialized identity blob and never an empty slice.
- **`feature/identityfederation`: query parameters stripped from the client ID** (`34e992f59`) —
  **already covered**. Upstream was sending the whole `tskey-client-…?ephemeral=…` string as the
  OAuth `client_id` in the JWT-for-token exchange, and now sends the part before the `?`.
  `ts_control::wif` has always split the secret at the first `?` into a `stripped` value plus its
  parsed attributes, and `token_exchange_body` takes that stripped id.
- **SOCKS5 proxy credentials compared in constant time** (`net/socks5`: `60576f8bd`) — **still needs
  port**, and it is now by a wide margin the oldest open row here: it has survived four revisions of
  this ledger unchanged. Upstream's SOCKS5 server checked the client-supplied
  username and password with plain string equality, which returns on the first differing byte, and
  replaced both with `subtle.ConstantTimeCompare`, evaluating both halves so the username result does
  not gate whether the password is examined. The same asymmetry exists here: `src/loopback.rs`'s
  `negotiate` does
  `uname.as_slice() == PROXY_USERNAME.as_bytes() && passwd.as_slice() == cred.as_bytes()` — two
  data-dependent comparisons, the second short-circuited by the first. The threat model transfers
  unchanged: `gen_cred` mints a 16-byte random credential that gates every dial into the tailnet, the
  listener is on `127.0.0.1`, and any local process may retry without limit, so a reject that is
  timeable leaks the credential a byte at a time. No new dependency is needed —
  `src/tsnet.rs`'s `localapi::cred_ok` is already a constant-time comparison, so the SOCKS5 path is
  the one place on the loopback that does not use it. Host-facing, not wire-facing.
- **`tsnet.Server.HTTPClient` carries `http.DefaultTransport`'s settings** (`tsnet`: `49e148c4a`,
  then `d9cc55e33`) — **still needs a port** (narrow, host-facing), unchanged at this pin.
  `49e148c4a` stopped returning `&http.Client{Transport: &http.Transport{DialContext: s.Dial}}` and
  *cloned* `http.DefaultTransport`; `d9cc55e33` undid the clone a day later, because an application
  is permitted to replace or mutate the package-level `http.DefaultTransport` and cloning it made
  `HTTPClient` inherit whatever an embedder had done to a global. Upstream now spells the transport
  out as a literal and pins the settings by hand: `ForceAttemptHTTP2: true`, `MaxIdleConns: 100`,
  `IdleConnTimeout: 90s`, `TLSHandshakeTimeout: 10s`, `ExpectContinueTimeout: 1s`,
  `DialContext: s.Dial`, and no `Proxy` — with a comment telling the next reader to keep it in sync
  with `http.DefaultTransport` by hand.
  So the port is a fixed list — five settings, plus the tailnet dialer this tree already installs —
  to decide about one at a time. The hazard upstream backed out of (a mutable process-global leaking
  into a tailnet client) is one this tree never had, because `hyper_util`'s builder has no such
  global; the divergence is only that `Server::http_client` (`src/tsnet.rs`) takes the builder's own
  defaults rather than Go's chosen ones. The `Proxy = nil` half stays structurally true:
  `TailnetConnector` dials the overlay directly and has no environment-proxy path to disable.
  Upstream's `TestHTTPClientDefaultTransport` fails on any unrecognised future field and asserts that
  `TLSClientConfig`, `TLSNextProto` and `HTTP2` are *nil*; that test shape is the one worth copying,
  because it forces a decision instead of drifting.
- **`Dialer.Close` no longer touches the peerapi transport when omitted** (`net/tsdial`:
  `72780705e`) — **not applicable**. The bug is that Go's `Dialer.Close` called `PeerAPITransport()`
  unconditionally, which panics in a binary built with the `ts_omit_peerapiclient` build tag. There
  are no build tags here and no equivalent unconditional accessor; the peerapi client is ordinary
  Rust state whose absence is an `Option`, not a panic.
- **`Sys.ExtraRootCAs` plumbed through the TLS dial paths** (`net/tlsdial`: `a182b864a`) —
  **already covered**, and by an older mechanism than upstream's. `ts_tls_util` builds its
  `RootCertStore` from `webpki_roots::TLS_SERVER_ROOTS` and additively loads extra trust anchors from
  the PEM file named by `TS_RS_EXTRA_CA_PEM`, which is the same capability reached by configuration
  rather than by a `tsd.Sys` field. Failure to load is logged and non-fatal, so a bad path cannot
  silently weaken trust — it surfaces as a handshake error.
- **LetsEncrypt Generation Y roots (`YE`, `YR`)** (`net/bakedroots`: `f65372c9b`) — **not
  applicable as a port**, but it names a real maintenance obligation. Go bakes a hand-curated root
  list into the binary because a Go client cannot rely on the OS trust store everywhere; this tree
  has no such list to append to, because `webpki-roots` *is* the compiled-in bundle and tracks
  Mozilla's set on the crate's own release cadence. The obligation upstream discharges by editing
  `bakedroots.go` is discharged here by keeping that dependency current, which is a `cargo update` in
  its own PR (see [`CONTRIBUTING.md`](CONTRIBUTING.md#dependencies)), not a code change. A stale
  `webpki-roots` is the failure mode this row exists to name: it looks like nothing until a CA
  rotates and control or DERP stops verifying.
- **`UserDial` happy eyeballs, and `UserDialPlan` for non-Tailscale addresses** (`net/tsdial`:
  `f3a117e81`, `0e10a3f58`) — **not applicable as the tree stands**. Both are about `tailscaled`
  dialling *on behalf of a local user process*: racing A and AAAA candidates with a 300 ms delay when
  userspace networking sits behind an exit node, and letting the LocalAPI `/dial` handler tell a
  client to dial a non-Tailscale address itself. Neither has a target here. This fork's overlay is
  IPv4-only by default and its MagicDNS resolver returns a single `Option<Ipv4Addr>`
  (`loopback::Resolver`), so there is no second address family to race; and the one-route LocalAPI
  serves no `/dial`. The first would become live if IPv6 MagicDNS lands — it pairs with row 133 and
  `Config::enable_ipv6`, and is noted here so that port is not written IPv4-shaped a second time.

Deliberately **not** listed: upstream refactors with no observable behaviour (the
`tailcfg/{nodecap,selfcap}` package split, `DERPRegionID` typing, `NodeMutationAdd` →
`NodeMutationUpsert`, the `feature/` build-tag reorganization, removal of `LazyWG` and the engine
watchdog, `types/netmap` field removals), and upstream-internal locking/allocation fixes in
`control/controlclient`, `derp/derpserver` and `ipn/ipnlocal` (including `886d1b2e6`, `5be05f2c0`,
`d64aaffc0` and `e32b9bde1`, all of which are Go concurrency shape rather than wire behaviour). From
the packages earlier revisions added to the sweep: the tree-wide renames and modernizers that touched `net/socks5`
(`bd2a2d53d`, `2810f0c6f`, `3ec5be3f5`, `c2e474e72`) and the `net/tsdial` commits that only follow
upstream's own refactors of `types/netmap`, `netmon` and `syncs`. `wgengine/router`'s Linux
netfilter, `ip rule` and connmark work (`ts_host_net` installs no firewall rules),
`wgengine/wgcfg`'s removal of `Peers` from its config struct and the `wireguard-go` bumps that go
with it, `ssh/tailssh`'s exit-status framing and incubator test fixes, `feature/acme`'s per-domain
locking, and `ipn/ipnlocal`'s locking and delta-path rework. Also out, from earlier
pins: `91d10d38a` (`net/portmapper`, a package with no counterpart here — *roadmap*), `99f1ee74b`
(`feature/conn25`, likewise), the `fuzz:` and `go.toolchain.rev` housekeeping, `9ea7cba44` (a
licence-notice regeneration), `a8b023c06` (a `cmd/k8s-operator` change) and `3945b82f8` (`paths`, the
daemon's default socket path on Android), the four previous pins. `023255e8a` — the pin for three
revisions and now the *previous* pin — was likewise only what HEAD was when it was first derived: it
tightens an on-disk DNS cache this fork does not keep, and is written up with the other commits
above. `e2ed43239`, the pin at this revision, is the same kind of commit: a `go.toolchain.rev` bump,
out of scope in itself, and the pin only because it was HEAD when the window was re-derived. Also
out, from the package the sweep list gained at the previous revision: `sessionrecording`'s
`7f3bbc986` (a `net/netutil` helper), `5ef3713c9` (a `cmd/vet` analyzer) and `3ec5be3f5` (the
`AUTHORS` removal), all three tree-wide commits that touch that package only incidentally; its four
substantive commits are assessed above.
`tsnet` has over a hundred commits in the window and is **not** re-derived here: that facade has its
own line-by-line parity matrix in [`docs/TSNET_PARITY.md`](docs/TSNET_PARITY.md), and duplicating it
into this ledger would create two records that disagree. Only `tsnet` changes that alter behaviour a
mapped crate already implements are pulled in, as `49e148c4a` and `d9cc55e33` were above.

### Re-deriving this ledger

```sh
# The capability-version window (§A): everything above CapabilityVersion::CURRENT here.
git -C <tailscale-go> grep -n 'CurrentCapabilityVersion CapabilityVersion' e2ed43239 -- tailcfg/tailcfg.go
git -C <tailscale-go> grep -nE '^//[[:space:]]*-[[:space:]]*1[3-9][0-9]:' e2ed43239 -- tailcfg/tailcfg.go

# What upstream touched per mapped package since capver 130 landed (§B). Every upstream package
# named in "Package mapping" is in this list; parent paths (wgengine, ipn) are used where the
# mapping names several children, so a subdirectory upstream adds later cannot fall outside it.
for p in tailcfg disco derp net/packet net/tstun net/netcheck net/stun net/dns \
         net/udprelay net/socks5 net/tsdial net/tlsdial net/bakedroots net/netmon net/art \
         net/routemanager \
         control/controlclient control/controlbase control/controlhttp control/controlknobs \
         wgengine ipn tsd tka types/key types/persist tsnet \
         feature/remoteconfig feature/identityfederation feature/taildrop feature/ssh \
         feature/acme ssh/tailssh sessionrecording util/clientmetric tstime tstest tool/; do
  echo "== $p"; git -C <tailscale-go> log --since=2025-10-06 --oneline -- "$p"
done

# Only what moved since the pin this ledger currently carries — the fast path on a re-derivation
# that follows soon after the last one. Read it *in addition to* the full sweep, never instead of
# it: a row's assessment can change because this tree moved, with upstream perfectly still, the
# sweep list itself can be wrong (it has been, five times), and this range can be EMPTY (it has been
# at most of the recent revisions, this one included) without the ledger being finished. Three
# revisions ago it was seven commits and supplied one of five new rows; it has been empty at the three
# since. Two of those took every new row from re-reading merged ports here against upstream; THIS one
# had no merged port either, and took all three from reading an enumeration nobody had opened.
# A non-empty delta is not a licence to skip the reading either.
git ls-remote https://github.com/tailscale/tailscale HEAD   # <new-pin>; may already equal the pin
git -C <tailscale-go> log --oneline e2ed43239..<new-pin>

# And the mirror image of that: what moved *here* since the tree revision the header table names.
# Three revisions ago it was TWENTY-THREE commits, six of which closed rows; two revisions ago ONE,
# that document's own rewrite; at the previous revision FOUR, of which two were code and both in one
# module; at THIS revision it is TWO and NEITHER IS CODE. Read it against the open rows first -- that
# is the cheapest way to find a row that closed -- and then check that every port in it has been read
# against upstream, in PARITY_AUDIT.json or under "Read at this revision" in §B. A port that closed a
# row and was never audited is where a row that closed INCOMPLETELY hides: two revisions ago that
# audit produced the one new row, and at the previous revision it produced all three.
# WHEN THIS RANGE HAS NO CODE IN IT, THE AUDIT SOURCE CANNOT FIRE AT ALL, and the revision's whole
# yield has to come from reading. Budget for that when you see an empty-looking delta; this revision's
# three rows came from one enumeration, not from the log.
git log --oneline 0c8796a..HEAD

# Wire types, both directions. Cheap, mechanical, and it found two rows two revisions ago after six
# revisions of not being run. Re-run at this pin it returns the one known phantom and nothing new.
#
# Outbound: resolve every `pub` field in ts_control_serde to the wire name it actually serializes
# under -- its #[serde(rename = "...")] if it has one, else the PascalCase of the field name -- and
# look that name up in upstream's Go. A name upstream no longer has is a field this tree models
# alone (NetInfo.HairPinning was found this way).
grep -rhoE '#\[serde\((.*)rename = "[^"]+"' ts_control_serde/src | grep -oE '"[^"]+"$' | tr -d '"' \
  | sort -u | while read -r n; do
      git -C <tailscale-go> grep -qw "$n" e2ed43239 -- '*.go' || echo "phantom: $n"
    done
# The un-renamed half is the one that matters and it is NOT optional: the renamed-field pass above
# returns nothing at this pin, and the one phantom this tree has -- NetInfo.HairPinning -- is an
# un-renamed field whose wire name is the PascalCase of its Rust name. Do that derivation explicitly
# (hair_pinning -> HairPinning), and when you automate it, do not "skip fields with a rename" by
# grepping a few lines of context for the word `rename`: net_info.rs has that word in a *comment*
# six lines above the field, which is enough to make a naive filter skip the only true positive.
# The cheap manual form, per module:
#   grep -nE '^\s*pub [a-z_]+\s*:' ts_control_serde/src/<mod>.rs
# Look the derived name up CASE-INSENSITIVELY. Go spells acronyms in capitals -- DERPMap, AllowedIPs,
# NodeID -- so a case-sensitive lookup of the PascalCase derivation reports about sixty phantoms that
# are not.
# AND DO NOT SCOPE THE LOOKUP TO 'tailcfg/*.go' 'tka/*.go', which is what this recipe said until this
# revision. tailcfg EMBEDS wire types upstream defines in sibling packages -- types/dnstype,
# types/opt, types/views, types/ipproto, types/tkatype, types/structs -- so a field that reaches the
# wire inside a tailcfg message can have its Go counterpart outside tailcfg. Scoped that way at this
# pin the walk reports a THIRD phantom, BootstrapResolution, which is not one: it is
# dnstype.Resolver.BootstrapResolution (types/dnstype/dnstype.go:37), modelled field for field by
# ts_control_serde::dns::Resolver (dns.rs:103). Search '*.go' and let the case-insensitivity do the
# work. So scoped, at this pin it reports HairPinning (the known row) and two false positives:
# action_type, renamed to "Type", and Endpoint::ty, which reaches the wire only as MapRequest's
# parallel Endpoints/EndpointTypes arrays.

# Inbound: the reverse -- every json-tagged field of tailcfg's structs checked against those same
# wire names. A name this tree lacks is a field control may send that nothing here reads.
git -C <tailscale-go> show e2ed43239:tailcfg/tailcfg.go   # plus derpmap.go, tka.go, c2ntypes.go

# And the coarser version of the same question, which is where five rows started two revisions ago:
# a `pub` field in ts_control_serde whose name appears nowhere else in this workspace is a field this
# node decodes and drops.

# Node attributes, the same question for control's switches rather than for its fields. Every
# constant in tailcfg/nodecap is a string control can set on this node; one that appears nowhere in
# this workspace is a switch this node ignores. Most misses are correct -- check each against the
# capability-version table and the *no counterpart here* list before opening a row. (This found
# magicdns-aaaa at this pin.)
# NOTE the character class: the first version of this command used '"[a-z0-9-]+"', which silently
# skipped every attribute whose key contains ?, =, :, . or an uppercase letter -- one-cgnat?v=true,
# one-cgnat?v=false, linux-netfilter?v=iptables, linux-netfilter?v=nftables, drive:share,
# drive:access, tailnet.maxKeyDuration and every URL-form cap. one-cgnat is a real row and six
# revisions of this walk never reported it. Widen the class; do not narrow it back.
git -C <tailscale-go> show e2ed43239:tailcfg/nodecap/nodecap.go | grep -oE '"[A-Za-z0-9?=:./_-]+"' \
  | tr -d '"' | sort -u | while read -r cap; do
      grep -rqI --include='*.rs' -- "\"$cap\"" . || echo "unhandled node attribute: $cap"
    done
# And read the HANDLED side with the same suspicion, because a bare string match is not a handler:
# at this pin "https", "full" and "foo.com" all come back handled and none of them is an attribute
# this node acts on. Only six attributes are genuinely read here: cache-network-maps,
# disable-cache-network-maps, dns-subdomain-resolve, funnel, service-host, suggest-exit-node.
```

The capability-history pattern is deliberately whitespace-tolerant: upstream writes those entries as
`//   - 133: …`, but the exact indentation is a comment convention, not something `gofmt` enforces,
and a pattern that pins it would go silently empty the day it changes. Check the row count rather
than trusting the exit status — at the pinned commit the second command returns **18 lines**, 130
through 147, i.e. the seventeen-version window of §A plus the 130 row that anchors it. Three
revisions derived **17** against a pin that never moved; the revision after that derived **18** when the
pin moved by one version; the two since, this one included, have derived **18** again against the
same pin. That is why counting is the whole of the check rather than a formality:
when the pin does not move, "upstream added nothing", "I re-ran it against the same tree" and "the
pattern broke" all produce the same *feeling*, and only the count tells them apart. An empty or short
result means the pattern broke, not that upstream added nothing.

**The same argument applies to the sweep loop's own count, and the discrepancy the previous revision
opened is closed at this one by a third run.** The previous revision recorded **854** against the
**857** the revision before it had recorded at the identical pin, could not reconstruct either, and
ruled `TZ` out (`--since` is interpreted in the local zone; UTC and CEST both give 854, while
`America/Los_Angeles` gives 845, a much larger step than three). Re-run at this revision the loop as
printed above returns **854** again. Two agreeing derivations against one, so 857 is the outlier and
the number to carry is 854. Nothing in §B ever turned on it; what the episode bought is the line of
discipline that resolved it — **when a derivation records a count, record the command that produced it
exactly** — and the demonstration that the only way to settle a count dispute is to run the recorded
command again, not to reason about what might have differed.

**Three of the commands above have been wrong: two were rewritten two revisions ago, and the third was
widened at the previous revision.** They are
called out here rather than only in the comments, because a command that returns a plausible short
answer is worse than one that fails: nobody re-checks it. All three behaved as intended when they were
last run, and the first of them is why two revisions ago the ledger could see six rows close at all — the six
attributes those ports added include two `one-cgnat?v=…` keys that the narrow class could not match.

1. **The node-attribute walk's character class was too narrow.** `'"[a-z0-9-]+"'` matches only
   attributes whose key is lowercase letters, digits and hyphens, and upstream has fourteen that are
   not: the four query-string forms (`one-cgnat?v=true`, `one-cgnat?v=false`,
   `linux-netfilter?v=iptables`, `linux-netfilter?v=nftables`), the two `drive:` forms,
   `tailnet.maxKeyDuration`, and the URL-form caps. The walk was added one revision ago and reported
   forty-seven unhandled attributes both times it was run; it never reported `one-cgnat`, which became a
   row in §B two revisions ago. The lesson generalises past this one command: **a filter written to
   match the examples in front of you is a filter that will not report the thing it was written to
   find.** Derive the class from the type's documented grammar, not from the values you happened to
   read.
2. **The outbound wire-name check only ever ran on half its input.** As written it walks fields with
   an explicit `#[serde(rename = …)]`, and at this pin that half returns *nothing*. The one phantom
   this tree actually has — `NetInfo.HairPinning`, opened as a row one revision ago — is an
   un-renamed field, found by the un-renamed half that the recipe mentioned in a parenthesis and did
   not spell out. Worse, the obvious way to automate the un-renamed half (skip a field if the lines
   above it mention `rename`) skips `hair_pinning` specifically, because the word appears in a
   comment six lines above it. The recipe now spells the derivation out.
3. **The un-renamed half's lookup was scoped to the wrong packages, and that is new at this
   revision.** It searched `'tailcfg/*.go' 'tka/*.go'`, on the reasonable-sounding theory that the
   outbound wire types are `tailcfg`'s. They are not all `tailcfg`'s: `tailcfg` imports
   `types/dnstype`, `types/opt`, `types/views`, `types/ipproto`, `types/tkatype` and
   `types/structs`, and embeds their types in the messages it defines. So the walk reported
   `BootstrapResolution` as a field this tree models alone, when it is
   `dnstype.Resolver.BootstrapResolution`. A false *positive* is cheaper than the false negatives
   above — it costs a reader an hour, not a row — but it is the same failure: **the filter was
   written from where the author expected the answer to be, not from where the type system says it
   can be.** Search `'*.go'`.

**The sweep list is part of the ledger, and it has now been wrong five times.** The rule that
governs it has not changed since it was written down: when [Package mapping](#package-mapping)
gains an upstream package — a table row or a *partial* entry alike — add it here too, or the mapping
is a claim the sweep never checks. Four revisions ago the list gained `net/socks5`, `net/tsdial`,
`net/tlsdial`, `net/bakedroots`, `ipn/localapi`, `feature/remoteconfig` and `tsnet`; the revision
after that found it short by sixteen more and rebuilt the loop from the mapping rather than
extending it by hand (`net/netmon`, `net/art`, `control/controlhttp`, `types/persist`,
`feature/identityfederation`, `feature/taildrop`, `feature/ssh`, `feature/acme`, `ssh/tailssh`,
`util/clientmetric`, `tstime`, `tstest`, `tool/`, `tsd`, and — the consequential ones — the parent
paths `wgengine` and `ipn`).

**The third miss was the most instructive of the first three, because the rule as written would not
have caught it.** `net/routemanager` (`a5102d3fc`, 2026-07-09) is upstream's incremental route
manager: it took over the per-peer data-plane attributes that used to be derived inside
`wgengine/wgcfg`, and `net/tstun`'s `peerConfigTable` now reads `PeerRoute.Jailed`,
`PeerRoute.MasqAddr4` and `PeerRoute.MasqAddr6` out of it. The mapping had no row for it, so the
loop had no entry for it, so four commits' worth of behaviour sat outside the sweep for two months.
The generalisation worth carrying forward: the rule "add a package to the loop when the mapping gains
one" only fires when someone *notices* the mapping should gain one. **Upstream splitting an existing
package is the case that slips**, because nothing about the mapping looks stale — `wgengine` was
already in the loop, and the behaviour simply walked out of it. The check that would have caught this
is cheap and is part of the recipe: `git -C <tailscale-go> log --diff-filter=A --oneline
<old-pin>..<new-pin> -- '*/*.go'` for new top-level packages, and for each one, ask not "does it have
a counterpart here" but "does anything *already swept* now read from it".

**The fourth miss looked like it did not pay, and the fifth revision found that it had.**
`control/controlknobs` is upstream's single struct of the client behaviours control can switch on and
off. The previous revision added it by the rule, read its fifteen commits in the window, found every
one already assessed, and concluded that "a mapping row that is honest is worth adding even when
adding it pays nothing." Reading the **struct** instead of its **commits** opened eight rows at this
revision. Both halves of that are worth keeping. The conclusion was wrong, and the reason it was
wrong is a distinction this document had not drawn before: sweeping a package asks *what changed in
it*, and for a package whose whole content is a list of switches, nothing changing is not the same as
nothing being owed. **When a package's purpose is to enumerate something, read the enumeration, not
its history.** `controlknobs` is one such package; `tailcfg/nodecap` is the other, and `tailcfg`'s
capability-version comment is a third.

**The fifth miss is the one to be embarrassed by, because the entry had been considered and written
out.** `sessionrecording` has had a [Package mapping](#package-mapping) row since it moved out of the
*no counterpart* list four revisions ago, and the previous revision recorded, in §B, that it "needs no
loop entry of its own, because the client half lives behind `ssh/tailssh`'s calling code, which is
swept." One command falsifies that: `git log --since=2025-10-06 --oneline -- sessionrecording`
returns seven commits, and not one of them appears in `ssh/tailssh`'s log for the same window. Being
*called from* a swept package does not put a package's commits in a swept package's log — nothing
about Go's import graph makes that true, and the sentence reads as though it should be. All seven
assess *not applicable* (they are written up in §B), so the cost this time was zero and the rule the
episode establishes is not: **the only admissible reason to leave a mapped package out of the loop is
that it has no upstream path to sweep.** `golang.zx2c4.com/wireguard` qualifies — it is a dependency,
not a directory in this repository. Nothing else does. If a package is in the mapping and has a path,
put the path in the loop and let the assessments come out *not applicable*; that costs one line of
output per revision and it is re-checkable, which an argument is not.

**A sixth candidate was chased two revisions ago and is not a miss — but only by luck, and the luck is
worth naming.** Fixing the outbound wire-check's lookup scope (above) surfaced that `tailcfg` embeds
wire types from six sibling packages, none of which has a loop entry: `types/dnstype`, `types/opt`,
`types/views`, `types/ipproto`, `types/tkatype`, `types/structs` — and `types/netmap`, which is the
struct `ipn/ipnlocal` hands the engine. By the `net/routemanager` rule that is the exact shape of a
miss: the mapping names `tailcfg`, the behaviour lives next door. Checked commit by commit against the
sweep's own output, it pays nothing: every commit in the first six is already in the sweep because it
also touched a swept path, and of `types/netmap`'s nineteen only three are not — `323198b3`
(`envknob/logknob` removal) and `4a832d8d`/`618dfd40`, a LocalAPI services-map key format and its
revert, both *not applicable* to a fork whose LocalAPI serves one route. The reason it pays nothing is
that upstream almost never touches those packages alone, which is **a property of upstream's commit
habits, not of the sweep list**. Adding the entries would cost seven lines of empty output per
revision and would stop this from being luck; the argument for not adding them — that they are type
definitions rather than behaviour — is the same argument that was wrong about `controlknobs`. Left out
here, with the evidence recorded, so the next revision decides it on facts rather than re-deriving
them.

New-package runs, for completeness. With the pin unchanged the `--diff-filter=A` check over
`<old-pin>..<new-pin>` is empty by construction, so the whole-window form (`--since=2025-10-06`) from
the previous revision stands unchanged and is not re-derived here: it returned about two hundred
directories, roughly a hundred genuinely new packages, almost all of them `cmd/`, `k8s-operator`,
`gokrazy`, `tstest` and `feature/` surfaces already recorded as out of scope. The three worth the
question "does anything already swept now read from it" were `feature/captiveportal`,
`net/routecheck` and `net/traffic`, all three already carried as rows or as *no counterpart*
entries, and `net/porttrack` is a test helper nothing imports. The five packages recorded as new and
deliberately unswept stand: `net/connreject` and `feature/connreject` (`85c1efb46`),
`feature/androidbin` (`60d9c54b6`), `feature/dnsresolvecache` (`aa2681ac5`) and `feature/androiddns`
(`86b3cd5aa`). All five are documented under Package mapping as having no counterpart, and — per the
rule the fifth miss establishes — they stay out of the loop because the sweep exists to catch
behaviour a mapped crate already implements, not because anything else covers them.

`wgengine` was the lesson for the sweep list. **The lesson the previous revisions drew — that a
package being *in* the loop does not mean its commits have been *read* — stopped being a warning two
revisions ago and became rows; two revisions ago it acquired a sharper form, and at THIS revision a
sharper one still — see the note after the five techniques below.** That revision
found two rows from commits inside the window and inside a swept package that six revisions had read
past (`de733c595` removing `tailcfg.NetInfo.HairPinning`; `Node.InitDisplayNames`, which never
changed at all). The previous revision found seven from a package that was swept, whose commits
were all correctly assessed, and whose *contents* nobody had opened. **A swept package is not a
read package, and a read commit log is not a read package either.**

**And when the upstream delta is empty, that is not a signal to do less reading — it is the revision
where the reading is the whole job.** Budget for it, not just for the `git log`. Across the consecutive
revisions at which the pin has not moved at all, the ledger gained eight rows, then nine, then one,
then three, and now three again. At the previous revision the *tree* delta was four commits of which
two were code, in one module, and those two commits produced every row. **At this revision the tree
delta contains no code at all, and the yield was the same three.** That is the case worth planning for,
because it is where the shape of the work changes rather than its size: with nothing to diff and
nothing to audit, every row has to come from opening something nobody had opened. Pick an enumeration
and read it. **Size the reading by what the document claims to have finished, not by how big the diff
is** — and when the diff is empty, by what it has never looked at.

Five techniques have paid, and at this revision the fifth produced every row while the first four
produced none. **Say plainly which were re-run, because "re-checked" and "re-read" are not the same
claim.** Techniques 1, 2 and 4 read *this tree* against *upstream at the pin*, and at this revision
neither side changed a byte — so re-running them could not return anything the previous revision had
not already recorded, and they were not re-derived. Technique 3's mechanical half (the
`tailcfg/nodecap` grep) **was** re-run, as were all four commands under the recipe above, and every
count matched; its judgement half was not re-asked. Technique 5 was the work of this revision. A
future revision at a moved pin owes all five a re-run; a revision at a still pin owes an explicit note
of which it skipped and why, which is this paragraph. They are kept in full because their yield is
lumpy, not because it is zero:

1. **Take a wire field this tree decodes and follow it forward.** A field modelled in
   `ts_control_serde` that reaches nothing. A `git grep` for the field name finds it and looks like
   evidence of a port. The question that separates the two is "what *reads* this", and it has a
   mechanical form: take every `pub` field in `ts_control_serde` and grep the workspace, excluding
   that crate, for its name. Seventy-two come back with no reader at this revision; most are
   `Hostinfo`/`NetInfo` reporting facets with nothing to report, and the residue is where the rows
   are. Re-run at this revision it produced no *new* row, which is itself worth recording: the vein
   was worked out by the previous two revisions.
2. **Take a function this ledger already claims parity for and read it top to bottom at the pin.**
   That is how earlier revisions found the reply-admission rows in `runIn4`, the `SrcCaps` callers,
   `select_home_region` and `handle_ping`; at this revision it produced the periodic-STUN row, where
   `stun_probe_should_run` ports one of Go's four stop conditions and documents that it ports one of
   four. **The best place to look is a divergence the code already admits to in a doc comment**, on
   the theory that if the author noticed it, it is real, and if it never reached this ledger, nobody
   has decided about it.
3. **Walk `tailcfg/nodecap` against this workspace** — and then ask the second question. The first
   question ("is this string in the tree?") is a grep and produced one row when it was added. The
   second ("does the behaviour it switches exist here to be switched?") is a judgement, cannot be
   automated, has to be asked once per miss, and produced seven. Forty of the misses are correct.
   The walk tells you where to ask; it does not answer.
4. **Check the wire types in both directions, mechanically** — outbound, each `ts_control_serde`
   field resolved to the wire name it serializes under and looked up in Go; inbound, the reverse over
   `tailcfg`'s json-tagged fields. It found `NetInfo.HairPinning` and `Node.ComputedName*` two
   revisions ago. Re-run at this revision it returns the one known phantom, the two known false
   positives and one *new* false positive that turned out to be a bug in the recipe's lookup scope
   rather than in the tree (above). Twice now the command has needed fixing before it could be
   trusted, which is the argument for re-running it at every revision even when it returns nothing:
   a command nobody runs is a command nobody notices is broken.
5. **Read the enumerations, not their history.** The highest-yield of the five, and the only one that
   has paid at two different revisions. `controlknobs.Knobs` is twenty-seven fields, each a switch
   control can throw on this client, and `UpdateFromNodeAttributes` is the one function that fills
   them; reading that file against this tree took minutes and opened eight rows. At **this** revision
   the same technique applied to `derp/derp.go`'s sixteen `FrameType` constants opened three, and it
   opened them past a filter that had hidden them from six revisions of sweeping: `ts_derp` models
   and decodes all sixteen, so every structural check reports the DERP package complete. **The
   question that pays is not "is this member modelled" but "what does this node DO when this member
   arrives, and when does it send one".** Asked that way, `PeerGone` is decoded and dropped,
   `NotePreferred` is never sent, and the route learned from a `RecvPacket` is consulted in one of the
   three places Go consults it. The generalisation is in the fourth-miss note above: a package whose
   purpose is to enumerate something owes you a read of the enumeration, and its commit log will never
   tell you that. **Record the clean reads too.** `tailcfg.PeerChange`'s eleven fields were walked at
   this revision and are all modelled *and* applied (`ts_control_serde::netmap::PeerChange`,
   `ts_runtime::peer_tracker::apply_peer_patches`), so that enumeration is done and the next revision
   should spend its hour elsewhere. Enumerations not yet walked, for whoever is next: `disco`'s nine
   message types (spot-checked at this revision and apparently complete, not walked), `tailcfg`'s c2n
   dispatch table (mostly moot behind the held declaration), `ipn.Prefs`, and `netcheck.Report`.

**The sharper form of "a swept package is not a read package", new at this revision.** Earlier
revisions established that sweeping a package's commits is not reading the package. `derp` adds the
next step: **modelling every member of a package's enumeration is not acting on every member of it**,
and it is a much better disguise than an unread commit log. `ts_derp` has all sixteen `FrameType`
constants, a body type for each, a `TryFrom<u8>` that rejects anything else, and regression tests
naming `Health`, `Restarting`, `Pong` and three `PeerGone` shapes by number. Every structural check
this ledger owns — the sweep, the node-attribute walk, both wire-type walks — reports the DERP client
as complete, and each is right about the question it asks. The question none of them asks is what
happens *after* the decode, and for three of the sixteen the answer is "a `tracing` call and nothing
else". **A `tracing::debug!` is the specific shape to look for**: it is what a faithful decoder of a
frame nobody wired up looks like from the inside, it passes review as handling, and it leaves no
symbol for a grep to find in the crate that should have consumed it.

Two entries are noisy by nature and should be read with that in mind: `ipn` (which subsumes
`ipn/localapi` and `ipn/ipnlocal`) catches every multi-package commit that also touched
`cmd/tailscale`, most of which is the daemon CLI this library deliberately does not have, and
`tsnet` is swept but not itemised row-by-row in §B — see the note at the end of §B for why. One
mapping row has no upstream path to sweep at all: `golang.zx2c4.com/wireguard`'s device, which
`ts_tunnel` re-implements, is an upstream *dependency* rather than a package in this repository —
track it through upstream's `go.mod` bumps, not through this loop. It is the only admissible
exception; see the fifth miss above.

When the pin is advanced, bump the header table, re-run the above, and rewrite §A and §B. **And when
it cannot be advanced, because upstream's default branch is already what the header pins, re-run
everything anyway and rewrite §A and §B from what came back** — that has now happened at
several consecutive revisions, and it is not a special case to be handled once: a pin catches up with upstream
whenever a re-derivation follows soon after the last one, and the value of the document at that
moment is entirely in the reading. Two revisions ago the reading that paid was not of upstream at all:
it was of the two commits *this tree* merged in the interval, read against the upstream they cite. At
**this** revision the tree merged no code either, and what paid was reading one upstream enumeration —
`derp/derp.go`'s frame types — against a crate that models every member of it.

A row whose assessment changes should say *why* it changed — and note that "why" has **four**
sources, not three. The fourth was added at the previous revision, and at this one it is the only
source that produced anything.

1. **Upstream can move.** `29cfb0b4c` added capability version 147 two revisions ago; `85c1efb46`
   added 146 five revisions ago, `2ae2808b6` moved the index-eviction row before that, `e1d17a6b9`
   and `f53c28101` moved the disco-key rows before that, and `d9cc55e33` moved the
   `tsnet.Server.HTTPClient` row before that. It contributed nothing at three consecutive revisions,
   one row three revisions ago, and nothing at the three since, which is about its long-run rate.
2. **This tree can move, with upstream still**, and it is the only thing that closes rows.
   **Nothing closed at this revision**, because no Rust changed in the interval — the one thing that
   can close a row. **One closed at the previous revision** on tree movement alone (#462 with #464),
   the only row this ledger has opened and closed in consecutive revisions. **Six closed three
   revisions ago** (#438, #440, #442, #443, #445, #448), the largest single-revision close in this ledger's
   history; none closed at the previous revision, where the tree moved by one documentation commit.
   Six closed before that (#415, #417, #418, #419, #421,
   #423), four before that (#404, #406, #408/#410, #412), six before that (#360, #363, #367, #369,
   #370, #372), three before that (#339, #342/#343/#345, #347), and three capability-version rows at
   the one before.
3. **The sweep itself can widen, or simply be read more carefully**, and surface something that was
   true all along. That is where all seventeen changes across two earlier revisions came from, and at
   **this** revision it is the only source that *could* fire — upstream did not move, the tree did not
   move, and no port merged — so all three new rows are its, from the DERP frame-type enumeration.
   Called "the technique of last resort when the delta is thin" by earlier revisions; at a revision
   with no delta at all it is the technique of first resort, and it should be budgeted as such.
4. **A port that landed here can turn out to be narrower than the upstream behaviour it copied.**
   Added three revisions ago, when **four of its five new rows** came from re-reading three merged
   ports against the Go tree they cited, which is what [`PARITY_AUDIT.json`](PARITY_AUDIT.json)
   records; two revisions ago it produced the one new row, and at the previous revision **all three**.
   At **this** revision it produced nothing, and not because it was skipped: it has a precondition the
   other three sources do not, which is a merged port to read, and the interval contains none. Note
   that precondition when you plan a revision — this source is the dominant one over the long run and
   it is silent at exactly the revisions where the pin has caught up and the tree is quiet. The reason
   it dominates when it can fire is structural: none of these rows is visible to the sweep, to the
   node-attribute walk, or to the wire-type checks — every one of those reports the attribute as
   *handled* and the behaviour as *present*, because it is. What is wrong is the decision logic
   underneath, and only reading the merged diff against upstream finds it. **So a revision that
   closes rows owes the next revision an audit of what it closed.** This revision closed one row and
   the audit of it opened three, which sharpens the rule into something worth stating on its own:
   **a port written from one of this ledger's rows is scoped by the row, and the row is a summary of
   upstream, not upstream.** The closer a port looks to the row that asked for it, the more likely
   the gap is in the row.

One consequence worth stating for whoever advances the pin next: **a row this ledger closes is not a
row that stops needing evidence.** Every "already covered" and "closed at this revision" bullet above
names the tree code that covers it, because the next revision has to be able to re-check the claim
without re-deriving the whole document — and because a closed row can reopen if the code it names is
refactored away. The fourth source above sharpens that into something stronger than a filing
convention: a closed row can be *wrong on the day it closes*, and naming the code is what makes that
discoverable.

## The quality bar for port PRs

A port PR is a claim that a behaviour now matches upstream Go. The bar exists so the claim is
checkable.

1. **One focused area per PR.** One gap-list row, or one coherent slice of one row (the TSMP
   receive side is a fine PR; "TSMP plus services actions" is not). A PR that fixes two things
   cannot be reviewed against either. Anything else you find on the way goes in the PR body as a
   note, not in the diff.
2. **Real tests that exercise the ported behaviour.** Not "it compiles", not a test that only
   asserts the shape of a struct: a test that would fail if the behaviour were wrong. For wire
   formats, assert against bytes taken from the Go implementation or its test vectors; for
   decisions (filter verdicts, fail-closed drops, retry timing), assert the decision, and assert
   the negative case too — the drop that must still happen, the fallback that must *not* be taken.
3. **Cite the upstream source in the code.** Name the Go function, file or commit the behaviour
   comes from in a doc comment, the way the existing crates do — "Go `runIn4`", "Go
   `tkaFilterNetmapLocked`". That citation is what makes the next re-derivation of this ledger
   cheap.
4. **The full gate is green before you push:**
   ```sh
   TS_RS_EXPERIMENT=this_is_unstable_software bin/check
   ```
   `cargo +nightly fmt --check`, `cargo run -p checks` (the anti-leak check), `cargo clippy` over
   the lib and then over bins/tests/benches/examples with `-D warnings`, `cargo doc`,
   `cargo deny check all`, `cargo machete`, `cargo nextest run --all-features`,
   `cargo test --doc`, and `cargo build --all-targets`. Run it locally and mean it — it is
   strictly wider than what CI checks on this fork: the job carrying fmt/deny/machete
   (`arch_independent`) is gated to the upstream repository owner and never runs here, and
   `bin/check` passes `--all-features` where the hosted job passes only `--workspace`, so
   feature-gated code is linted and tested locally and nowhere else. The corollary: if a step
   fails on code your change does not touch, re-run it against the base commit before chasing it —
   the wider flags surface pre-existing, feature-gated findings that CI has never seen. The pair
   that answers "is this green" is the `rust` workflow's `hosted test` job plus
   `cargo run -p checks`.
   `ts_forwarder/tests/forwarding.rs::udp_forwarder_splices_subnet_route_to_real_socket` is a
   real-UDP timing test that flakes under load — re-run it, do not "fix" it.
5. **Interop first, and fail-closed stays fail-closed.** This engine is always the dialing client
   against real Tailscale, `wireguard-go` and kernel peers: never ship a change that assumes the
   peer implements a fork-specific behaviour. The invariants in
   [`docs/PARITY_ROADMAP.md`](docs/PARITY_ROADMAP.md#invariants-that-must-never-regress) — no
   origin-IP leak, no silent direct-dial fallback, `ring`-only on the tailnet/TLS path,
   `panic=unwind` — outrank parity: if upstream Go does something this fork's anti-leak posture
   forbids, document the divergence here rather than porting it.
6. **No new dependencies on the egress path**, and dependency changes ride in their own PR — see
   [`CONTRIBUTING.md`](CONTRIBUTING.md#dependencies).