Bridge: full IFLA_BR_* support, filter defaults from show
Summary
Implement the complete set of settable kernel bridge attributes and make ifstate show emit a usable, schema-valid config. A bridge at kernel defaults now shows as kind: bridge + state: up instead of dumping 40+ runtime attrs.
Changes
New attributes (30 in schema/2/ifstate.conf.schema.json)
- STP:
br_forward_delay, br_hello_time, br_max_age, br_stp_state, br_priority
- Forwarding:
br_group_fwd_mask, br_group_addr
- Multicast:
br_mcast_snooping, br_mcast_router, br_mcast_querier, hash/cnt/intvl knobs, IGMP/MLD versions, stats
- Netfilter:
br_nf_call_iptables/ip6tables/arptables
- Boolopt trio (via
IFLA_BR_MULTI_BOOLOPT): br_no_linklocal_learn, br_mcast_vlan_snooping, br_mst_enabled
- FDB cap:
br_fdb_max_learned
New module libifstate/link/bridge.py provides:
BridgeLink subclass — collapses the three boolopt booleans into the packed kernel struct on apply, decodes them on read
- pyroute2
bridge_data.nla_map extension for the five IFLA_BR_* attrs missing from pyroute2 ≤ 0.9.5 (all-or-nothing patch — bails if pyroute2 has any of them, since type ids are derived from nla_map position)
- HZ-aware tolerant compare for USER_HZ timer attrs (
br_forward_delay, br_hello_time, br_max_age, br_ageing_time, all mcast_*_intvl). The kernel rounds USER_HZ → jiffies → USER_HZ lossily on HZ=300/250/...; without tolerance every re-apply would chase its tail
is_schema_default(), split_multi_boolopt(), bridge_boolean_attrs() consumed by the show path
Show path (libifstate/__init__.py)
- Suppresses attrs matching the schema default (timer attrs match both raw and HZ-rounded variants)
- Suppresses read-only/runtime attrs (
br_root_id, br_bridge_id, *_timer, br_topology_change*, br_fdb_n_learned, br_pad, br_mcast_querier_state)
- Coerces kernel uint8 0/1 → Python
True/False for attrs declared type: boolean, so show output validates against the schema and ifstate show | ifstate apply round-trips clean
showall=True still dumps everything for debugging
Tests
- Every attr in the full config lands in kernel state
- Second apply records zero link changes (idempotent)
- Individual boolopt toggle preserves the other bits
- Read-only attrs filtered from show; boolopt struct split into individual bools
- External flip of an unmanaged boolopt survives ifstate apply (optmask only covers user keys)
- External timer-attr clobber is detected and corrected — HZ tolerance doesn't mask real drift
vlan_protocol switch 802.1q→802.1ad on a vlan-filtering bridge preserves brvlan entries on the port
show | apply round-trip is a no-op
- Schema rejects 7 malformed values (priority overflow, igmp/mld version range, malformed MAC, ...)
- Kernel constraint surface:
mcast_vlan_snooping without vlan_filtering fails at netlink and the error is logged
show(showall=False) AND show(showall=True) both validate against the schema
Notes
- Tested on HZ=300; HZ detection falls back to
/proc/config.gz / /boot/config-$(uname -r). On unknown HZ the tolerance is disabled (re-apply will re-set timer attrs every run, but kernel state stays correct).
- pyroute2 patch is global per Python process — documented in the patch's docstring.
## Bridge: full IFLA_BR_* support, filter defaults from show
### Summary
Implement the complete set of settable kernel bridge attributes and make `ifstate show` emit a usable, schema-valid config. A bridge at kernel defaults now shows as `kind: bridge` + `state: up` instead of dumping 40+ runtime attrs.
### Changes
**New attributes** (30 in `schema/2/ifstate.conf.schema.json`)
- STP: `br_forward_delay`, `br_hello_time`, `br_max_age`, `br_stp_state`, `br_priority`
- Forwarding: `br_group_fwd_mask`, `br_group_addr`
- Multicast: `br_mcast_snooping`, `br_mcast_router`, `br_mcast_querier`, hash/cnt/intvl knobs, IGMP/MLD versions, stats
- Netfilter: `br_nf_call_iptables`/`ip6tables`/`arptables`
- Boolopt trio (via `IFLA_BR_MULTI_BOOLOPT`): `br_no_linklocal_learn`, `br_mcast_vlan_snooping`, `br_mst_enabled`
- FDB cap: `br_fdb_max_learned`
**New module** `libifstate/link/bridge.py` provides:
- `BridgeLink` subclass — collapses the three boolopt booleans into the packed kernel struct on apply, decodes them on read
- pyroute2 `bridge_data.nla_map` extension for the five `IFLA_BR_*` attrs missing from pyroute2 ≤ 0.9.5 (all-or-nothing patch — bails if pyroute2 has any of them, since type ids are derived from `nla_map` position)
- HZ-aware tolerant compare for USER_HZ timer attrs (`br_forward_delay`, `br_hello_time`, `br_max_age`, `br_ageing_time`, all `mcast_*_intvl`). The kernel rounds USER_HZ → jiffies → USER_HZ lossily on HZ=300/250/...; without tolerance every re-apply would chase its tail
- `is_schema_default()`, `split_multi_boolopt()`, `bridge_boolean_attrs()` consumed by the show path
**Show path** (`libifstate/__init__.py`)
- Suppresses attrs matching the schema default (timer attrs match both raw and HZ-rounded variants)
- Suppresses read-only/runtime attrs (`br_root_id`, `br_bridge_id`, `*_timer`, `br_topology_change*`, `br_fdb_n_learned`, `br_pad`, `br_mcast_querier_state`)
- Coerces kernel uint8 0/1 → Python `True`/`False` for attrs declared `type: boolean`, so `show` output validates against the schema and `ifstate show | ifstate apply` round-trips clean
- `showall=True` still dumps everything for debugging
### Tests
- Every attr in the full config lands in kernel state
- Second apply records zero link changes (idempotent)
- Individual boolopt toggle preserves the other bits
- Read-only attrs filtered from show; boolopt struct split into individual bools
- External flip of an unmanaged boolopt survives ifstate apply (optmask only covers user keys)
- External timer-attr clobber is detected and corrected — HZ tolerance doesn't mask real drift
- `vlan_protocol` switch 802.1q→802.1ad on a vlan-filtering bridge preserves brvlan entries on the port
- `show | apply` round-trip is a no-op
- Schema rejects 7 malformed values (priority overflow, igmp/mld version range, malformed MAC, ...)
- Kernel constraint surface: `mcast_vlan_snooping` without `vlan_filtering` fails at netlink and the error is logged
- `show(showall=False)` AND `show(showall=True)` both validate against the schema
### Notes
- Tested on HZ=300; HZ detection falls back to `/proc/config.gz` / `/boot/config-$(uname -r)`. On unknown HZ the tolerance is disabled (re-apply will re-set timer attrs every run, but kernel state stays correct).
- pyroute2 patch is global per Python process — documented in the patch's docstring.