diff --git a/docs/netlink/README.md b/docs/netlink/README.md index 015e8c27..83913c61 100644 --- a/docs/netlink/README.md +++ b/docs/netlink/README.md @@ -24,6 +24,7 @@ If you are looking for the daemon as a whole, start at the | [goip parity: remaining gaps](goip-parity-gaps.md) | What full `ip` coverage still needs: the 20 objects goip does not run, the missing verbs on the 11 it does, the generic netlink and XFRM families, and the 23 ungated rows, 22 of which G0 has gated. For each netlink message type `ip` uses, a measured count of nlmon pcaps, table-driven parse tests and parse benchmarks. The plan to close the gaps runs in three tiers: complete the capture corpus, then the parsers, then goip and the matrix. | | [goip parity gaps — status](goip-parity-gaps-status.md) | The tracker for that plan: the next action, the state of each phase from C0 to G7, the task checklist, the verification ledger and the decision log. A phase is closed, and the assessment re-measured, in its last task PR. | | [vrf show NAME](vrf-show-name.md) | **Landed.** `goip vrf show NAME`, the refused `ipvrf_get_table` single lookup — the one netlink path left on the `vrf` object. Five tasks (VS0–VS4), each with its test table: pin the already-committed request bytes, the `VrfTableByName` accessor, the argv and output, the capture rows, and the three matrix rows — gated. | +| [vrf family forms](vrf-family-forms.md) | **Landed.** `goip -4/-6/-0 vrf show NAME`, the parked decision-8 family byte — the kernel resolves the name without consulting `ifi_family`, so the three forms are the 44-byte get with one byte changed. Three tasks (VF0–VF2): the captures with the family-byte pin, the guard deletion (the object change is one line), and the three matrix rows — gated. | | [Upstream bug: bond partner state width](upstream/iproute2-bond-partner-state-width.md) | An iproute2 bug found while designing the link write path: `ip` reads the u16 `IFLA_BOND_SLAVE_AD_PARTNER_OPER_PORT_STATE` as a u8, so big-endian hosts always print `0 <>`. | The last three are a chain — the audit finds the gaps, the roadmap decides the @@ -39,9 +40,9 @@ each sent** and the **text each printed**. It is the strongest evidence in the tree that `pkg/xtcpnl` decodes what the kernel actually said, because the reference is iproute2 itself rather than our reading of iproute2. -510 commands are compared, 275 of them dry-run link, link property, route, +513 commands are compared, 275 of them dry-run link, link property, route, address, neighbor, rule, nexthop, address label, neighbor table, statistics, -token, nsid, tcp_metrics, sr, ioam, mptcp, fou, ila, l2tp, macsec, XFRM state and XFRM policy writes, flushes and restores, and 509 are gated. The counts, what "gated" costs to earn, and the scope boundaries are +token, nsid, tcp_metrics, sr, ioam, mptcp, fou, ila, l2tp, macsec, XFRM state and XFRM policy writes, flushes and restores, and 512 are gated. The counts, what "gated" costs to earn, and the scope boundaries are in [parity at a glance](coverage-status.md#goip--ip-parity-at-a-glance). The comparator is `pkg/nlparity` (protocol level) and `internal/goipparity` (command level); the accepted-divergence list is diff --git a/docs/netlink/coverage-status.md b/docs/netlink/coverage-status.md index 10466c55..6318a0ba 100644 --- a/docs/netlink/coverage-status.md +++ b/docs/netlink/coverage-status.md @@ -2,7 +2,7 @@ ## goip ↔ `ip` parity at a glance -Counted from the tree on 2026-11-01. Every number has a file behind it, named +Counted from the tree on 2026-11-04. Every number has a file behind it, named in the last column; **if this table and the file disagree, the file is right and this table is stale.** That has happened before in this document — see the "no route command is in `gated_commands`" line that outlived its own truth by @@ -10,9 +10,9 @@ several steps, further down under [Remaining](#remaining). | | count | counted from | |---|---|---| -| commands in the comparison matrix | **510** | `internal/goipparity/commands.go` — 235 read rows, the 3 `nexthop show` dev, master and vrf selector rows of the iproute2 7.2.0 move and the 3 `vrf show NAME` single-get rows of VS4 ([vrf show NAME](vrf-show-name.md)) among them, 8 of them G6-T01's `tcp_metrics` reads, each with a `Setup`, 4 G6-T02's `sr` reads, 4 G6-T03's `ioam` shows, 9 G6-T04's `mptcp` reads, 8 G6-T05's `fou` reads, 7 G6-T06's `ila` reads, 13 G6-T07's `l2tp` reads, 15 G6-T08's `macsec` reads, 9 G7-T01's `xfrm state` reads, G7-T02's `xfrm state delall` with no SA to delete, 10 G7-T03's `xfrm policy` reads and G7-T04's `xfrm policy delall` with no policy to delete, NETLINK_XFRM, (31 of them `addr show` state, selector and output-mode rows with a `Setup`, and 6 `route get`, 2 `neigh get`, 4 `nexthop get` and `nexthop bucket`, 3 `link xstats` and `link afstats`, 6 `mroute show`, 3 `token`, 4 `netns list-id`, 3 `route save` and `route showdump`, 1 `route flush`, 1 `neigh flush`, 1 `rule save` and 1 `nexthop flush` rows with one; the missing-table `route flush` row has none), the 21 link write rows, the 26 route write rows, the 27 address write rows, the 7 address flush rows, the 2 address restore rows, the route restore row, the 2 route flush write rows, the 6 neigh write rows, the 2 neigh flush write rows, the 17 rule write rows, the rule flush write row, the rule restore row, the 6 nexthop write rows, the nexthop flush write row, the 2 addrlabel write rows, the addrlabel flush write row, the 3 ntable write rows, the stats write row, the 3 link property write rows, the 2 token write rows, the netns set write row, the 6 `tcp_metrics` delete and flush write rows, the 7 `sr` set write rows, the 16 `ioam` namespace and schema write rows, the 19 `mptcp` endpoint and limits write rows, the 16 `fou` add and del write rows, the 11 `ila` add and del write rows, the 15 `l2tp` tunnel and session add and del write rows and the 24 `macsec` SA, receive channel and offload write rows, generic netlink, the 16 `xfrm state` add, update, delete, deleteall, flush and allocspi write rows and the 12 `xfrm policy` add, update, delete, deleteall, flush, set and setdefault write rows, NETLINK_XFRM | -| of those, `Implemented: true` | **510** | same file — every row: none is `SKIP` and, since G6-T08, none is an `ip` stand-in (`IPStandIn`), which is why the comparator's unimplemented branch and the stand-in's rendering are exercised by synthetic commands | -| of those, in `gated_commands` | **509** | `pkg/nlparity/goip-parity-allowlist.json` | +| commands in the comparison matrix | **513** | `internal/goipparity/commands.go` — 238 read rows, the 3 `nexthop show` dev, master and vrf selector rows of the iproute2 7.2.0 move, the 3 `vrf show NAME` single-get rows of VS4 ([vrf show NAME](vrf-show-name.md)) and the 3 `vrf show NAME` family rows of VF2 ([vrf family forms](vrf-family-forms.md)) among them, 8 of them G6-T01's `tcp_metrics` reads, each with a `Setup`, 4 G6-T02's `sr` reads, 4 G6-T03's `ioam` shows, 9 G6-T04's `mptcp` reads, 8 G6-T05's `fou` reads, 7 G6-T06's `ila` reads, 13 G6-T07's `l2tp` reads, 15 G6-T08's `macsec` reads, 9 G7-T01's `xfrm state` reads, G7-T02's `xfrm state delall` with no SA to delete, 10 G7-T03's `xfrm policy` reads and G7-T04's `xfrm policy delall` with no policy to delete, NETLINK_XFRM, (31 of them `addr show` state, selector and output-mode rows with a `Setup`, and 6 `route get`, 2 `neigh get`, 4 `nexthop get` and `nexthop bucket`, 3 `link xstats` and `link afstats`, 6 `mroute show`, 3 `token`, 4 `netns list-id`, 3 `route save` and `route showdump`, 1 `route flush`, 1 `neigh flush`, 1 `rule save` and 1 `nexthop flush` rows with one; the missing-table `route flush` row has none), the 21 link write rows, the 26 route write rows, the 27 address write rows, the 7 address flush rows, the 2 address restore rows, the route restore row, the 2 route flush write rows, the 6 neigh write rows, the 2 neigh flush write rows, the 17 rule write rows, the rule flush write row, the rule restore row, the 6 nexthop write rows, the nexthop flush write row, the 2 addrlabel write rows, the addrlabel flush write row, the 3 ntable write rows, the stats write row, the 3 link property write rows, the 2 token write rows, the netns set write row, the 6 `tcp_metrics` delete and flush write rows, the 7 `sr` set write rows, the 16 `ioam` namespace and schema write rows, the 19 `mptcp` endpoint and limits write rows, the 16 `fou` add and del write rows, the 11 `ila` add and del write rows, the 15 `l2tp` tunnel and session add and del write rows and the 24 `macsec` SA, receive channel and offload write rows, generic netlink, the 16 `xfrm state` add, update, delete, deleteall, flush and allocspi write rows and the 12 `xfrm policy` add, update, delete, deleteall, flush, set and setdefault write rows, NETLINK_XFRM | +| of those, `Implemented: true` | **513** | same file — every row: none is `SKIP` and, since G6-T08, none is an `ip` stand-in (`IPStandIn`), which is why the comparator's unimplemented branch and the stand-in's rendering are exercised by synthetic commands | +| of those, in `gated_commands` | **512** | `pkg/nlparity/goip-parity-allowlist.json` | | allowlisted divergences | **5** | same file, `entries` | | of those, `kind=version-skew` | **0** | the pin is `ip` 7.2.0 and goip follows it; the seven 7.1.0 entries were retired on 2026-10-21 (see [iproute2 7.2.0](#iproute2-720-the-pin-moves-and-goip-moves-with-it)) | | of those, `kind=accepted-divergence` | **5** | the five `ip netns` rows' `NETNSA_FD`, the descriptor each process opened the namespace file as, a slot in its own table (G4-T04). Nothing else is: the nine wire divergences runs 8 and 9 measured **could not** be, because a transaction-count divergence is unsuppressible (`nlparity_allowlist.go:143`), and G0 retired every one by changing goip to send `ip`'s requests | diff --git a/docs/netlink/goip-parity-gaps.md b/docs/netlink/goip-parity-gaps.md index 95999efc..16dce454 100644 --- a/docs/netlink/goip-parity-gaps.md +++ b/docs/netlink/goip-parity-gaps.md @@ -109,7 +109,19 @@ G4 delivered all six: goip runs `mroute`, `mrule`, `token`, `netns list-id` and | monitor | Every NEW/DEL notification, across the multicast groups at `ip/ipmonitor.c:267-367` | `ip/ipmonitor.c:194` | each subsystem's notifier | The 10 decoded event types (`IsRtnetlinkEventType`, `RTM_NEWRULE`/`RTM_DELRULE` among them since G4-T05) were not rendered; `goip monitor` renders every type `accept_msg` prints since G4-T06 | | prefix (in monitor) | `RTM_NEWPREFIX` (`prefixmsg`) | `ip/ipprefix.c:33` | `net/ipv6/addrconf.c` | Emitted only when a router advertisement is received | -**maddress depends on the version.** At the pin, `ip maddress show` reads `/proc/net/{dev_mcast,igmp,igmp6}` (`ip/ipmaddr.c:95-179`), and add/del are ioctls, so it is class F at 7.1.0. The 7.2 checkout dumps `RTM_GETMULTICAST` instead *(7.2: `ip/ipmaddr.c:276`)*, so maddress moves to this class at the next pin bump. +**maddress depends on the version.** `ip maddress show` reads +`/proc/net/{dev_mcast,igmp,igmp6}` (`ip/ipmaddr.c:95-179`) at the 7.2.0 +pin, and add/del are ioctls, so it is class F there. Commit b8cdda55 +("ipmaddr: use RTM_GETMULTICAST to list multicast addresses"), first +released in **v7.3.0**, switches the read to an `RTM_GETMULTICAST` dump — +so maddress moves to this class at the 7.3.0 pin bump. (This note +originally said the move was "the 7.2 checkout", citing `ipmaddr.c:276` — +written against a post-7.3.0 dev snapshot misread as 7.2. At 7.2.0 +every form reads /proc, family-filtered included: a capture run on +2026-11-04 recorded zero netlink datagrams for `ip -4/-6 maddress show`, +which is what the rows' floors are for.) The reply is already grounded +either way: the addr plan's self-sent mcdump captures are the corpus's +only `IFA_MULTICAST`/`IFA_MC_USERS` holders ([addr-write-path.md](addr-write-path.md), table 6). ### D. Generic-netlink objects goip does not run @@ -144,7 +156,7 @@ These surfaces have no netlink message to capture, so they are out of scope for |---|---| | `tunnel`/`tunl6` parameters and add/change/del/prl/6rd | ioctls `SIOC{GET,ADD,CHG,DEL}TUNNEL`, `SIOC{ADD,DEL}PRL`, `SIOC{ADD,DEL}6RD` (`ip/tunnel.c:63`) | | `tuntap` add/del, and its flags, owner and group | `/dev/net/tun` plus `TUNSETIFF` and related ioctls (`ip/iptuntap.c:62`), and sysfs | -| `maddress` at 7.1.0 | `/proc` reads and `SIOCADDMULTI`/`SIOCDELMULTI` | +| `maddress` through the 7.2.0 pin | `/proc` reads and `SIOCADDMULTI`/`SIOCDELMULTI`; b8cdda55 (v7.3.0) moves `show` to an `RTM_GETMULTICAST` dump | | `netns add/del/exec` | `unshare`, bind mount, `setns` | | `vrf exec/identify/pids` | cgroup2 plus `bpf(BPF_PROG_LOAD)` (`ip/ipvrf.c:55`, `:288`) | | `route flush cache` | a write to `/proc/sys/net/ipv4/route/flush` (`ip/iproute.c:1649`) | diff --git a/docs/netlink/vrf-family-forms.md b/docs/netlink/vrf-family-forms.md new file mode 100644 index 00000000..bc29ce23 --- /dev/null +++ b/docs/netlink/vrf-family-forms.md @@ -0,0 +1,225 @@ +# Plan: `goip -4/-6/-0 vrf show NAME` — the family byte + +## Status + +**Last updated:** 2026-11-04. All three tasks have landed (VF001–VF003); +the plan is complete. This document is the plan and its own status. + +[vrf-show-name.md](vrf-show-name.md) parked the family forms in its +decision 8: "-4/-6 changes only the request's `ifi_family` byte on this +form — the kernel resolves the name without consulting the family — but +**no capture grounds those bytes**". This plan grounds them: three capture +rows, one guard deleted, three matrix rows. It is the parked increment +reopened, the smallest one left at the 7.2.0 pin — see the corrective note +in [goip-parity-gaps.md](goip-parity-gaps.md)'s class C (maddress waits for +the 7.3.0 pin bump). + +### Update rules + +- A task is `done` only when its "Done when" has passed, with evidence in + the checklist's Evidence column. +- Task IDs (`VFn-Tmm`) are stable and never renumbered. Evidence IDs are + `VF###`, so they cannot be confused with the `VS###`, `A###`, `DOC###` or + `MA###` ledgers of the other plans. +- A statement elsewhere in `docs/netlink/` that a task makes false is + amended in that task's commit. Two are named here: VF0 (this plan's own + commit) adds the README index row; VF2 amends + [coverage-status.md](coverage-status.md)'s + [parity-at-a-glance] table and the README counts. +- One task, one commit, as a rule. + +[parity-at-a-glance]: coverage-status.md#goip--ip-parity-at-a-glance + +### Task checklist + +| Done | Task | Work | State | Evidence | +|---|---|---|---|---| +| [x] | [VF0-T01](#vf0--the-captures-and-the-family-byte-pin) | the captures and the family-byte pin | done | VF001 | +| [x] | [VF1-T01](#vf1--the-object) | delete the guard, pass the family | done | VF002 | +| [x] | [VF2-T01](#vf2--the-matrix-rows-and-gating) | matrix rows and gating | done | VF003 | + +``` +VF0 ──> VF1 ──> VF2 +``` + +## Why this increment + +1. **The parked decision names it.** Decision 8's refusal exists *only* + because no capture grounds the family byte; this plan is the capture, + the guard deletion and nothing else. +2. **The kernel is family-blind on the get.** `rtnl_getlink`'s by-name arm + (`rtnl_dev_get_rcu`) resolves the device without consulting + `ifi_family` (`net/core/rtnetlink.c:4346-4353`), so all three forms + print the same `goipvrf 100` — the rows pin the request byte, not a new + answer shape. +3. **One guard, already plumbed.** `vrfShowName`'s refusal + (`internal/goip/obj_vrf.go:89-91`) is the whole delta; line 93 already + passes `c.family` to `VrfTableByName`, and the builder takes the family + (`req.VrfCheckByName`, `internal/goip/req/req.go:251`). + +## Reference sources and ground truth + +The pin is `ip` 7.2.0; kernel cites are the 7.3-rc6 tree. Both trees +read-only; cite `file:line` for every claim. + +| behavior | source | +|---|---| +| the request's `ifi_family` is `preferred_family`, the `-4/-6/-0` flag | `ip/iplink_vrf.c:110-113` (`ipvrf_get_table`) | +| the kernel resolves the name without consulting the family; ENODEV only when the name misses | `net/core/rtnetlink.c:4346-4355` | +| everything else — flags, `IFLA_IFNAME`, the answer's decode, the plain-line print — is the parked form's, byte for byte | `vrf-show-name.md`'s table, VS0–VS2's pins | +| the dump form's family refusal is separate and stays | `internal/goip/obj_vrf.go:142-148` | + +## Decisions + +1. **All three family forms, not two.** `-4` and `-6` pin AF_INET and + AF_INET6; `-0` pins AF_PACKET, the byte no other vrf request carries. + The guard covers all three at once, so refusing `-0` alone would be a + special case with no ground to stand on. +2. **Only the `goipvrf` name, not `default` or a non-VRF device.** The + ENODEV arm is family-blind — the kernel answers ENODEV before any family + question could exist — so the parked rows already ground those answers; + three more rows would re-pin one reply shape three times. +3. **The dump form's refusal is untouched.** `-4 vrf show`'s dump really + does change shape (a minimal inet6 link dump whose replies carry no + `IFLA_LINKINFO`), a form goip does not ground + (`obj_vrf.go:142-148`); only the NAME form's guard goes. + +## Phases and tasks + +### VF0 — The captures and the family-byte pin + +#### VF0-T01 Three rows, three namespaces + +**Depends on: nothing. Delivers testdata and the pin test; no production +code.** + +Three `capio` rows in `capture_set` (`nix/microvms/scripts/capture-netlink-dumps.exp`, +beside the `netlink_route_getvrf_name` row at `:354`), floor 2, run in +every namespace — mesh and tunnel hold no `goipvrf`, so the same rows +ground the ENODEV arm for free, exactly as the plain rows did: + +| row | command | request shape | +|---|---|---| +| `netlink_route_getvrf_name_v4` | `ip -4 vrf show goipvrf` | VS0's 44-byte get, `ifi_family` AF_INET | +| `netlink_route_getvrf_name_v6` | `ip -6 vrf show goipvrf` | the same, AF_INET6 | +| `netlink_route_getvrf_name_0` | `ip -0 vrf show goipvrf` | the same, AF_PACKET | + +Run `nix run .#microvm-x86_64-netlink-dump-capture -- --out ` and +commit only the new directories into `pkg/xtcpnl/testdata/7_3_0/dumps/` +(`git add` before any nix build). The pin test — +`TestVrfFamilyFormsRequestIsTheNameGet`, in +`internal/goip/obj_vrf_test.go` beside VS0's — is the commit's fixture +consumer: + +| description | expected | +|---|---| +| positive: each base pcap's recorded request | byte-identical to `req.VrfCheckByName(, "goipvrf", seq)`, `nlmsg_seq`/`nlmsg_pid` zeroed | +| boundary: the family is the only delta | each equals the plain form's recorded request (VS0's pinned bytes) in every byte but `ifi_family` | +| corner: the family byte's offset | byte 16 — the `ifinfomsg`'s first field, straight after the 16-byte `nlmsghdr` | +| positive: mesh and tunnel | the get and an `NLMSG_ERROR` ENODEV; the sidecar empty | +| boundary: each pcap's datagram count meets its floor | 2; a short capture is discarded, never committed | +| corner: the pin walks all three namespaces | every sidecar and pcap pair the driver wrote is pinned | + +Done when: the driver prints `NLCAP_DRIVER_PASS`, the pin is green, the +inherited gates pass (including `vm-lib-exp` — an `.exp` file changed), and +a scratch worktree mutant that hardcodes AF_UNSPEC in the family-arm pin +is killed by the byte-compare rows. + +### VF1 — The object + +#### VF1-T01 Delete the guard, pass the family + +**Depends on: VF0-T01.** + +Delete `vrfShowName`'s family refusal (`obj_vrf.go:89-91`) and the doc +comment's decision-8 paragraph (`:82-87`), leaving the dump form's refusal +alone. Extend `TestVrfShowName` — VS2's `-4 vrf show goipvrf` corner row +(refused, rc 1, nothing sent) flips to a positive row — and add +`TestVrfShowNameFamilyReplay` over VF0's pcaps; the corpus rows (three per +namespace, `cleanOnly`, beside the plain NAME row in +`internal/goip/corpus_7_3_0_test.go`) land in this commit, the first one +where goip can replay them: + +| description | expected | +|---|---| +| positive: `-4 vrf show goipvrf` | `goipvrf 100\n`, rc 0; the captured request carries AF_INET | +| positive: `-6` and `-0` likewise | the same line; AF_INET6 and AF_PACKET in the request | +| positive: the replay over the three pcaps | stdout byte-equal to the sidecar (`goipvrf 100\n` in base) | +| negative: the family forms in mesh and tunnel | `Invalid VRF\n` on stderr, rc 1, stdout empty — the ENODEV arm, family-blind | +| corner: the dump form still refuses `-4` | `vrf show` with a family stays refused, rc 1 — decision 3 | + +Done when: the tests are green, the corpus rows replay green, `go test +./internal/goip/...` is green, the inherited gates pass, and a scratch +worktree mutant that restores the guard is killed by exactly the three +family rows. + +### VF2 — The matrix rows and gating + +#### VF2-T01 Three rows, four clean runs + +**Depends on: VF1-T01.** + +Three read rows in `internal/goipparity/commands.go`'s vrf block — `-4 +vrf show goipvrf` (`vrf_show_name_v4`), `-6 vrf show goipvrf` +(`vrf_show_name_v6`) and `-0 vrf show goipvrf` (`vrf_show_name_0`), each +`Floor: 2, Implemented: true`, no `Setup`, no `Write`. Expected live +shape, the parked rows' own bar: `txns: ip=1 goip=1`, +`control: nl=0 stdout=0`. + +Run `nix run .#microvm-x86_64-goip-parity -- --timeout 32400` twice. After +two consecutive clean ungated runs: add the three names to +`gated_commands` (`pkg/nlparity/goip-parity-allowlist.json`); pin the rows +in `TestFamilyTableAndJSONRows` and with a cohort test +`TestVrfFamilyFormRowsGated` (following `TestVrfShowNameRowsGated`, +`internal/goipparity/commands_test.go:4684`) plus the earned list in +`TestAllowlistCommitted`; amend coverage-status's +[parity-at-a-glance] table and the README counts; then two consecutive +clean gated runs, back to back with no edit between them — the `-d` rows' +bar. + +| description | expected | +|---|---| +| positive: each form | both sides send the 44-byte get, family byte and all; both print `goipvrf 100` | +| boundary: a divergence in any run | reported and decided; a transaction-count divergence is unsuppressible (`nlparity_allowlist.go:143`) | +| corner: the ungated surface | `GOIP_PARITY_UNGATED_CLEAN` still reads `addr show dynamic` and nothing else | + +Done when: two consecutive ungated runs and the gated pair after them +print `GOIP_PARITY_DRIVER_PASS` and `GOIP_PARITY_OVERALL_PASS` with all +three rows `GOIP_PARITY_PASS`, the rows are gated with their cohort test +and earned-list entries green, the docs are amended, and the task's +checklist row and Evidence column here are flipped. + +## Gates every task inherits + +The table [vrf-show-name.md](vrf-show-name.md) carries, unchanged: +unit tests on the packages touched, `.#test-focused-goip`, netlink-audit, +lint-baseline, quality-report, the nix hygiene checks, `vm-lib-exp` when +an `.exp` file changes, the capture flavor's `NLCAP_DRIVER_PASS`, the +parity flavor's `GOIP_PARITY_DRIVER_PASS`, the conventions review, the +test-shape review, and a mutation check in a scratch worktree. + +## Verification ledger + +| Evidence ID | Date | Tasks / scope | Command or check and environment | Result | Evidence location | +|---|---|---|---|---|---| +| VF001 | 2026-11-04 | VF0-T01 | `nix run .#microvm-x86_64-netlink-dump-capture -- --out /tmp/vf0-cap/dumps` on the host: `NLCAP_DRIVER_PASS`, all nine new rows `datagrams=2 floor=2` — base stdout 12 bytes (`goipvrf 100\n`), mesh and tunnel 0 (`Invalid VRF` is stderr, which read rows drop). `go test ./internal/goip/ -count=1` and `go vet`: green, `TestVrfFamilyFormsRequestIsTheNameGet` 9/9 subtests — each recorded get byte-identical to `req.VrfCheckByName(, goipvrf)` with seq/pid zeroed, the family byte the only delta from the plain form's built request, base's reply `RTM_NEWLINK` and mesh/tunnel's `NLMSG_ERROR` `-ENODEV`. `nix build --no-link` of `.#test-focused-goip`, `.#checks.x86_64-linux.vm-lib-exp`, `.#checks.x86_64-linux.netlink-audit`, `.#checks.x86_64-linux.lint-baseline`, `.#quality-report`, and the nix-fmt, statix and deadnix checks: green. Mutation check in a scratch worktree, removed after — `VrfCheckByName`'s family parameter overwritten to hardcode AF_UNSPEC — killed exactly the nine byte-compare subtests and nothing else (`TestVrfShowNameRequestIsNameIsVrf`, `TestVrfShowName`, `TestVrfShowNameReplay` and `TestCorpus730MatchesSidecarsAt720` stayed green), and was reverted. | Passed: 9/9 rows and 9/9 subtests; the gates green; the mutant caught | the three capio rows in `nix/microvms/scripts/capture-netlink-dumps.exp`; the 18 new files under `pkg/xtcpnl/testdata/7_3_0/dumps/{,mesh/,tunnel/}`; `TestVrfFamilyFormsRequestIsTheNameGet` in `internal/goip/obj_vrf_test.go` | +| VF002 | 2026-11-04 | VF1-T01 | The guard deleted from `vrfShowName` (`obj_vrf.go`), the doc comment amended to name the family pass-through and the dump form's distinct refusal. `go test ./internal/goip/... -count=1` and `go vet`: green — `TestVrfShowName`'s `-4` corner row flipped to a positive row (`goipvrf 100\n`, rc 0, one Talk); three family rows in `TestVrfShowNameReplay` (the planned `TestVrfShowNameFamilyReplay` folded into the existing table instead — one table, one walk), each replaying its own capture through the family-blind dump fallback; three `cleanOnly` corpus rows in `corpus_7_3_0_test.go`, mesh and tunnel refusing with `Invalid VRF` as the plain NAME row does. `nix build --no-link` of `.#test-focused-goip`, `.#checks.x86_64-linux.netlink-audit`, `.#checks.x86_64-linux.lint-baseline`, `.#quality-report`, and the nix-fmt, statix and deadnix checks: green. Mutation check in a scratch worktree, removed after: the guard restored verbatim failed exactly the seven family rows — `TestVrfShowName`'s flipped positive row, the three `TestVrfShowNameReplay` family rows and the three `TestCorpus730MatchesSidecarsAt720` clean-namespace subtests — and nothing else, and was reverted. No VM runs; no `.exp` change. | Passed: the flipped row, 3/3 replay rows, 3/3 corpus rows; the gates green; the mutant caught | `vrfShowName` in `internal/goip/obj_vrf.go`; the flipped row and the family replay rows in `internal/goip/obj_vrf_test.go`; the three corpus rows in `internal/goip/corpus_7_3_0_test.go` | +| VF003 | 2026-11-04 | VF2-T01 | Four live runs of `nix run .#microvm-x86_64-goip-parity` on the host: two with the rows ungated, then, after the gating edits, two back to back with **no edit of any kind between them**. All four read `GOIP_PARITY_DRIVER_PASS` and `GOIP_PARITY_OVERALL_PASS` with **513/513 `GOIP_PARITY_PASS`**, 0 FAIL, `GOIP_PARITY_UNGATED_CLEAN`; each of the three rows `txns: ip=1 goip=1`, `control: nl=0 stdout=0`, the answer and stdout the plain NAME row's own. The three names join `gated_commands` (512 now) and the earned slice; `TestVrfFamilyFormRowsGated` pins the flip (the dump form's `-4 vrf show` is not in the matrix), the three family rows join `TestFamilyTableAndJSONRows`, and `TestUngatedSurfaceIsNotVacuous` is back to exactly `addr show dynamic`. `go test ./internal/goipparity/ ./pkg/nlparity/ -count=1` and `go vet`: green. `nix build --no-link` of `.#test-focused-goip`, `.#checks.x86_64-linux.netlink-audit`, `.#checks.x86_64-linux.lint-baseline`, `.#quality-report`, and the nix-fmt, statix and deadnix checks: green. Mutation check in a scratch worktree, removed after: the `-6 vrf show goipvrf` row dropped from `commands.go` failed exactly the `-6` family row of `TestFamilyTableAndJSONRows`, the `-6` row of `TestVrfFamilyFormRowsGated`, and the gated-command-is-in-the-table corners of `TestCommandsCoverAllowlist` and `TestObjectGatingStatus`, and was reverted. coverage-status's at-a-glance (513 commands / 512 gated, counted from 2026-11-04) and the README counts (513/512) amended in this commit. | Passed: 4/4 live runs clean; the gates green; the mutant caught | the three rows in `internal/goipparity/commands.go`; the family rows and `TestVrfFamilyFormRowsGated` in `internal/goipparity/commands_test.go`; the three names in `pkg/nlparity/goip-parity-allowlist.json` and the earned slice in `pkg/nlparity/nlparity_allowlist_test.go` | + +## Out of scope + +- **`-4/-6/-0 vrf show` (the dump form)** — the family changes the dump's + shape (decision 3). +- **The family forms of `vrf show default` or a non-VRF device** — the + ENODEV arm is family-blind (decision 2). +- **maddress at this pin** — class F until the 7.3.0 pin bump + (`goip-parity-gaps.md`'s corrected class C note). + +## See also + +- [vrf-show-name.md](vrf-show-name.md) — the parked increment this reopens; + VS0–VS2's pins and the decision-8 text this plan deletes. +- [coverage-status.md](coverage-status.md) — the tracker; the + [parity-at-a-glance] table VF2 amends. +- [goip-parity-gaps.md](goip-parity-gaps.md) — the corrected class C note + that closed the maddress door at this pin. diff --git a/internal/goip/corpus_7_3_0_test.go b/internal/goip/corpus_7_3_0_test.go index 60c45581..7d2d7f20 100644 --- a/internal/goip/corpus_7_3_0_test.go +++ b/internal/goip/corpus_7_3_0_test.go @@ -122,6 +122,12 @@ func TestCorpus730MatchesSidecarsAt720(t *testing.T) { args: []string{"vrf", "show"}, pcap: "netlink_route_getvrf", sidecar: "ip_vrf"}, {category: "positive", description: "vrf show goipvrf, ipvrf_get_table's single lookup", expected: "byte-identical", args: []string{"vrf", "show", "goipvrf"}, pcap: "netlink_route_getvrf_name", sidecar: "ip_vrf_name", cleanOnly: true}, + {category: "positive", description: "-4 vrf show goipvrf, the family byte reaching the request", expected: "byte-identical", + args: []string{"-4", "vrf", "show", "goipvrf"}, pcap: "netlink_route_getvrf_name_v4", sidecar: "ip_vrf_name_v4", cleanOnly: true}, + {category: "positive", description: "-6 vrf show goipvrf likewise", expected: "byte-identical", + args: []string{"-6", "vrf", "show", "goipvrf"}, pcap: "netlink_route_getvrf_name_v6", sidecar: "ip_vrf_name_v6", cleanOnly: true}, + {category: "corner", description: "-0 vrf show goipvrf, the AF_PACKET byte no other vrf request carries", expected: "byte-identical", + args: []string{"-0", "vrf", "show", "goipvrf"}, pcap: "netlink_route_getvrf_name_0", sidecar: "ip_vrf_name_0", cleanOnly: true}, {category: "corner", description: "vrf show default, ENODEV's main-table arm", expected: "byte-identical in every namespace", args: []string{"vrf", "show", "default"}, pcap: "netlink_route_getvrf_default", sidecar: "ip_vrf_default"}, {category: "negative", description: "vrf show $dev, an existing non-VRF device", expected: "goip refuses as ip did: non-zero exit, nothing on stdout", diff --git a/internal/goip/obj_vrf.go b/internal/goip/obj_vrf.go index 367d3009..05ec6aee 100644 --- a/internal/goip/obj_vrf.go +++ b/internal/goip/obj_vrf.go @@ -77,18 +77,13 @@ func parseVrfShowArgs(args []string) (string, error) { // ipvrf_get_table lookup, its table printed as "NAME table" with the name as // typed. The branch returns before new_json_obj is ever called, so -j, -br and // -o change nothing here — the plain line is printed under every output mode -// (decision 4, docs/netlink/vrf-show-name.md). -// -// The family refusal is the NAME form's own (decision 8): -4/-6 changes only -// the request's ifi_family byte on this form — the kernel resolves the name -// without consulting the family (net/core/rtnetlink.c:4346-4353) — but no -// capture grounds those bytes, so the form stays refused and the message names -// it, keeping the dump form's refusal (the family changes the dump's shape) -// distinct. +// (decision 4, docs/netlink/vrf-show-name.md). The family flag passes through +// to the request's ifi_family byte (decision 1, docs/netlink/ +// vrf-family-forms.md): the kernel resolves the name without consulting the +// family (net/core/rtnetlink.c:4346-4355), so -4, -6 and -0 print the same +// line the plain form does, and the dump form's family refusal stays distinct +// (the family changes the dump's shape). func vrfShowName(c *runCtx, name string) error { - if c.family != unix.AF_UNSPEC { - return fmt.Errorf("vrf show NAME with -4/-6: family selection changes only the request's ifi_family byte, a form no capture grounds: %w", ErrNotImplemented) - } svc := service.New(c.src, c.nextSeq) table, err := svc.VrfTableByName(c.family, name) if err != nil { diff --git a/internal/goip/obj_vrf_test.go b/internal/goip/obj_vrf_test.go index 064f6186..6586e98a 100644 --- a/internal/goip/obj_vrf_test.go +++ b/internal/goip/obj_vrf_test.go @@ -564,16 +564,17 @@ func TestVrfShowName(t *testing.T) { wantExit: ExitMinusOne, }, { - // The family changes only the request's ifi_family byte on this - // form, but no capture grounds those bytes, so the form stays refused - // (decision 8) and nothing is sent. - description: "corner: -4 vrf show goipvrf", - expected: "refused, rc 1, nothing sent", + // The family flag is no longer refused (vrf-family-forms.md VF1): + // the byte reaches the request and the answer is the plain form's + // own, so the fake source answers it the same way. + description: "positive: -4 vrf show goipvrf", + expected: "goipvrf 100\n, rc 0 — the family byte reaches the request, the answer is the plain form's", args: []string{"show", "goipvrf"}, links: map[string]vrfNameLink{"goipvrf": {kind: "vrf", table: 100}}, family: unix.AF_INET, - wantErrSub: "vrf show NAME with -4/-6", - wantExit: ExitUsage, + wantStdout: "goipvrf 100\n", + wantTalks: 1, + wantExit: ExitOK, }, } for _, tc := range tests { @@ -639,6 +640,32 @@ func TestVrfShowNameReplay(t *testing.T) { args: []string{"vrf", "show", "goipvrf"}, wantStdout: "goipvrf 100\n", }, + { + // The family forms (VF1): under replay the dump fallback answers, and + // it is family-blind — the fallback's dump finds each capture's one + // recorded RTM_NEWLINK answer, the get's own, exactly as the nexthop + // row above does. The family byte reaches the request only live; the + // VF0 pin holds it. + description: "positive: -4 vrf show goipvrf over the -4 capture", + expected: "goipvrf 100\n through the family-blind dump fallback", + pcap: corpus730Dir + "netlink_route_getvrf_name_v4.pcap", + args: []string{"-4", "vrf", "show", "goipvrf"}, + wantStdout: "goipvrf 100\n", + }, + { + description: "positive: -6 vrf show goipvrf over the -6 capture", + expected: "the same with AF_INET6", + pcap: corpus730Dir + "netlink_route_getvrf_name_v6.pcap", + args: []string{"-6", "vrf", "show", "goipvrf"}, + wantStdout: "goipvrf 100\n", + }, + { + description: "corner: -0 vrf show goipvrf over the -0 capture", + expected: "the same with AF_PACKET, the byte no other vrf request carries", + pcap: corpus730Dir + "netlink_route_getvrf_name_0.pcap", + args: []string{"-0", "vrf", "show", "goipvrf"}, + wantStdout: "goipvrf 100\n", + }, } for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { @@ -654,3 +681,105 @@ func TestVrfShowNameReplay(t *testing.T) { }) } } + +// TestVrfFamilyFormsRequestIsTheNameGet pins VF0's three family rows: -4/-6/-0 +// changes only the request's ifi_family byte (iplink_vrf.c:110-113), so each +// recorded get must be req.VrfCheckByName's bytes for its family, and the +// plain form's built request in every other byte. The kernel resolves the name +// without consulting the family (rtnetlink.c:4346-4355): base's answer is the +// vrf link itself, mesh's and tunnel's an NLMSG_ERROR ENODEV, and the family +// byte is the whole difference the rows carry. No hand bytes: every row is a +// capture or the builder. +// +// go test ./internal/goip/ -run TestVrfFamilyFormsRequestIsTheNameGet +func TestVrfFamilyFormsRequestIsTheNameGet(t *testing.T) { + tests := []struct { + name, file, category, description, expected string + family uint8 + }{ + {name: "v4", file: "v4", category: "positive", + description: "the -4 get, ifi_family AF_INET", + expected: "byte-identical to req.VrfCheckByName(AF_INET, goipvrf); the plain form's bytes elsewhere", + family: unix.AF_INET}, + {name: "v6", file: "v6", category: "positive", + description: "the -6 get, ifi_family AF_INET6", + expected: "the same with AF_INET6", + family: unix.AF_INET6}, + {name: "af0", file: "0", category: "corner", + description: "the -0 get, ifi_family AF_PACKET, the byte no other vrf request carries", + expected: "the same with AF_PACKET", + family: unix.AF_PACKET}, + } + for _, tc := range tests { + for _, ns := range corpus730Namespaces { + name := strings.TrimSuffix(ns.subdir, "/") + if name == "" { + name = "clean" + } + t.Run(tc.name+"/"+name, func(t *testing.T) { + t.Logf("%s: %s in the %s namespace; expected: %s", tc.category, tc.description, name, tc.expected) + pcap := corpus730Dir + ns.subdir + "netlink_route_getvrf_name_" + tc.file + ".pcap" + capt, err := nlparity.ParseRouteCaptureFile(pcap) + if err != nil { + t.Fatal(err) + } + msgs := capt.Msgs() + var gets []int + for i, m := range msgs { + if m.IsRequest() && m.Hdr.Type == unix.RTM_GETLINK { + gets = append(gets, i) + } + } + if len(gets) != 1 { + t.Fatalf("%s: %d RTM_GETLINK requests, want the one get", name, len(gets)) + } + captured := capturedRequest(msgs[gets[0]]) + if len(captured) != 44 { + t.Errorf("%s: recorded get is %d bytes, want 44", name, len(captured)) + } + built, err := req.VrfCheckByName(tc.family, "goipvrf", 1) + if err != nil { + t.Fatal(err) + } + if got := zeroSeqPidCopy(built); !bytes.Equal(got, captured) { + t.Errorf("%s: %s get differs\n got %x\nwant %x", name, tc.name, got, captured) + } + if captured[16] != tc.family { + t.Errorf("%s: family byte at offset 16 is %d, want %d", name, captured[16], tc.family) + } + // The family is the only delta: the plain form's built request, + // which VS0 pinned byte for byte, equals this one elsewhere. + plain, err := req.VrfCheckByName(unix.AF_UNSPEC, "goipvrf", 1) + if err != nil { + t.Fatal(err) + } + zeroed := zeroSeqPidCopy(plain) + for i := range captured { + if captured[i] != zeroed[i] && i != 16 { + t.Errorf("%s: byte %d is %d, want the plain form's %d — the family is the only delta", + name, i, captured[i], zeroed[i]) + } + } + // rtnl_talk is synchronous: the reply follows the get. The + // kernel answers the vrf link in the clean namespace and + // ENODEV in mesh and tunnel, which hold no goipvrf. + reply := msgs[gets[0]+1] + if reply.IsRequest() { + t.Fatalf("%s: the datagram after the get is a request", name) + } + if ns.subdir == "" { + if reply.Hdr.Type != unix.RTM_NEWLINK { + t.Errorf("%s: reply type %d, want RTM_NEWLINK", name, reply.Hdr.Type) + } + return + } + if reply.Hdr.Type != unix.NLMSG_ERROR { + t.Fatalf("%s: reply type %d, want NLMSG_ERROR", name, reply.Hdr.Type) + } + if errno := int32(binary.LittleEndian.Uint32(reply.Body[0:4])); errno != -int32(unix.ENODEV) { + t.Errorf("%s: NLMSG_ERROR errno %d, want -ENODEV", name, errno) + } + }) + } + } +} diff --git a/internal/goipparity/commands.go b/internal/goipparity/commands.go index 042eccea..8faa213a 100644 --- a/internal/goipparity/commands.go +++ b/internal/goipparity/commands.go @@ -2373,6 +2373,28 @@ var commands = withArgs([]Command{ // ground. Floor: 2, Implemented: true, }, + { + Name: "-4 vrf show goipvrf", Slug: "vrf_show_name_v4", + // The parked NAME get's family forms (vrf-family-forms.md): -4/-6/-0 + // changes only the request's ifi_family byte (iplink_vrf.c:110-113) — + // the kernel resolves the name without consulting the family + // (rtnetlink.c:4346-4355), so the answer and stdout are the plain + // row's own, `goipvrf 100`, and the floor with it. VF0's captures + // ground the bytes; VF1 deleted the guard. + Floor: 2, Implemented: true, + }, + { + Name: "-6 vrf show goipvrf", Slug: "vrf_show_name_v6", + // The AF_INET6 twin: the same get with the v6 family byte, the same + // answer, the same floor. + Floor: 2, Implemented: true, + }, + { + Name: "-0 vrf show goipvrf", Slug: "vrf_show_name_0", + // The AF_PACKET corner: the family byte no other vrf request carries; + // the kernel answers it exactly as the others. + Floor: 2, Implemented: true, + }, // The link write rows (link-write-path.md phase 7): the 21 rows of the // committed corpus in pkg/xtcpnl/testdata/7_1_4/link-write/write/, with // the same preconditions and floors as capture-link-writes.exp. All 21 diff --git a/internal/goipparity/commands_test.go b/internal/goipparity/commands_test.go index b848eaa7..504d4435 100644 --- a/internal/goipparity/commands_test.go +++ b/internal/goipparity/commands_test.go @@ -874,6 +874,27 @@ func TestFamilyTableAndJSONRows(t *testing.T) { floor: 2, implemented: true, }, + { + description: "positive: -4 vrf show goipvrf, the NAME get with the family byte, the kernel family-blind on the lookup", + name: "-4 vrf show goipvrf", + slug: "vrf_show_name_v4", + floor: 2, + implemented: true, + }, + { + description: "positive: -6 vrf show goipvrf, the AF_INET6 twin, the same answer and floor", + name: "-6 vrf show goipvrf", + slug: "vrf_show_name_v6", + floor: 2, + implemented: true, + }, + { + description: "corner: -0 vrf show goipvrf, the AF_PACKET byte no other vrf request carries", + name: "-0 vrf show goipvrf", + slug: "vrf_show_name_0", + floor: 2, + implemented: true, + }, { description: "positive: vrf show default, the ENODEV arm both sides resolve to the main table", name: "vrf show default", @@ -4724,3 +4745,47 @@ func TestVrfShowNameRowsGated(t *testing.T) { }) } } + +// TestVrfFamilyFormRowsGated pins VF2's three family rows: implemented, on no +// Setup (the NAME form resolves nothing up front), each floor 2 — the one +// RTM_GETLINK get by name, family byte and all, and its answer — and gated +// with no allowlist entry after the runs. The plain NAME row they sit beside +// stays gated beside them, and a family form of the dump twin is not in the +// matrix at all. +// +// go test ./internal/goipparity/ -run TestVrfFamilyFormRowsGated +func TestVrfFamilyFormRowsGated(t *testing.T) { + al, err := nlparity.EmbeddedAllowlist() + if err != nil { + t.Fatalf("EmbeddedAllowlist: %v", err) + } + type want struct { + implemented, gated, setup bool + floor, entries int + } + gated := want{implemented: true, gated: true, floor: 2} + rows := []struct { + description string + command string + expected want + }{ + {"positive: -4, the AF_INET family byte", "-4 vrf show goipvrf", gated}, + {"positive: -6, the AF_INET6 twin", "-6 vrf show goipvrf", gated}, + {"corner: -0, the AF_PACKET byte no other vrf request carries", "-0 vrf show goipvrf", gated}, + {"boundary: the plain NAME row stays gated beside them", "vrf show goipvrf", want{implemented: true, gated: true, floor: 2}}, + {"negative: the dump form's family twin is not in the matrix", "-4 vrf show", want{}}, + } + for _, r := range rows { + t.Run(r.description, func(t *testing.T) { + var got want + if c, lookupErr := Lookup(r.command); lookupErr == nil { + got.implemented, got.setup, got.floor = c.Implemented, len(c.Setup) > 0, c.Floor + } + got.gated = al.IsGated(r.command) + got.entries = len(al.EntriesFor(r.command)) + if got != r.expected { + t.Errorf("%q: got %+v, want %+v", r.command, got, r.expected) + } + }) + } +} diff --git a/nix/microvms/netlink-capture.nix b/nix/microvms/netlink-capture.nix index e4921baf..f6ec4057 100644 --- a/nix/microvms/netlink-capture.nix +++ b/nix/microvms/netlink-capture.nix @@ -142,8 +142,9 @@ let # captures (docs/netlink/addr-write-path.md, table 6). Those are the only # replies that carry IFA_MULTICAST, IFA_ANYCAST and IFA_MC_USERS, and the # pinned `ip` never asks for them: `ip maddr` reads /proc until iproute2 - # b8cdda55, which is in no release, and nothing in iproute2 sends - # RTM_GETANYCAST. So this sends the request itself, the way + # b8cdda55, which is first released in v7.3.0 (after this pin), and + # nothing in iproute2 sends RTM_GETANYCAST. So this sends the request + # itself, the way # rtnl_mcaddrdump_req (lib/libnetlink.c:338) builds it: an nlmsghdr with # NLM_F_REQUEST | NLM_F_DUMP and a zeroed ifaddrmsg naming the family. # diff --git a/nix/microvms/scripts/capture-netlink-dumps.exp b/nix/microvms/scripts/capture-netlink-dumps.exp index b98c416d..5c36cc15 100644 --- a/nix/microvms/scripts/capture-netlink-dumps.exp +++ b/nix/microvms/scripts/capture-netlink-dumps.exp @@ -352,6 +352,15 @@ proc capture_set {ns subdir dev} { # rc and the stderr line stay unit assertions (VS2); these rows ground the # request and answer bytes. capio $ns $subdir netlink_route_getvrf_name 2 "ip vrf show goipvrf" ip_vrf_name + # The family forms of the NAME get (vrf-family-forms.md VF0): -4/-6/-0 + # changes only the request's ifi_family byte (iplink_vrf.c:110-113) — the + # kernel resolves the name without consulting the family + # (rtnetlink.c:4346-4355), so every answer here is the plain row's own: + # goipvrf 100 in base, ENODEV in mesh and tunnel, and the family byte is + # all the rows add. Floor 2, the get and its answer. + capio $ns $subdir netlink_route_getvrf_name_v4 2 "ip -4 vrf show goipvrf" ip_vrf_name_v4 + capio $ns $subdir netlink_route_getvrf_name_v6 2 "ip -6 vrf show goipvrf" ip_vrf_name_v6 + capio $ns $subdir netlink_route_getvrf_name_0 2 "ip -0 vrf show goipvrf" ip_vrf_name_0 capio $ns $subdir netlink_route_getvrf_default 2 "ip vrf show default" ip_vrf_default capio $ns $subdir netlink_route_getvrf_novrf 2 "ip vrf show $dev" ip_vrf_novrf diff --git a/pkg/nlparity/goip-parity-allowlist.json b/pkg/nlparity/goip-parity-allowlist.json index 8605c1ec..1cdbceeb 100644 --- a/pkg/nlparity/goip-parity-allowlist.json +++ b/pkg/nlparity/goip-parity-allowlist.json @@ -1251,6 +1251,9 @@ "vrf show goipvrf", "vrf show default", "vrf show goip0", + "-4 vrf show goipvrf", + "-6 vrf show goipvrf", + "-0 vrf show goipvrf", "nexthop show", "-j nexthop show", "nexthop show dev goip0", diff --git a/pkg/nlparity/nlparity_allowlist_test.go b/pkg/nlparity/nlparity_allowlist_test.go index 5777a057..181cca4e 100644 --- a/pkg/nlparity/nlparity_allowlist_test.go +++ b/pkg/nlparity/nlparity_allowlist_test.go @@ -404,6 +404,17 @@ func TestAllowlistCommitted(t *testing.T) { "vrf show goipvrf", "vrf show default", "vrf show goip0", + // The three `vrf show NAME` family rows (VF2 of + // docs/netlink/vrf-family-forms.md, the parked decision-8 increment) + // - gated on two consecutive clean ungated runs measured 2026-11-04, + // both GOIP_PARITY_PASS 513/513 with GOIP_PARITY_UNGATED_CLEAN, txns + // 1/1, control nl=0 stdout=0, the answer and stdout the plain NAME + // row's own — the family byte is the whole difference the rows carry + // (iplink_vrf.c:110-113; the kernel resolves the name without + // consulting the family, rtnetlink.c:4346-4355). + "-4 vrf show goipvrf", + "-6 vrf show goipvrf", + "-0 vrf show goipvrf", // nexthop and addrlabel, gated on runs 10 and 11 - the first live // grounding of those two objects, which were replay-grounded only. // Both runs GOIP_PARITY_PASS with stdout=0 and no unsuppressed findings, diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_0 b/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_0 new file mode 100644 index 00000000..21ee2858 --- /dev/null +++ b/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_0 @@ -0,0 +1 @@ +goipvrf 100 diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_v4 b/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_v4 new file mode 100644 index 00000000..21ee2858 --- /dev/null +++ b/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_v4 @@ -0,0 +1 @@ +goipvrf 100 diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_v6 b/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_v6 new file mode 100644 index 00000000..21ee2858 --- /dev/null +++ b/pkg/xtcpnl/testdata/7_3_0/dumps/ip_vrf_name_v6 @@ -0,0 +1 @@ +goipvrf 100 diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/ip_vrf_name_0 b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/ip_vrf_name_0 new file mode 100644 index 00000000..e69de29b diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/ip_vrf_name_v4 b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/ip_vrf_name_v4 new file mode 100644 index 00000000..e69de29b diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/ip_vrf_name_v6 b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/ip_vrf_name_v6 new file mode 100644 index 00000000..e69de29b diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_0.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_0.pcap new file mode 100644 index 00000000..a0a27acb Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_0.pcap differ diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_v4.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_v4.pcap new file mode 100644 index 00000000..2ddd4312 Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_v4.pcap differ diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_v6.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_v6.pcap new file mode 100644 index 00000000..a3a13a42 Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/mesh/netlink_route_getvrf_name_v6.pcap differ diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_0.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_0.pcap new file mode 100644 index 00000000..23756373 Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_0.pcap differ diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_v4.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_v4.pcap new file mode 100644 index 00000000..db566667 Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_v4.pcap differ diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_v6.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_v6.pcap new file mode 100644 index 00000000..11f73700 Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/netlink_route_getvrf_name_v6.pcap differ diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/ip_vrf_name_0 b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/ip_vrf_name_0 new file mode 100644 index 00000000..e69de29b diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/ip_vrf_name_v4 b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/ip_vrf_name_v4 new file mode 100644 index 00000000..e69de29b diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/ip_vrf_name_v6 b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/ip_vrf_name_v6 new file mode 100644 index 00000000..e69de29b diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_0.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_0.pcap new file mode 100644 index 00000000..fa57d83c Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_0.pcap differ diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_v4.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_v4.pcap new file mode 100644 index 00000000..190a68f6 Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_v4.pcap differ diff --git a/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_v6.pcap b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_v6.pcap new file mode 100644 index 00000000..c21d60e0 Binary files /dev/null and b/pkg/xtcpnl/testdata/7_3_0/dumps/tunnel/netlink_route_getvrf_name_v6.pcap differ