Skip to content

goip link kinds, write keywords and lwt encaps, with the nl_mon workbench (1373 parity rows) - #287

Merged
randomizedcoder merged 163 commits into
mainfrom
goip-kinds/k0
Nov 9, 2026
Merged

randomizedcoder merged 163 commits into
mainfrom
goip-kinds/k0

Conversation

@randomizedcoder

Copy link
Copy Markdown
Owner

Summary

goip's link kinds, write keywords and lwt encaps, plus the nl_mon workbench that captures them. There are 161 commits, one per task, and each plan has a status doc under docs/netlink/.

  • Link kinds (goip-link-kinds.md; KC, KP, KG):

    • KC: write captures for every kind iproute2 7.2.0 can create, and the link set … inet form.
    • KP: an xtcpnl codec per kind.
    • KG: goip's words and -d output for bridge, the VLAN family, the UDP overlays, the IP tunnels, hsr, netkit, vrf, vxcan, batadv and the kinds with no parser.
  • Write keywords (goip-write-keywords.md; GW1–GW5): route nhid, pref, expires, tos, realms, vrf and multipath; link group, GSO/GRO, addrgenmode, protodown, xdp off, index and netnsid; nexthop hw_stats and encap mpls; rule realms.

  • Live forms (goip-live-forms.md; LF1–LF3): live rows for the keyword forms that only O5 had checked.

  • lwt encaps (goip-lwt-encap.md, goip-lwt-srv6.md; LE, LX): encap ip, ip6, seg6, seg6local, ila, xfrm, ioam6 and rpl, on a route, a multipath hop and a nexthop object.

  • Workbench (W0–W5): an ssh-driven nl_mon VM that runs generated JSONL rows from:

    • tdc tc cases;
    • pairwise matrices for route, rule, nexthop, address, neigh and link;
    • fib-selftest rows;
    • bridge fdb/mdb, ss and ethtool.

    Each row is checked by oracles O1–O7, and W4 adds a coverage report against the kernel's policy tables. The rows produce 9468 pcaps under pkg/xtcpnl/testdata/*/ws/, within the corpus budget (64 MB).

  • BIRD (B2–B4): bridge FDB, VLAN and MPLS captures and parsers.

  • Other fixes:

    • nlparity compares a nested attribute past its padding (3f4b704).
    • Lint: the gosec suppressions are replaced with checked conversions (78ae084), and docs/static-analysis.md records the 474 G115 sites the exclusion hides.
    • xtcp2's soft restart carries its secrets in a sealed memfd and rebuilds argv from flags (01c37b6).
    • linkmonitor digests replayed values without their zero fields (4da0e47).

Coverage

  • Parity matrix: 1373 rows, up from 510; 1372 are gated, up from 509. The one ungated row is still addr show dynamic.
  • Allowlist: unchanged at 5 entries (the ip netns FDs).

Test plan

  • Every task:
    • table-driven unit tests;
    • go vet;
    • nix build --no-link of the focused tests, netlink-audit, lint-baseline and the nix hygiene checks;
    • a mutation check in a scratch worktree.
  • Gating: each family's rows were gated only after two clean ungated goip-parity section runs.
  • On e74898b, before this push:
    • a full goip-parity VM run: GOIP_PARITY_DRIVER_PASS, GOIP_PARITY_OVERALL_PASS, 1373/1373 GOIP_PARITY_PASS and GOIP_PARITY_UNGATED_CLEAN;
    • nix build .#checks.x86_64-linux.test-go-race: pass.
  • The branch merges cleanly with main at 97bed69 (release 0.2.1).

🤖 Generated with Claude Code

randomizedcoder and others added 30 commits October 24, 2026 10:22
…nk-kinds)

goip-parity-gaps.md is finished, but its inventory counted objects and
verbs, not link kinds: iproute2 7.2.0 has 39 iplink_*.c/link_*.c files and
goip covers bond, vrf, dummy and veth. goip-link-kinds.md plans the 34 kinds
a VM can create with a module, plus the one new write on the 7.2.0 pin,
`ip link set dev X inet <devconf>…`, in the same three tiers (KC captures,
KP parsers, KG goip and the matrix). goip-link-kinds-status.md tracks it
(DOC001).

KC0-T01 is planned in full: the kind modules join late_setup, which first
sets net.core.fb_tunnels_only_for_init_net=2 so the namespaces the
link-write driver builds afterwards get no fallback tunnels; the dump
driver's tunnel set, which wants them, loads its modules earlier and is
unchanged. A non-fatal `xtcp2-nlcap kinds` probe records which kinds the
guest kernel creates.

Corrects goip-parity-gaps.md: maddress moved to RTM_GETMULTICAST in
b8cdda55, after v7.2.0, so it is still class F at the pin. Notes the pin
bump there, points link-write-path.md's "small follow-up" at the new plan,
and indexes both new docs in docs/netlink/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… close KC0 (KC0-T01)

late_setup now sets net.core.fb_tunnels_only_for_init_net=2, loads the
link-kind modules after every existing row, checks that a namespace made
afterwards holds only lo and nlmon0, and runs a new `xtcp2-nlcap kinds`
probe whose late/kinds sidecar records which kinds the guest kernel
creates: 31 of 32 on 7.3.0, gtp refused (KC4).

The link-write driver's one failure, genl/mptcp_limits_set_over, predates
this task (kernel c8646664fbf1 raised the MPTCP limit to 64) and fails
identically at 34724ad. KC1 is broken into KC1-T01 with its tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1-T01)

18 `ip link set dev d0 inet ...` rows in the rtnl corpus: one on/off and
one numeric param, hex and octal values, medium_id's signed minimum, the
kernel's range refusals, several and all 32 params in one nest, a repeated
param, the push-back to iplink_parse, and seven refusals ip makes before
netlink. Those exit with 255 (exit(-1)) or 1 (parse_on_off's error,
negated by do_cmd), which the hermetic check now admits with floor 0 and
no readback.

KC2 is broken into KC2-T01 (the kind corpus, and bridge) and KC2-T02
(bridge_slave), with their tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kind-write-corpus.exp is the link kinds' write corpus: rows of
{slug floor pre command readbacks rc}, rtnl-write-corpus.exp's shape,
which capture-link-writes.exp runs into link-write/kinds/<kind>/<name>/
after its xfrm rows, once late setup has loaded the kind modules. Each
row the kernel answers reads the device back three ways (ip -d, ip -d -j
-p and ip link show).

bridge has 60 rows: the default, one add per value-taking option (39),
fdb_flush, 7.2.0's three stp_mode names, the boolopts, several options
in one add, four sets (one refused while STP runs), the abbreviation
rules, four kernel refusals and four refusals by ip. kind-write-corpus-
test.exp (34 rows, run by vm-lib-exp) pins them.

The first VM run found that bridge_parse_opt's returned errors exit 1,
not 255; the corpus and its test now say so, and the second run holds
every status, with all 39 options reading back as sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kind-write-corpus.exp gets 42 bridge_slave rows after bridge's, each an
`ip link set dev p0 type bridge_slave …` on a dummy enslaved to br0
with p1 beside it: one set per value-taking port option (24), the two
flags, 7.2.0's neigh_forward_grat, backup_port by name, the abbreviation
rules, seven kernel refusals (state, priority and cost ranges, the MAC
pause bit, MAB unlocked, a port backing itself up, a non-port) and four
by ip. kind-write-corpus-test.exp grows to 51 rows.

The VM run holds every planned status on the first try; every option
but state reads back as sent (with STP off the bridge forwards any
enabled port, so state 4 reads back forwarding).

KC2 is closed, and KC3 is broken into vlan, macvlan/macvtap and
ipvlan/ipvtap tasks with their tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
28 vlan rows in the kind corpus, stacked on a dummy d0: an add per
option and flag, the qos maps, the id range, three sets (vlan_changelink
ignores the id), the kernel's six refusals and ip's seven. The VM run
held every planned status on the first try, and every value read back
as sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 macvlan rows and 6 macvtap rows in the kind corpus, one parser for
two kinds: each mode, both flag spellings, bcqueuelen and bclim (an
s32), the source list added, set, deleted and flushed, the kernel's
five refusals (passthru changed, a second passthru, sources outside
source mode, a multicast source, a loopback lower) and ip's eleven.
The VM run held every planned status on the first try, and every value
read back as sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (KC3-T03)

18 ipvlan rows and 4 ipvtap rows in the kind corpus: each mode and
flag word, the change path (a mode alone clears the flags), the
kernel's three refusals (no lower, a loopback lower, a lower whose rx
handler a macvlan holds) and ip's five. The VM run held every planned
status on the first try. That closes KC3: 90 rows over five kinds.

KC4 is broken into vxlan, geneve, bareudp and amt, and gtp, each with
its rows argued from iplink_*.c at v7.2.0 and the kernel's checks. KC0's
gtp refusal was its probe sending no option, so gtp stays in scope, and
7.2.0's geneve option is one keyword, local, for both families.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 vxlan rows in the kind corpus, the first kind with no lower of its
own: id, dstport, remote and group in both families, local, ttl, tos,
df, flowlabel, ageing, maxaddress, srcport, the thirteen flag words,
external, vnifilter and gpe, three changes the kernel takes and four it
refuses, its other eight refusals and ip's fifteen. The VM run held
every planned status on the first try, and each readback differs from
the base row only in what the row sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 geneve rows in kind-write-corpus.exp: the base and each option,
7.2.0's one `local` keyword with a row per family and `local6`/`loc`
refused by ip, the externally controlled conflicts, the VNI and TTL
ranges, and the sets the kernel allows and refuses. The VM holds every
planned status on the first run (22 at 0, 13 at 2, 7 at 1, 7 at 255).
kind-write-corpus-test.exp grows to 104 checks; 4 of 4 mutants killed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16 bareudp and 20 amt rows in kind-write-corpus.exp. bareudp's
required dstport and ethertype are ip's missarg; amt's lower, mode and
local are amt_validate's. Neither kind has a changelink, so both sets
are -EOPNOTSUPP. The VM holds every planned status on the first run.
kind-write-corpus-test.exp grows to 112 checks; 4 of 4 mutants killed.
The plan's amt lower test now counts only the rows the kernel answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
19 gtp rows in the kind corpus: role, hsize (its 1<<17-1 top, and 0
meaning the default) and restart_count, an unknown word skipped against
an abbreviated role, the second socket-owning device refused, and the
set gtp has no changelink for. 7 accepted, 4 refused by the kernel and
8 by ip, each holding its planned status on the first VM run.

KC4 is closed (V026-V037). KC5 is broken into five tasks, one per
iproute2 parser or pair, with tables for the twelve IP tunnel kinds;
the decision log records that fou is loaded and fou6 isn't, and that
the guest kernel has no 6RD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 rows for the three kinds gre_parse_opt parses: every key, seq and
csum form, ttl/tos and their aliases, pmtudisc and ignore-df, fwmark,
fou encap, external, multicast keys, abbreviations, erspan's versions,
index, direction and hwid, and the set path, where ip prefetches the
device and resends its values. The refusals cover tunnel collisions,
gre's point-to-point change, erspan's seq|key and 10-bit key checks,
the collect_md extack, ip's usage text, broadcast check and invargs.
44 accepted, 16 refused by the kernel and 21 by ip, each holding its
planned status on the first VM run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-T02)

64 rows for the three kinds gre_parse_opt parses in link_gre6.c: the
hoplimit, tclass, flowlabel and encaplimit forms and their tops, the
ip6_tnl flags, keys, fou and gue encaps (refused, as fou6 isn't
loaded), external, erspan's versions, index, direction and hwid, and
the prefetching set path. The refusals cover tunnel collisions (which
ip6gretap's set doesn't check), erspan's version 0, seq|key, key and
hwid checks, a set naming another kind, ip's usage text and invargs.
36 accepted, 14 refused by the kernel and 14 by ip, each holding its
planned status on the first VM run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 rows from link_iptnl.c: 47 ipip and 37 sit. The first VM run
found two ip behaviors the draft missed: 6rd-reset segfaults ip 7.2.0
(link_iptnl.c:288 hands get_prefix a string literal it writes into),
so it has no row; and a set on sit's wildcard (ttl 64, no DF) exits
255 at the prefetch, so those rows expect 255 and set_remote_on_inherit
keeps the kernel's point-to-point refusal. The test has 154 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 rows from link_ip6tnl.c: the modes and their aliases, the hex
tclass and flowlabel, the inherit words, encaplimit none, the encap
words (fou6 isn't loaded, so encap fou is refused), the prefetched
set, the conflicts and ip's refusals. The test has 163 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…KC6 (KC5-T05)

58 rows: 23 vti, 12 vti6 and 23 xfrm, from link_vti.c, link_vti6.c
and link_xfrm.c. The draft's bare-set test named three set_no_option
rows but its table had two, so vti6 gains one. 27 accepted, 15
refused by the kernel and 16 by ip, each holding its planned status
on the first VM run. The test has 178 checks.

KC5 is closed (V038-V052). KC6 is broken into four tasks (hsr;
netkit; vrf, vrf_slave and vxcan; batadv and the parser-less kinds)
with 181 drafted rows. The decision log records that iplink.c's
nodev returns (status 1), that a bare set reaches no kind code, that
hsr's version/proto checks in ip test get_u8's return rather than
the value, netkit's statics and vxcan's usage on stdout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
29 rows from iplink_hsr.c, each on dummy slaves d0 and d1: the
supervision byte's range, version and proto (ip's check tests
get_u8's return, so version 2 and proto 2 reach the kernel), PRP,
the interlink, the abbreviations, hsr_newlink's extacks and the set
hsr has no changelink for. All hold their planned statuses on the
first VM run. The test has 186 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
43 netkit rows in the kind corpus, after hsr's: 19 the kernel
accepts, 8 it refuses and 16 ip refuses before netlink. The sets
naming the peer name the one their pre made (nt1), since a set looks
the peer's name up. netkit's parser keeps state in statics, so its
rows never run under ip -batch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
18 vrf, 10 vrf_slave and 18 vxcan rows in the kind corpus, after
netkit's: 18 the kernel accepts, 10 it refuses and 18 ip refuses
before netlink. vrf_slave has no parser, so a word after its type is
Garbage; the vxcan sets name the peer their pre made (vc1), since a
set looks the peer's name up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… KC6, plan KC7 (KC6-T04)

17 batadv, 9 ifb, 6 nlmon, 13 vcan, 14 team and 4 team_slave rows in
the kind corpus, after vxcan's: 29 the kernel accepts, 18 it refuses
and 16 ip refuses before netlink. ifb, nlmon, vcan, team and
team_slave have no parser, so a word after their type is Garbage, or
usage() for a prefix of help.

KC6 is done. KC7 becomes two tasks: commit a 7.3.0 link-write corpus
beside 7.1.4's, then the completeness gate that reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…KC7-T01)

One run of the link-write capture at the iproute2 7.2.0 pin on the
7.3.0 guest kernel, in testdata/7_3_0/link-write: 10558 files, every
row of KC1 to KC6 included. Every status matches V064's run.

TestKindRowsComplete checks that each kind row has its status and
request, and has readbacks exactly when the kernel saw the request. Two
provenance rows tie the set to kernel 7.3.0 and iproute2 7.2.0.
corpusAll skips the new set: it recaptures the 7.1.4 rows, and 17
corpus tests tally one capture of each row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ier, plan KP0 (KC7-T02)

TestLinkKindUniverse requires every link_util id in iproute2 7.2.0's
ip/iplink_*.c and ip/link_*.c (46, not the plan's 48) to be captured in
the 7.3.0 link-write corpus, or excluded with a reason. A kind is
captured when one of its rows is at rc 0 with an ip.pcap and a -d -j
readback whose linkinfo names it (info_slave_kind for a _slave kind).
39 are captured and 7 excluded. bond, bond_slave, dummy and veth predate
the readbacks, so they pair their write/ rows with a shared -d -j dump.

TestLinkKindCaptured drives the capture rule over built rows. Three
mutants (stale case, _slave match, rc filter) are all killed.

Docs: KC7-T02's section matches the gate; KC7 and the KC tier are done;
KP0-T01, the inet devconf codec, is planned with its tables (DOC009).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ip link set dev X inet <param> <value>... is the one new write at the
iproute2 7.2.0 pin. xtcpnl_inet_devconf.go adds:

- InetDevconfParams: the 32 params iplink_parse_inet writes, in its
  order, 23 on/off and 9 numeric.
- LinkAttrInetDevconf: the whole IFLA_AF_SPEC -> AF_INET ->
  IFLA_INET_CONF attribute. Only the innermost nest carries
  NLA_F_NESTED, and repeats are kept.
- ParseInetDevconfRequest: the request's nested u32 settings.
- ParseInetDevconfValues: the dump's raw IPV4_DEVCONF_MAX array, which
  is a different shape from the request.

The encoder reproduces the 11 KC1 rows the kernel answered byte for byte,
and the decoders read them back. Three mutants (NLA_F_NESTED dropped,
repeats deduplicated, off-by-one index) are all killed.

Docs: KP0 is done; KP1 is broken into the bridge and bridge_slave
parsers with their tables (DOC010).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BridgeOptions is the bridge's option table in bridge_print_opt's order:
53 rows, IFLA_BR_MULTI_BOOLOPT as one per bit, with each row's wire,
JSON key and match rule. ParseBridgeInfoData decodes IFLA_INFO_DATA
into LinkInfo.Bridge; EncodeBridgeInfoData writes it as
bridge_parse_opt does, the boolopt last.

On the 7.3.0 kind corpus every accepted row's dump, rendered, equals
its -d -j readback, and all 55 rows that sent a request are rebuilt
byte for byte. vlan_protocol is big-endian on the wire. The keyword
ageing_time is one constant, so misspell needs no exclusion. The new
LinkInfo field moves linkmonitor's replay digests; regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xtcpnl_bridge_slave.go is the port's 44-row option table (36 printed
attributes, mcast_fast_leave, fdb_flush, and six read-only rows for the
counters ip never prints), ParseBridgeSlaveData into LinkInfo.BridgeSlave
on IFLA_INFO_SLAVE_KIND, and EncodeBridgeSlaveData. Two new wires, on/off
and ifindex, join the bridge's; the test helpers become a kindRows harness
both kinds share.

Every one of the 30 accepted rows' dumps renders as its -d -j readback,
and the 37 rows that sent a request rebuild byte for byte. Mutants: flood
as MCAST_FLOOD, neigh_forward_grat off by one, the branch keyed on Kind;
3 of 3 killed. Replay golden digests regenerated; value counts unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xtcpnl_vlan.go decodes the vlan kind's IFLA_INFO_DATA into LinkInfo.Vlan
(id, big-endian protocol, the flags/mask pair, the two qos maps) and
encodes it as vlan_parse_opt does: id, protocol and maps in argument
order, then one IFLA_VLAN_FLAGS last. VlanFlags keeps vlan_print_flags'
order, which is not bit order.

All 15 accepted rows' dumps render as their -d -j readbacks, and the 21
rows that sent a request rebuild byte for byte. Mutants: protocol read
little-endian, egress as ingress, egress unknown, flags in argument
order; 4 of 4 killed. Replay golden digests regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xtcpnl_macvlan.go decodes both kinds' IFLA_INFO_DATA into
LinkInfo.Macvlan (mode, flags, the broadcast queue lengths and cutoff,
the source list) and encodes it as macvlan_parse_opt does: macaddr ops,
bcqueuelen and bclim in argument order, then the last mode, then the
flags, with ip's nopromisc-outside-passthru refusal.

All 25 accepted rows' dumps render as their -d -j readbacks, and the 30
rows that sent a request rebuild byte for byte. Mutants: passthru and
source swapped, the macvtap branch dropped, mode in argument order;
3 of 3 killed. Replay golden digests regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xtcpnl_ipvlan.go decodes both kinds' IFLA_INFO_DATA into LinkInfo.Ipvlan
(mode and flags, read only at exactly 2 bytes, as ipvlan_print_opt does)
and encodes it as ipvlan_parse_opt does: one MODE per mode word, then
FLAGS always, 0 without a flag word, and nothing without options.

All 14 accepted rows' dumps render as their -d -j readbacks, and the 17
rows that sent a request rebuild byte for byte. Mutants: FLAGS only with
a flag word, vepa over private, the ipvtap branch dropped; 3 of 3
killed. Replay golden digests regenerated.

KP2 is closed. KP3 (vxlan, geneve, bareudp and amt, gtp) is planned with
its test tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ParseVxlanInfoData decodes the vxlan kind's IFLA_INFO_DATA into
LinkInfo.Vxlan: the id, the group or remote and the local (GROUP over
GROUP6, as vxlan_print_opt reads them), the big-endian ports, label and
reserved bits, vxlan_opts' 14 u8 booleans, and the flag attributes. All
46 accepted rows' dumps render as their `ip -d -j` readbacks print them.

EncodeVxlanInfoData(settings, set) follows vxlan_parse_opt: the words'
attributes in argument order, then ID, the local, the group or remote,
LEARNING (on an add always; on a set only after a learning word) and
PORT (4790 for gpe without dstport). It rebuilds the request of all 58
rows that sent one byte for byte, and refuses ip's conflicts with
ErrVxlanConflict.

misspell reads the kernel's "ageing", so the keyword is
VxlanKeywordAgeing and the id IflaVxlanAgeing, as for the bridge.
Mutants 3 of 3 killed; the replay goldens' digests regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
randomizedcoder and others added 29 commits November 3, 2026 14:42
The five kwlive spaces join wsO5Spaces; all 36 rows are the same. Four
kwnh rows first differed only in padding: ipnexthop_modify builds the
MPLS encap in an uninitialized stack buffer and rta_addattr_l leaves
the alignment padding, so ip sends stack bytes after MPLS_IPTUNNEL_TTL
and NHA_ENCAP_TYPE. wsO5ZeroEncapPad zeroes them before comparing, as
wsO5ZeroMetricsPad does for RTA_METRICS, and the report counts them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GW5 forms tables read the kwlive spaces too, and the 20 forms that
had no row pick 23 kwlive rows, one per form and ip rc; commands.go
gains them as gw_kw* write rows, argv, setup and floor from each
capture, as the last route rows, since the kwnh rows' encap mpls
autoloads the MPLS modules. The vrf forms name their VRF, and the
one-label MPLS form no longer matches ttl. The rows stay ungated until
LF3-T02's runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first --section route,link,nexthop,rule run at 04bb605 passes 926
of 927 rows; gw_kwnh_ec_003 diverges on NHA_ENCAP, where ip's three pad
bytes after MPLS_IPTUNNEL_TTL come from an uninitialised stack buffer
(ipnexthop.c) and goip's are zero.

A top-level attribute's value already stops at nla_len, so its padding
is never compared. samePastPadding gives an NLA_F_NESTED payload the
same treatment: two that parse to the end into the same attributes, in
the same order with the same types and values (descending into inner
nests), are the same. Anything else is still one value finding.
Unflagged nests such as IFLA_AF_SPEC are unchanged.

Ledger V016-V018; V013-V015 backfilled with 04bb605.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 3f4b704 a --section route,link,nexthop,rule run (927 of 927 PASS)
and the full goip-parity run with the rows gated (1304 of 1304 PASS)
are clean, so the 23 gw_kw rows go in gated_commands and
TestAllowlistCommitted's earned list, and the GW matrix tables' gated
check and TestUngatedSurfaceIsNotVacuous lose their gw_kw exceptions.

Ledger V019-V021; V017-V018 backfilled with 3f4b704.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in vrf show NAME (VS0-VS4), the linkmonitor RDMA and learn
deadline fixes, nicinfo uplink expansion, and the 0.2.0 release work.

Two conflicts:
- docs/netlink/README.md: both sides added rows; both kept.
- internal/goip/service/service.go: main's VrfTableByName and this
  branch's VRFTable sent the same get. VrfTableByName is kept as main
  has it, and VRFTable, which route `vrf NAME` uses, is now a wrapper
  applying ipvrf_get_table's errno ladder to it, so the request is built
  in one place. A dump-fallback miss, which VrfTableByName reports as
  ENODEV, now takes the same ladder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
goip refuses a route's and a hop's `encap` and builds only `mpls` for a
nexthop object. The plan builds lwt_parse_encap's mpls, ip and ip6 types,
with the geneve/vxlan/erspan options, in all three places, then rows,
captures, O3, O5, goip-parity rows and a VM gate. The other encap types
are a follow-on. The closed plans' next actions point here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PutIPEncap writes parse_encap_ip's and parse_encap_ip6's nest: the items
in argv order, the geneve/vxlan/erspan option nests, then LWTUNNEL_IP_FLAGS
big-endian (TUNNEL_* are __cpu_to_be16). PutLwtEncap picks mpls or ip and
writes nothing on a failure; EncodeLwtEncap gives a route's or a hop's
RTA_ENCAP and RTA_ENCAP_TYPE as RouteAttrs within a bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lwtEncapArgs reads lwt_parse_encap's grammar for mpls, ip and ip6 with
the geneve, vxlan and erspan options, giving `ip`'s refusal messages.
The nexthop object's encap uses it; ip/ip6 there refuse until LE2-T02.
dst any/all send an empty address, as get_addr gives them. geneve's
class and type read base 0, so the plan's forms use 0x. Closes LE1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A route's encap and a hop's go where the keyword is, from lwtEncapArgs.
ip's buffer bounds are refusals: addraw_l's 1024 bytes count the request
so far, and a hop's what is left of parse_nexthops' 4096. O5's two fibt
rows move from refuses to differs, on ip's stack padding after a hop's
RTA_ENCAP_TYPE, which LE3-T03 zeroes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nexthopWrite.encap no longer refuses ip and ip6. EncodeLwtEncapAs takes
the two attribute types, so NHA_ENCAP and NHA_ENCAP_TYPE share the
route's encoder, under ipnh_modify's addraw_l bound of 1024 bytes over
the request so far. mpls is unchanged. Closes LE2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-T01)

Four spaces (lwtroute4, lwtroute6, lwthop, lwtnh) write the plan's 27
forms, 21 at rc 0, 2 at rc 2 and 4 at rc 255, and 12 duplicate adds to
nix/microvms/workbench/lwt.rows.jsonl. Every row loads kwNhModules. The
tunnel's id is tun_id in the axes, apart from a nexthop's id. The plan's
counts are corrected to these.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3-T02)

Two fresh-boot nlcap-ws batches of lwt.rows.jsonl agree on all 39 rows,
every rc as written. Their only byte differences are ip's unzeroed
padding, after RTA_ENCAP_TYPE and inside the geneve option nests, which
LE3-T03's O5 zeroing covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
O3 checks each captured lwt request's encap against its axes: the type,
each sub-attribute's width and byte order, the option nests and the
flags. O5 reads the four lwt spaces, and wsO5ZeroEncapPad now reaches a
route's RTA_ENCAP, each hop's in RTA_MULTIPATH and every nest inside
them, so all 39 rows and the two fibt rows are same. Closes LE3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestGwLwtMatrixRows picks 23 lwt rows, one per encap form and ip rc,
and commands.go gains them as gw_lwt* write rows after the kwnh rows.
nlparity compares RTA_MULTIPATH hop by hop past each hop's padding, as
the captures show ip's stack bytes after a hop's RTA_ENCAP_TYPE. The
rows stay ungated until LE4-T02's runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 84260fe, two --section route,nexthop runs (124 of 124 PASS each)
and the full goip-parity run with the rows gated (1330 of 1330 PASS)
are clean, so the 23 gw_lwt rows go in gated_commands and
TestAllowlistCommitted's earned list. The GW matrix tables' gated check
and TestUngatedSurfaceIsNotVacuous lose their gw_lwt exceptions, and
gwLwtSlugPrefixCst goes with them.

Ledger V025-V029, V025 being the main merge's clean full run (1307 of
1307); V022-V024 backfilled with 84260fe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
goip-lwt-encap.md is closed, and lwtEncapArgs refuses every type but
mpls, ip and ip6. This plan builds seg6, seg6local, ila, xfrm, ioam6
and rpl in all three places, a 56-form lwtx workbench family, O3, O5,
goip-parity rows and a VM gate (LX1-LX4).

The guest kernel (nixpkgs linux_latest 7.3) has SEG6_LWTUNNEL,
SEG6_HMAC, ILA and XFRM_INTERFACE but not IOAM6_LWTUNNEL or
RPL_LWTUNNEL, so the ioam6 and rpl rows compare the request at rc 2
(EOPNOTSUPP). The plan lists the argv quirks goip copies: xfrm
swallowing the rest of the command, ioam6's and seg6local srh's
end-of-argv step back, hmac ignoring a bad number, seg6 with no mode.

Ledger DOC001.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xtcpnl_lwt_seg6.go builds parse_srh's segment routing header (SRH),
encap seg6 (Seg6Encap: the int mode, the SRH, tunsrc) and encap
seg6local (Seg6LocalEncap: the items in argv order, the counters and
flavors nests, the SRH last). LwtEncap gains Seg6 and Seg6Local, and
PutLwtEncap refuses anything but exactly one type set.

SRH takes the slot count rather than parse_srh's encap flag: strtok
skips an empty field between commas, so the slots and the addresses
can differ, and slot 0 stays zero.

TestSRH (11 rows) and TestPutSeg6Encap (18 rows), goldens hand-built
from the uapi headers. Ledger V001-V003; DOC001 backfilled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xtcpnl_lwt_other.go builds encap ila (the locator and its u8s in argv
order), xfrm (if_id, link), ioam6 (the frequencies, the mode, tunsrc
and tundst unless inline, and ioam6_trace_hdr) and rpl (RplSRH, the
segments last to first). LwtEncap now takes all eight types, and
PutLwtEncap sends the six new ones through one nest writer.

TestPutLwtEncapOther (17 rows), goldens hand-built from the uapi
headers. netlink-audit flagged ioam6Trace's indexed writes; it now
appends. Ledger V004-V006, closing LX1; V001-V003 backfilled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lwtEncapArgs now reads every lwt_parse_encap type but bpf
(lwt_encap_srv6_args.go), so a route, a hop and a nexthop object
build them. The argv quirks are ip's: xfrm swallows the rest of the
command; ioam6's trace loop and seg6local's srh segs S at the end of
argv step back so the caller reads the last word again; hmac ignores
a bad number; seg6 with no mode sends -1.

lwtEncapArgs takes the resolver, and routeParseState keeps it, so
seg6local's iif and oif and xfrm's link_dev are looked up mid-loop as
ip does. nh4/nh6 any is an empty attribute. ila reuses obj_ila.go's
parsers and xtcpnl_ila.go's constants; LX1-T02's copies are gone.

TestLwtEncapArgsSrv6 (65 rows). The refusal rows that named seg6 now
name bpf. Ledger V007-V009; V004-V006 backfilled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…T02)

TestRouteWriteSrv6 (14 rows) and TestNexthopWriteSrv6 (5 rows) check
each type's attributes where the keyword is, seg6local's oif looked
up before dev, the route bound at 59 segments, and the quirks that
change what the caller reads next: xfrm swallowing dev on a route, a
hop and a nexthop, and ioam6's trace and seg6local's srh segs S last,
whose last word ip reads again as a second destination. No new code
was needed. O5's golden is unchanged.

Ledger V010-V012, closing LX2; V007-V009 backfilled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3-T01)

matrix_lwtx.go's eight spaces hold goip-lwt-srv6.md's forms: seg6 on
IPv6 and IPv4 routes, seg6local's actions and flavors, ila, xfrm, a
hop's seg6, a nexthop's seg6 and seg6local, and lwtxreq, the ioam6 and
rpl forms the guest kernel refuses, all Judged negatives. Every row
loads ila and xfrm_interface. nix/microvms/workbench/lwtx.rows.jsonl
has the 73 rows.

TestLwtxSpaces (65 subtests). The plan's space and duplicate counts
are corrected. Ledger V013-V015; V010-V012 backfilled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…X3-T02)

Two fresh-boot nlcap-ws batches of lwtx.rows.jsonl agree on all 73
rows' rcs and message types; every rc is as written. The bytes differ
only in ip's padding after a route's RTA_ENCAP_TYPE, in 2 requests.
The first batch is in pkg/xtcpnl/testdata/7_3_0/ws/ (228 KB).

TestWsCorpusRows corrected two expectations: seg6local's oif nosuch
and xfrm's link_dev look the device up first, so they are refused by
the kernel and ack-then-fail. The generator and the two cmd.json are
corrected; the captures are kept.

Ledger V016-V018; V013-V015 backfilled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
O3 (xtcpnl_ws_lwtx_test.go) checks each captured lwtx request's encap
against its axes: seg6's mode and SRH, seg6local's action, attributes,
counters and flavors, ila's locator and u8s, xfrm's if_id and link,
ioam6's frequencies, mode, addresses and trace header, and rpl's SRH.
Each SRH's segments are checked against the row's own segs, last to
first. 60 requests checked; 13 refusals send none.

O5 has the eight lwtx spaces: all 73 rows same, no existing row moved.
TestWsO5ZeroEncapPad gains a seg6local flavors nest's u8 padding.

Ledger V019-V021, closing LX3; V016-V018 backfilled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
43 goip-parity write rows after the lwt rows, one per lwtx form at rc
0 or 2: seg6 on IPv6 and IPv4, seg6local's actions and flavors, ila,
xfrm, a hop's seg6, a nexthop's seg6 and seg6local, and ioam6 and rpl,
which a guest kernel without them refuses after ip and goip send the
same request. TestGwLwtxMatrixRows (38 forms) picks them from the
corpus. The gated checks skip them (gwIsLwtx) until LX4-T02.

The rc 1 ila form stays O5's, as a GW row is rc 0 or 2; the plan is
corrected. Ledger V022-V024; V019-V021 backfilled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On dfe50cf, two --section route,nexthop runs (167 of 167 PASS each)
and the full goip-parity run with the rows gated (1373 of 1373 PASS)
are clean, so the 43 lwtx rows go in gated_commands and
TestAllowlistCommitted's earned list. The GW matrix tables' gated check
and TestUngatedSurfaceIsNotVacuous lose their lwtx exception, and
gwIsLwtx goes with it.

Ledger V025-V028; V022-V024 backfilled with dfe50cf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reproducing the ip bugs LX found showed `flavors next-csid lblen 32` as
the last words segfaults ip too (strcmp on argv[argc] for nflen), not
only `next-csid` last. goip already refuses it as incomplete; a row in
TestLwtEncapArgsSrv6 now pins it.

The same repros showed the hdrlen decision was wrong: 128 segments wrap
hdrlen to 0 and ip sends an 8-byte SRH, inside the 1024-byte bound, which
the kernel refuses (EINVAL). goip refuses before sending, so it exits 1
where ip exits 2. The decision log says so now. Ledger V029.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Building and testing the iproute2 write-up's patches showed the plan's
out-of-scope note wrong: 26 segments written out in full pass segs'
1023 bytes with a 424-byte SRH, inside the route bound, so ip installs
the route with the last segment cut. The route-bound row said ip
refuses; it prints addraw_l's error and sends the route without the
encap, which goip-lwt-encap-status.md's decision log already recorded
and goip refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s (V031)

From PR #287's review.

- llProtoA2N matched names with strings.EqualFold, which folds Unicode:
  "ſnap" matched snap and "atal" plus the Kelvin sign matched atalk.
  proto_a2n uses strcasecmp, so it now uses cStrcaseEq.
- cStrtoul read any base other than 8 or 16 as decimal. It now converts
  nothing for a base other than 0, 8, 10 or 16, as strtoul does for a
  base it refuses, so a bad base fails.
- The link kinds' ifindex conversions go through cint, as the rest of
  goip's do.

TestLLProtoA2N and TestCStrtoulBase are new; ledger V031.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se its memfd (V032)

From PR #287's review.

- splitSecrets blanked the config with its own copy of
  ConfigService's redactSecrets list, so a secret added to one and not
  the other would reach XTCP_CONFIG_JSON and /proc/<pid>/environ. It now
  calls the exported xtcp.RedactSecrets. TestSplitSecretsMatchesRedactSecrets
  sets every string field of a config and fails if a redacted field isn't
  carried back, or a carried secret is left in the redacted JSON.
- secretsMemfd refuses a payload past reconfigureSecretsMaxBytes, so the
  parent logs and keeps running instead of exec'ing into an image that
  refuses it (TestSecretsMemfdBound).
- loadReconfigureSecrets closes a memfd it refuses for missing seals;
  before, only a memfd it read was closed.

Ledger V032 in goip-lwt-srv6-status.md, committed with V031.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@randomizedcoder
randomizedcoder merged commit 75c68a0 into main Nov 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant