geiserx_tailscale 0.45.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
# Porting ledger: upstream Go `tailscale` → this repository

| | |
| --- | --- |
| **Upstream source** | `https://github.com/tailscale/tailscale` (Go) |
| **Upstream commit this ledger was written against** | `d9cc55e33b4a9f092e21b882df39aa4005cb0fa4` (2026-08-31, `tsnet: avoid depending on mutable DefaultTransport`) |
| **Upstream `tailcfg.CurrentCapabilityVersion` at that commit** | **145** (2026-08-04) — unchanged from the previous two pins |
| **This repository at ledger time** | `7c39ae0` — workspace version `0.44.0` |
| **`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 → 145**, i.e. upstream commits from 2025-10-06 to 2026-08-31 (the window is anchored to when capver 130 landed upstream; the declaration here being 125 rather than 130 does not change what upstream added) |
| **Previous pin** | `49e148c4a30b4f8098f69468fd27a7021d85ea02` (2026-08-30). Only two upstream commits separate the two, one of them in a mapped package — but the sweep list was widened again at this revision, which is where the new rows came from. See §B, *New at this revision* |

> 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) |

### 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) |
| `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) |
| `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) |
| `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.
- `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.)
- `sessionrecording`, `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 at this revision: 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 → 145

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 still at **145** at the new pin — `tailcfg/tailcfg.go:197` — so the
window is the same fifteen versions the previous revision covered. Descriptions are upstream's own
(`tailcfg/tailcfg.go`, `tailcfg/nodecap`).

**No row changed assessment at this revision.** Both halves of the usual reason are absent:
upstream added no capability version between `49e148c4a` and `d9cc55e33`, and no port landed in
this tree in the interval either — the only two commits here since the last revision are the two
documentation passes that produced and then corrected it (#323, #324). The three rows the previous
revision flipped (135, 142, 144) still say why they flipped, because that history is what makes the
row re-checkable; they did not move again. §B is where this revision's changes are.

| 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 after the previous revision was written. `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 |
| 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 |

Net: of the fifteen versions upstream added, **one still needs a port** — 133, host-OS-facing —
**four are already covered** (135, 137, 142, 144), and the remaining ten are not applicable to an
embedded userspace node (138 among them, once the declaration was held below the version that
promises it). That is the same count the previous revision reached, and for the same reasons: this
axis stood still on both sides. Row 133 was re-checked against the tree at this pin and is still
open — `ts_host_net::HostDns::nameservers` is a `Vec<Ipv4Addr>`, and `ts_runtime::tun_actor` fills
it with the single IPv4 service IP, so there is still no IPv6 MagicDNS address to register.

### 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 itself was widened at this revision — see
*New at this revision* below and the note in [Re-deriving this ledger](#re-deriving-this-ledger).

- **TSMP disco-key advertisement** (`net/packet`, `net/tstun`, `wgengine/magicsock`,
  `control/controlclient`: `c54d24369`, `c870d3811`, `bf467727f`, `82a381e54`, `014d5bd9e`,
  `3799eaf26`, `fb27d87e0`) —
  peers now 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 are now covered** — *changed from "receive side covered, send
  side needs a port"*, because the send half landed here in #314 and #318 after the previous
  revision. 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`), and
  `PeerTracker::learn_disco_key` applies it to the peer (Go
  `magicsock.Conn.HandleDiscoKeyAdvertisement`). 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, so a priority message never
  triggers a handshake), and `ts_dataplane` decides the content (Go
  `magicsock.Conn.PriorityMessageForPeer`). Row 144 above is the capability-version view of the same
  work.
  Two upstream commits that the previous revision did not name are worth recording, because both
  turn out to *confirm* what is here rather than to open a gap. `3799eaf26`
  (`wgengine/magicsock`, `wgengine`) replaced the periodic advertiser — a 2-minute timer with
  suppression rules bolted on (`c76113ac7`, `92ab4866d`, `ee76a7d3f`, `54005752a`) — with the
  single `SetPriorityMessageOnEstablishmentFunc` callback that `wireguard-go` invokes on rekey. At
  the pin there is no periodic sender left in `wgengine/magicsock/magicsock.go`, so the
  establishment-only send this tree implements is upstream's current shape, not a subset of it.
  And `fb27d87e0` (`net/tstun/wrap.go`) removed the `buildfeatures.HasCacheNetMap &&
  envknob.BoolDefaultTrue("TS_USE_CACHED_NETMAP")` guard from the *receive* side, so a Go node now
  consumes any advertisement carrying a non-zero key regardless of whether it participates in
  netmap caching — which is exactly what `ts_dataplane::filter_inbound_from_peer` has always done
  (the only guard here is the zero-key check, and the advertisement is still dropped rather than
  delivered). Upstream converged on this tree's behaviour; nothing to do.
- **IPv6 fragment extension-header handling in the filter** (`net/packet`, `wgengine/filter`:
  `4c4ec3d46`, `26b2ed0a6`) — upstream extended its RFC 1858-style fragment classification to IPv6
  fragment extension headers. **Needs port only under `Config::enable_ipv6`**: `ts_dataplane`
  implements the classification for IPv4 only (`Ipv4Fragment`, `MIN_FRAG_BLKS`), which matches the
  default IPv4-only posture but leaves the opt-in IPv6 path without upstream's fragment rules.
- **Peer relay** (`disco` 0x04–0x09, `net/udprelay`, `feature/relayserver`; capver 120/121, i.e.
  *behind* the declared 125) — **ported (client half)**. All nine disco message types now 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, as upstream requires. 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 — the `AllocateUDPRelayEndpointRequest`/`Response` pair is decoded but never originated,
  because a relay-capable peer allocates on our behalf.
- **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 — since #317 — the
  `/remoteapi/localapi/*` prefix of row 142. The three debug endpoints are **resolved by holding
  `CapabilityVersion::CURRENT`** below them, which was the alternative to porting the three handlers.
  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 now take Go's own `400`/`unknown c2n path`
  fallthrough (`handleC2N`, `ipn/ipnlocal/c2n.go`), which is asserted by test. The declaration
  landed at **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 now 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 that #317 ported sits behind 126 (seamless key renewal, not implemented),
  127 and 128 (two of the three c2n debug endpoints rejected on evidence above), 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. Recording it here so the
  next reader does not mistake a dormant handler for a broken one — and so a future bead to raise the
  declaration is cut against the full list, not against 126 alone. (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`) — upstream added
  client application *actions* (with attributes and `ServiceActionType` constants) to the VIP
  services model. **Needs port** only for the consume side to stay current:
  `ts_control_serde/src/service_vip.rs` models `VipService` and the c2n response with no action
  types.
- **`Node.IsRouter` / `PeerStatus.IsRouter`** (`8d830599b`) — **already covered** as of this
  ledger revision. Note the row's original wording ("a new status/netmap field") was wrong and is
  corrected here: upstream added no wire field. `tailcfg.Node.IsRouter` and
  `ipnstate.PeerStatus.IsRouter` 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. Control sends nothing new, so there was never a round-trip to match; adding
  an `IsRouter` JSON key would have been a divergence, not a port. Mirrored here as
  `ts_control::Node::is_router` (over `accepted_routes` vs `tailnet_address`) and
  `ts_runtime::status::StatusNode::is_router` (over `allowed_routes` vs `ipv4`/`ipv6`), both
  covering the present *and* absent case, and cross-checked against each other the way upstream's
  `TestNodeIsRouter` cross-checks its two definitions.
- **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` simply
  omits it, which is what a Go client without the option does. Note 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**:
  `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; this row exists so the audit is not forgotten.

#### New at this revision

Upstream moved almost nothing: two commits (`49e148c` → `d9cc55e3`), one of them
(`2a4d74356`, `wgengine/netstack`) a data-race fix on a *test* logger in a Go test helper
(`makeHangDialer`) with no production counterpart, and the other (`d9cc55e33`) a revision of the
`tsnet` row already above. Everything else new here came from **widening the sweep list a second
time**.

The previous revision widened the loop by seven packages and wrote down the rule that produced
them: *every upstream package [Package mapping](#package-mapping) names must be in the sweep, or the
mapping is a claim the sweep never checks.* Applying that rule literally at this revision shows it
was not finished. Sixteen more mapped packages were still unswept: `wgengine` itself (only three of
its subdirectories were in the loop, so `wgengine/userspace.go`, `wgengine/wgcfg` and
`wgengine/router` were not), `ipn` (only `ipn/localapi` was, so `ipn/ipnlocal`, `ipn/ipnstate` and
`ipn/store` were not), and then `net/netmon`, `net/art`, `control/controlhttp`, `types/persist`,
`feature/identityfederation`, `feature/taildrop`, `feature/ssh`, `feature/acme`, `ssh/tailssh`,
`util/clientmetric`, `tstime`, `tstest`, `tool/` and `tsd`.
The loop in [Re-deriving this ledger](#re-deriving-this-ledger) is rewritten to cover all of them,
and is now built from parent paths (`wgengine`, `ipn`) so that a *new* subdirectory upstream adds
cannot fall outside it the way `wgengine/router` did.

Two of the rows the widening surfaced need a port; the rest are recorded so the next re-derivation
does not re-cut them.

- **Quad-100 traffic is absorbed locally regardless of port and protocol** (`wgengine/netstack`:
  `1b4091161`) — **needs port**, and it is the sharpest row at this revision. Upstream's
  `handleLocalPackets` used to intercept traffic to the Tailscale service IP only for an
  allow-list — TCP 53/80/8080, UDP 53 — and returned `filter.Accept` for everything else, letting
  the packet fall through to the ACL filter and on to `wireguard-go`. Upstream removed the
  allow-list: quad-100 is now absorbed into netstack unconditionally, "so such traffic never
  reaches the conntrack / peer-routing layers", and a companion `hittingServiceIP` case in
  `acceptTCP` RSTs an unserved quad-100 TCP port instead of falling through to the
  `isTailscaleIP` branch that rewrote the dial to `127.0.0.1:<port>`.
  This tree has the same allow-list, and in **TUN transport mode it has the leak upstream closed**.
  `ts_runtime::tun_actor`'s `classify_magic_dns` intercepts an inbound packet only when it is
  IPv4 **and** UDP **and** destined to `100.100.100.100:53`; every other packet takes
  `Intercept::NotIntercepted` and is handed to the overlay unchanged. `tun_actor` steers
  `100.100.100.100/32` into the TUN whenever MagicDNS is enabled, so the host really does emit such
  packets — a stub resolver speculatively trying DoT on `100.100.100.100:853` is upstream's own
  cited example. `ts_overlay_router` then resolves the destination against the outbound table, and
  that table carries a configured exit node's `0.0.0.0/0` as `RouteAction::Wireguard(peer)`
  (`ts_runtime::route_updater`), which matches `100.100.100.100`. With an exit node selected — the
  configuration this fork exists for — the node's own service-IP traffic is encrypted and sent to a
  peer. Without one it is merely dropped as unrouted, which is why this has not been visible.
  The **netstack** transport is already correct and needs no change: `ts_runtime::netstack_actor`
  gives the netstack interface `100.100.100.100` as a local address, so all quad-100 traffic
  terminates there whatever its port or protocol. The second half of upstream's fix is
  **not applicable**: this tree has no `isTailscaleIP` → host-loopback dial rewrite for an
  unserved port to fall through to, so there is nothing to guard.
- **The DNS forwarder sets TC against the *client's* size limit, not just its own read buffer**
  (`net/dns/resolver`: `8cac8b117`) — **needs port (narrow)**. Upstream added
  `checkResponseSizeAndSetTC` and calls it on every path that returns a UDP answer: if the response
  exceeds the EDNS buffer size the *request* advertised — or 512 bytes when the request carried no
  EDNS OPT record, per RFC 1035 — the TC bit is set (the body is left intact), so the stub resolver
  knows to retry over TCP.
  Here, `ts_dns_wire` already does this correctly for the answers this node *builds* itself: it
  caps an authoritative response at 512 and sets TC when it drops an answer, asserted by
  `oversized_answer_set_sets_tc_and_caps_512`. The gap is the **forwarded** path.
  `ts_runtime::magic_dns`'s `cap_response` sets TC only when the upstream reply exceeds
  `MAX_UPSTREAM_RESPONSE` (4096) and has to be chopped mid-message. A reply between the client's
  limit and 4096 — say 900 bytes for a client that sent a plain, non-EDNS query — is relayed
  verbatim with TC clear, where Go would set it. The port is to parse the forwarded request's OPT
  record for its advertised UDP size, default to 512 when absent, and set TC when the reply exceeds
  it; the existing 4096 cap stays as the read bound it already is. Narrow in practice, because the
  query is forwarded verbatim and a well-behaved upstream honours the EDNS size itself — but "the
  upstream is well-behaved" is exactly the assumption upstream stopped making. Host-facing, not
  wire-facing.
- **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: Go passes accepted
  variables through to the session, this fork drops all of them. 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. Named because `net/netmon` is a mapped package that had never been swept.
- **`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. Recorded because
  `feature/identityfederation` is a mapped package this sweep reached for the first time.

#### Carried from the previous revision's widening

The six bullets below were new when the loop first gained `net/socks5`, `net/tsdial`,
`net/tlsdial`, `net/bakedroots`, `ipn/localapi`, `feature/remoteconfig` and `tsnet`. All six were
re-checked against this pin and against this tree. Five are unchanged; the `tsnet.Server.HTTPClient`
row is the one upstream revised at this pin, and its bullet is rewritten in place to describe what
upstream now does rather than what it did for one day.

- **SOCKS5 proxy credentials compared in constant time** (`net/socks5`: `60576f8bd`) — 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 comparisons with
  `subtle.ConstantTimeCompare`, evaluating both halves so the username result does not gate whether
  the password is examined. **Needs port** — and the same asymmetry upstream fixed 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 is upstream's
  own and it 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. The fix needs no new dependency —
  `src/tsnet.rs`'s `localapi::cred_ok` is already a constant-time comparison (this fork's mirror of
  the `subtle.ConstantTimeCompare` Go's LocalAPI has always used), so the SOCKS5 path is the one
  place on the loopback that does not use it. Host-facing, not wire-facing; no peer or control plane
  observes it.
- **`tsnet.Server.HTTPClient` carries `http.DefaultTransport`'s settings** (`tsnet`: `49e148c4a`,
  then `d9cc55e33`) — **still needs a port** (narrow, host-facing), but *the port target changed at
  this revision*, so the row is rewritten rather than restated. `49e148c4a` stopped returning
  `&http.Client{Transport: &http.Transport{DialContext: s.Dial}}` and *cloned*
  `http.DefaultTransport`. `d9cc55e33` — the new pin, landed a day later — undid the clone: an
  application is permitted to replace or mutate the package-level `http.DefaultTransport`, so
  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.
  That matters here twice over. First, the port is no longer "work out the `hyper` equivalent of
  whatever Go's global currently holds"; it is a fixed list — five settings, plus the tailnet
  dialer this tree already installs — to decide about one at a time. Second,
  the reason upstream backed the clone out — a mutable process-global leaking into a tailnet
  client — is a hazard 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, and its doc comment still calls itself "the exact analog" of the Go
  expression upstream stopped using at the previous pin. The `Proxy = nil` half stays structurally true:
  `TailnetConnector` dials the overlay directly and has no environment-proxy path to disable.
  Upstream's `TestHTTPClientDefaultTransport` still fails on any unrecognised future field — and now
  also asserts that `TLSClientConfig`, `TLSNextProto` and `HTTP2` are *nil*, since a literal
  transport must not pick up the lazily-populated state a shared global accumulates. That test shape
  is still the one worth copying: 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 (trust a self-hosted
  control plane's private CA without disabling verification) 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. Recorded so the row is not re-cut as a gap.
- **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` (1.0.9 in `Cargo.lock`) *is* the compiled-in
  bundle and tracks Mozilla's set on the crate's own release cadence. So there is nothing to port —
  but 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`; both predate the previous pin and were missed only because the package
  was unswept) — **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`. From the newly swept packages: 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`. From the packages swept for the first time at this revision:
`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, `ipn/ipnlocal`'s locking and delta-path rework, and `2a4d74356`, the new pin's
sibling commit, which fixes a data race on a *test* logger in a Go test helper. `tsnet` itself has
125 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' d9cc55e3 -- tailcfg/tailcfg.go
git -C <tailscale-go> grep -nE '^//[[:space:]]*-[[:space:]]*1[3-9][0-9]:' d9cc55e3 -- 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 \
         control/controlclient control/controlbase control/controlhttp \
         wgengine ipn tsd tka types/key types/persist tsnet \
         feature/remoteconfig feature/identityfederation feature/taildrop feature/ssh \
         feature/acme ssh/tailssh 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, and the
# sweep list itself can be wrong (it has been, twice).
git -C <tailscale-go> log --oneline d9cc55e3..<new-pin>
```

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 **16 lines**, 130
through 145, i.e. the fifteen-version window of §A plus the 130 row that anchors it. An empty or
short result means the pattern broke, not that upstream added nothing.

**The sweep list is part of the ledger, and it has been wrong twice.** The previous revision added
`net/socks5`, `net/tsdial`, `net/tlsdial`, `net/bakedroots`, `ipn/localapi`, `feature/remoteconfig`
and `tsnet`, and wrote down the rule that produced them: 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. Applying that rule
literally at this revision showed the list was still short by sixteen mapped packages, so the loop
above was rebuilt from the mapping rather than extended by hand. The additions are `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`.

`wgengine` is the lesson. The old loop swept `wgengine/filter`, `wgengine/magicsock` and
`wgengine/netstack` but not `wgengine` itself, so `wgengine/userspace.go`, `wgengine/wgcfg` and
`wgengine/router` — all three named in [Package mapping](#package-mapping) — were invisible, and a
subdirectory upstream created after the loop was written would have been invisible too. Sweeping
the parent removes that failure mode entirely. It is why the quad-100 row, the sharpest new row in
§B, is only reaching this ledger now: `1b4091161` landed upstream in April 2026, in a package that
*was* in the loop, but the pattern that hid `wgengine/router` is the same pattern that makes a
long-swept package's older commits easy to skim past. Re-derive against the list, not against
memory of the last derivation.

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.

When the pin is advanced, bump the header table, re-run the above, and rewrite §A and §B. A row
whose assessment changes should say *why* it changed — and note that "why" has three sources, not
one. Upstream can move (as `d9cc55e33` moved the `tsnet.Server.HTTPClient` row at this revision,
revising its own previous commit a day later). This tree can move, with upstream perfectly still (as
it did at the previous revision, when three capability-version rows flipped to *already covered*).
Or the **sweep itself** can widen and surface something that was true all along — which is where
every new row at this revision came from.

## 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).