Parsing Cumulus Linux interfaces(5) in Rust: Why Vec<(String, String)> Is a Crime Against Type Safety
Cumulus Linux stores its entire network configuration in /etc/network/interfaces — the same format Debian uses. It looks like this:
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address 192.168.1.1/24
gateway 192.168.1.254
dns-nameservers 8.8.8.8 8.8.4.4
auto swp1
iface swp1 inet manual
mtu 9216
auto bond0
iface bond0 inet static
address 10.0.0.1/24
bond-mode 802.3ad
bond-miimon 100
bond-slaves swp1 swp2
bond-lacp-rate fast
Simple, right? It’s been the standard since the 1990s. But try writing a tool that parses this format programmatically and you’ll quickly discover that “simple” is a lie told by people who’ve never had to handle the edge cases.
The Problem
I manage network configs across dozens of Cumulus Linux switches. The standard approach is to edit /etc/network/interfaces by hand, push the file via Ansible, and hope nothing breaks. This works until it doesn’t — a typo in a bond-slaves line, a missing auto directive, an interface name that conflicts with the hardware naming scheme — and suddenly you’re debugging why BGP won’t establish on a switch that’s supposed to be trivial.
The real problem is that interfaces(5) is a line-based format with implicit state. There’s no schema, no validation, and no way to programmatically reason about the configuration without writing a parser that handles every edge case. Which is exactly what I did.
interface-rs
I built interface-rs to parse, modify, and write Cumulus Linux interfaces(5) files in Rust. The goal was simple: eliminate the manual drudgery and reduce the risk of errors when configuring network interfaces at scale.
Here’s what the current API looks like:
use interface_rs::Interface;
let iface = Interface {
name: "swp1".to_string(),
auto: true,
family: Some("inet".to_string()),
method: Some("static".to_string()),
address: Some("192.168.100.1/24".to_string()),
options: vec![
("netmask".to_string(), "255.255.255.0".to_string()),
("gateway".to_string(), "192.168.1.254".to_string()),
],
..Default::default()
};
It works. But it’s ugly. And more importantly, it’s wrong.
The Vec<(String, String)> Problem
The options field is a Vec<(String, String)>. Every option — mtu, gateway, dns-nameservers, bond-mode, bond-slaves — is just a string key paired with a string value. There’s no type safety, no validation, and no way for the compiler to tell you that you’ve specified bond-mode on a non-bond interface.
This is a crime against type safety. And in Rust, that’s unforgivable.
Consider the Cumulus Linux interfaces(5) options. Some are boolean (auto, allow-hotplug). Some take a single value (address, gateway). Some take multiple values (dns-nameservers, bond-slaves). Some are only valid on specific interface types (bond-mode, bond-miimon only on bonds). Some are numeric (mtu, miimon). And some are enums (inet, inet6, inet6 with autoconf, manual, static, dhcp).
Representing all of this as Vec<(String, String)> means:
- The compiler can’t help you
- Typos in option names aren’t caught
- Invalid combinations aren’t detected
- You need runtime validation for everything
What It Should Look Like
Here’s what a proper Rust API for this problem should look like:
let bond = Interface::bond("bond0")
.with_auto(true)
.with_family(Family::Inet)
.with_method(Method::Static)
.with_address("10.0.0.1/24".parse().unwrap())
.with_mode(BondMode::Lacp)
.with_miimon(100)
.with_lacp_rate(LacpRate::Fast)
.with_slaves(["swp1", "swp2"])
.build();
This isn’t just prettier. It’s fundamentally different:
BondMode::Lacpis an enum, not a string. The compiler knows the valid values.with_slaves()takes a slice of interface names, not a space-separated string.with_address()takes aIpNet, not a string. Invalid addresses are caught at compile time.Interface::bond()is a constructor that only allows bond-specific options. You can’t accidentally setbond-modeon a regular interface.
Why This Matters
I’m not building this for fun. I manage network configs across dozens of switches, and the current approach — edit a text file, push it via Ansible, hope for the best — is a recipe for disaster at scale.
A proper library that understands the interfaces(5) format can:
- Validate configurations before they’re deployed
- Generate configs from structured data (Terraform state, inventory databases)
- Diff configurations and show meaningful changes (not just line diffs)
- Migrate configs between Cumulus Linux versions as the format evolves
- Integrate with infrastructure-as-code tooling
The Cumulus Linux interfaces(5) format is 30 years old. It’s stable, well-documented, and widely understood. But it was never designed to be parsed programmatically. That’s the gap interface-rs fills.
The Current State
The library works for basic interfaces. It can parse and write simple inet static and inet dhcp configurations. It handles the common options correctly. But the options field is still a Vec<(String, String)>, and that’s a problem I need to fix.
The path forward is clear:
- 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
Why Rust?
Because this is a configuration parsing and generation tool that needs to be fast, correct, and safe. Python would work, but it won’t catch your mistakes. Go would work, but it won’t give you the type system you need. Rust gives you both performance and correctness, and for a tool that’s going to be generating network configurations that affect production traffic, correctness is non-negotiable.
The compiler is your first line of defense against misconfigured networks. Use it.