interface-rs v0.3.0: I Actually Built the Typed API I Said I Needed
Six months ago I wrote about why Vec<(String, String)> is a crime against type safety and outlined a roadmap for what a proper Rust API for Cumulus Linux interfaces(5) files should look like.
I said I’d build it. I built it. And now I’m going to tell you what actually works, what I faked, and what’s still a lie.
The Roadmap I Promised
Here’s what I said I’d do:
- Replace
Vec<(String, String)>with a typedOptionsstruct - Add enums for known options (
Family,Method,BondMode, etc.) - Implement a fluent builder API
- Add validation for interface-type-specific options
- Write comprehensive tests against real Cumulus Linux configs
Let’s go through each one and see how I did.
What I Actually Built
The InterfaceOption Enum
The single biggest change: Vec<(String, String)> is gone. It’s been replaced by Vec<InterfaceOption>, where InterfaceOption is an enum with 25+ variants:
pub enum InterfaceOption {
Address(String),
Netmask(String),
Gateway(String),
Mtu(u16),
BridgePorts(Vec<String>),
BridgeVlanAware(bool),
VlanId(u16),
Vrf(String),
PostUp(String),
DnsNameservers(String),
Metric(u32),
// ... and 15 more
Other(String, String),
}
The Other variant is the escape hatch for anything the enum doesn’t know about yet. It’s not ideal, but it’s honest — the interfaces(5) format has more options than anyone could reasonably enumerate, and new ones get added when vendors decide to be creative.
The key insight: InterfaceOption::from_key_value() parses strings into the right type automatically. mtu "9216" becomes InterfaceOption::Mtu(9216). bridge-ports "swp1 swp2" becomes InterfaceOption::BridgePorts(vec!["swp1", "swp2"]). bridge-vlan-aware "yes" becomes InterfaceOption::BridgeVlanAware(true).
This means the parser does the right thing on read, and the Display impl does the right thing on write. No manual string formatting, no “did I use the right key name?” bugs.
Family and Method Enums
These were the easy wins. Family has Inet, Inet6, IpX, Can. Method has Static, Dhcp, Loopback, Manual, Other(String).
The Method enum is infallible to parse — unknown methods like ppp or tunnel just become Method::Other("ppp".to_string()). This is the right call: the parser shouldn’t fail on valid but uncommon methods.
Family is strict — invalid families return a FamilyParseError. This is also right: there are only four valid families in interfaces(5), and if you’re using something else, you’re doing something wrong.
The Builder API
Here’s what the API looks like now:
use interface_rs::interface::{Interface, Family, Method, InterfaceOption};
let iface = Interface::builder("swp1")
.with_auto(true)
.with_allow("hotplug")
.with_family(Family::Inet)
.with_method(Method::Static)
.with_typed_option(InterfaceOption::Mtu(9216))
.with_typed_option(InterfaceOption::BridgeAccess(199))
.with_typed_option(InterfaceOption::MstpctlBpduguard(true))
.build();
Or with the convenience method that auto-parses:
let iface = Interface::builder("swp1")
.with_auto(true)
.with_family(Family::Inet)
.with_method(Method::Static)
.with_option("mtu", "9216")
.with_option("bridge-access", "199")
.build();
The with_option() method calls InterfaceOption::from_key_value() internally, so you get typed parsing without the verbosity. Use with_typed_option() when you want to be explicit.
There’s also an edit() method for modifying existing interfaces:
if let Some(iface) = net_ifaces.get_interface("eth0") {
let modified = iface.edit()
.with_method(Method::Static)
.remove_option("address")
.with_option("address", "192.168.1.50")
.build();
net_ifaces.add_interface(modified);
}
And remove_option() / remove_option_value() for surgical removal:
// Remove ALL addresses
builder.remove_option("address");
// Remove only one specific address
builder.remove_option_value("address", "192.168.1.100");
Parser Improvements
The parser handles the weird ordering that real Cumulus Linux configs have — auto directives appearing after iface stanzas, allow-hotplug before the iface line, multiple address lines for dual-stack configs. It also preserves comments and source directives from the original file.
The Display impl sorts options alphabetically by key name before writing, which produces consistent output. Interface ordering uses natural sort (swp1, swp2, swp10 — not swp1, swp10, swp2).
Error Handling
ParserError includes line numbers. NetworkInterfacesError wraps I/O errors, parser errors, family parse errors, and a new FileModified variant that detects when the file has changed on disk since it was loaded.
The FileModified check prevents you from accidentally overwriting someone else’s changes. It’s a simple last-modified timestamp comparison, but it caught me at least twice during development.
What I Added That I Didn’t Promise
VNI helpers. Cumulus Linux uses VNI interfaces for VXLAN tunnels, and they’re just VLAN interfaces with a bridge-access option. I added next_unused_vlan_in_range(), get_existing_vni_vlan(), and get_bridge_interfaces() to make working with these less painful.
Bridge interface detection. get_bridge_interfaces() returns all interfaces with a bridge-access option defined. Useful for figuring out which ports are part of a VXLAN overlay.
Mapping support. The mapping stanza is obscure (it’s for dynamic interface configuration scripts), but it exists in real configs, so I added Mapping { script: PathBuf, maps: Vec<String> } support.
Zero dependencies. The entire library has no external dependencies. It’s just stdlib. This matters because it means you can use it in contexts where pulling in dependencies is annoying — embedded tools, CI scripts, whatever.
What I Didn’t Build
Validation for Interface-Type-Specific Options
This was item #4 on my roadmap, and I didn’t do it. You can still set bond-mode on a non-bond interface. The compiler won’t stop you. The runtime won’t stop you. You’ll just get a config that Cumulus Linux rejects.
This is the hardest part of the API to get right because interfaces(5) doesn’t have a formal schema. The rules for “which options are valid on which interface types” are scattered across the docs, implied by the kernel, and sometimes contradictory. Building a validation layer that’s both comprehensive and not annoying requires either a massive enum hierarchy or a rule engine, and neither fits the scope of what this library is trying to be.
For now, the library parses and writes correctly. Validation is left to the caller. If you want to validate before deploying, run ifquery --list or netplan apply in a test environment. The library gives you the config; you’re responsible for making sure it works.
The BondMode Enum
I mentioned BondMode::Lacp in the original post as an example of what a proper API should have. I didn’t build it. Bond options are still InterfaceOption::Other("bond-mode", "802.3ad").
This is less painful than it sounds because InterfaceOption::from_key_value() handles it, and the Display impl writes it back correctly. But it’s still not as good as having BondMode::Eight023ad as a first-class type.
Comprehensive Integration Tests
I have 38 unit tests that cover the parser, the builder, the enums, and the display logic. They include a realistic Cumulus Linux config with 48-port bridges, VRF, VLANs, and MSTP. But I don’t have integration tests that actually run on Cumulus Linux hardware.
This is a limitation of the project’s scope, not the code quality. The unit tests are thorough. But there’s a gap between “the parser handles this input correctly” and “this config actually works on a switch.”
The Numbers
- 38 unit tests, all passing
- 24 doc tests (4 fail due to a missing test file — pre-existing issue, not a regression)
- 0 dependencies
- Version 0.3.0 on crates.io
- ~1,200 lines of Rust across 10 source files
Why This Still Matters
I’m not building this for fun. I manage network configs across dozens of Cumulus Linux switches, and the current approach — edit a text file, push it via Ansible, hope for the best — is a recipe for disaster at scale.
With interface-rs, you can:
- Load a running config, modify it programmatically, and write it back
- Generate configs from structured data (inventory databases, Terraform state)
- Diff configurations and see meaningful changes (not just line diffs)
- Validate that your config is parseable before deploying
The Cumulus Linux interfaces(5) format is 30 years old. It was never designed to be parsed programmatically. That’s the gap this library fills.
The Honest Part
The library works. It parses real configs, modifies them, and writes them back correctly. The API is ergonomic. The types are meaningful.
But it’s not a configuration management tool. It’s a parser and serializer with a builder API. If you want to manage network configs at scale, you still need orchestration on top of this — and that’s fine. This library does one thing well: it turns a line-based text format into structured Rust data without losing fidelity.
The validation gap is real. The bond-mode enum is missing. The integration tests don’t exist. But the core is solid, and the roadmap from the original post is 4 out of 5 items complete.
The fifth item — validation — is the hardest one. It’s also the one that matters most in production. I’ll get to it when I have a real use case that demands it. Until then, the library does what it promises: it parses interfaces(5) files in Rust, and it does it without Vec<(String, String)>.