Packet types for the ethtool generic netlink family.
src/constants.rs is generated from Documentation/netlink/specs/ethtool.yaml
— the same file the kernel's ethtool_netlink_generated.h comes from — by the
ethtool-codegen crate. Generation is a deliberate step with checked-in
output, not a build.rs; nothing fetches at build time. See
spec/PROVENANCE.md.
566 constants, 74 operations, 62 attribute sets.
Only names and numbers are generated. Everything with semantics is hand-written, because the spec records an attribute's type but not what it means:
| shape | why a generator cannot handle it |
|---|---|
bitset |
two wire encodings, chosen by a request flag |
type-value |
the attribute's identity is its nla type number |
sub-type |
a binary blob reinterpreted as an array |
byte-order |
one big-endian attribute among host-endian neighbours |
uint |
variable width; 1, 2, 4 or 8 bytes for the same attribute |
IRREGULAR_ATTRIBUTES lists the eight attributes with those shapes, and a test
asserts it is unchanged — so a future spec adding a shape nothing can decode
fails the build rather than silently misparsing.
tests/generated.rs cross-checks every generated constant against the
build host's uapi header:
checked 498 generated constants against .../linux/ethtool_netlink_generated.h
It panics if it cannot find a header, rather than passing vacuously;
ETHTOOL_UAPI_HEADER=skip is the explicit opt-out. The first version searched
paths that do not exist on this machine, found nothing, and returned Ok —
which is worse than no guard at all.
It earned its keep immediately, catching four generator bugs before any protocol code existed:
subset-ofnever deserialised — the field was spelled with an underscore. A subset borrows its parent's numbering and the kernel emits no enum of its own for it, so the generator inventedETHTOOL_A_STATS_GRP_HIST_HIST_BKT_LOW = 0. The real constant is..._HIST_BKT_LOW = 7.- Reply numbering is not parallel to request numbering. A bare
_SETgets no reply entry — exceptfeatures-set, the one operation that declares its own. - The spec spells "notification" two ways,
notify:andevent:. Treating only the first as one shifted every request fromcable-test-tdr-actonwards. - Attribute numbering starts at 1, not 0. Zero is reserved for
unspecand a set that wants it says so explicitly.
tests/kernel_only.rs does the same for the hand-written constants that live
in linux/ethtool.h rather than the spec. It also earned its keep: the first
version of kernel_only::stringset had everything from ETH_SS_STATS_STD
onward off by one.
let request = StatsAttr::request("eth0", StatsAttr::STANDARD_GROUPS);Verified against an mlx5 ConnectX-6: 29 counters, byte-identical to
ethtool -S <dev> --groups eth-phy eth-mac eth-ctrl rmon, names resolved
through STRSET_GET.
Three things the wire does that the spec does not say:
A counter is wrapped in its own nest. type-value means each counter sits
in its own GRP_STAT holding exactly one attribute, whose nla type is the
counter's index:
GRP
GRP_ID = 1 <- eth-mac
GRP_SS_ID = 18 <- ETH_SS_STATS_ETH_MAC
GRP_STAT len 12
kind 0 len 8 <- counter 0, a u64
GRP_STAT len 12
kind 3 len 8 <- counter 3; indices are sparse
Miss the wrapper and the counters look like malformed 12-byte children and decode to nothing at all — which is exactly what happened here first.
Histogram buckets are one attribute each, not a list inside one. Reading
them as a list descends a level too far and tries to parse a u32 payload as
attributes: NLA has invalid length: 0.
Group ids are in no uapi header. enum ethtool_stats_group lives in
net/ethtool/stats.c yet crosses the netlink boundary in both directions. The
mapping was pinned empirically, by asking for each group and checking the
ss-id it replies with:
| group | id | ss-id |
|---|---|---|
eth-phy |
0 | 17 = ETH_SS_STATS_ETH_PHY |
eth-mac |
1 | 18 = ETH_SS_STATS_ETH_MAC |
eth-ctrl |
2 | 19 = ETH_SS_STATS_ETH_CTRL |
rmon |
3 | 20 = ETH_SS_STATS_RMON |
StatsAttr::request sidesteps them anyway by selecting groups by name,
which the kernel resolves against ETH_SS_STATS_STD. That is what ethtool(8)
does.
The kernel rejects a request whose nests lack the flag with a bare EINVAL.
Replies, confusingly, do not set it, so the parser must not require it.
This cost two debugging rounds: once for the statistics header and group
selector, again for the inner STRINGSET.
NlaBuffer::kind masks the flag off, which is what lets a nest carry it
outbound and still match its bare constant inbound — but it also means the
flag is invisible through kind(), and a test asserting it must read the raw
type field.
bitset.contains(31); // works in either encoding
bitset.indices(); // ditto
bitset.names(); // empty for a compact bitset -- it carries no namesVerbose is a bits nest of index/name/value triples. Compact is a size plus
value/mask as host-order 32-bit words. Which one arrives is decided by
RequestFlags::compact_bitsets, not by the attribute, and the spec lists all
five fields side by side with no hint they are alternatives.
nomask distinguishes a list from a change: without it, value says what
to set and mask says which bits the request is talking about.
RequestFlags::stats is why the previous Rust ethtool binding's pause
statistics could never be populated: the kernel omits every optional stats
sub-nest unless the request asks. Note this is not needed for STATS_GET,
which is the statistics command.
cargo test --features bolero — round-trip property tests over the generated
types. The previous binding emitted its pause Tx counter under the
RX_FRAMES attribute id, a one-line slip that no test caught because there
were none. attribute_kinds_are_distinct is that test.
STRSET_GET and STATS_GET are implemented and hardware-verified. The
remaining 70-odd operations have generated constants but no typed layer yet.