Apply and tear down named VLAN profiles for working with differently-configured
lidar sensors. Supports macOS (ifconfig/route), Linux (ip) and Windows
(Hyper-V virtual adapters plus netsh); the backend is chosen from the host at
run time.
cargo install vlanctl
Profiles are read from --profiles-dir (default ./profiles). The crate ships
profiles/example.toml as a starting point; the sensor profiles used on our
benches are bench configuration and live in the project repository rather than
the published crate.
vlanctl list # list profiles in ./profiles
vlanctl show <profile> # print the commands a profile would run
vlanctl validate <profile> # check a profile without applying
sudo vlanctl apply [profile] # bring it up (defaults to the `lum` profile)
vlanctl apply [profile] --dry-run # print commands without running them
sudo vlanctl apply lum --device eth0 # pin the parent adapter for this run
sudo vlanctl down # tear down the active profile
vlanctl status # what is currently up
A global --profiles-dir <dir> (default profiles) selects where profiles are
read from. apply with no profile argument defaults to the name lum; supply
your own profiles/lum.toml, or name a profile explicitly.
apply and show also take --device <name>, which picks the parent adapter
the VLANs attach to and overrides any device field in the profile. The parent
is host-local — macOS numbers adapters enN per machine, so a USB dongle
can be en7 on one Mac and en12 on another, while Linux uses
eth0/enp*/enx* and Windows uses the adapter's display name (Ethernet 2)
— so the shipped profiles deliberately do not pin one. Auto-detect takes the
single active wired adapter and skips Wi-Fi (which cannot carry 802.1Q VLANs);
use --device when it is ambiguous or picks wrong. On Windows there is no
auto-detect and --device is required — see below for why.
Windows has no general 802.1Q sub-interface, so vlanctl has two Windows backends and picks one by the profile's shape:
- One
[[interface]]entry: the adapter driver's own VLAN setting. Most wired drivers implement the standardizedVlanIDkeyword; with it set, the driver tags everything it sends, accepts only that VLAN on receive, and strips the tag. The physical adapter becomes an access port for that VLAN and gets the entry's address directly. No virtual switch is involved. The keyword holds one id, which is why this backend takes only single-entry profiles: to work with a sensor's live-data VLAN most of the time and its diagnostics VLAN occasionally, keep them as two profiles and apply the one you need. - Several entries: Hyper-V. The parent adapter is bound to an external
virtual switch named
vlanctl, with one management-OS virtual adapter per entry in access mode for that entry's VLAN. Interface names are the adapters Windows creates,vEthernet (vlan11)for VLAN 11 andvEthernet (untagged)for an untagged entry; because the parent's own stack is gone once bound, the untagged entry is a virtual adapter too and is removed ondown. Hyper-V must be enabled (Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All, which reboots; Windows 10/11 Pro, Enterprise, Education and Server, not Home). vlanctl never enables it.
Either way, addresses, routes and static neighbor entries are set with
netsh, and:
applyanddownneed an elevated shell (Run as administrator), the Windows equivalent ofsudo.- The parent adapter is taken over. Under the driver setting it transmits
tagged only; under the switch it has no addressing of its own. Pointing
vlanctl at the machine's uplink disconnects the machine until
down. That is why--deviceis required on Windows: vlanctl will not guess. - The state file records which backend applied, and
downruns through that one, so switching between the two across profiles is safe. - Everything persists across a reboot — the keyword, the switch and its
adapters, their addresses and routes. One thing does not recover by itself:
if the parent adapter is absent at boot (a dock that did not enumerate, a
USB NIC that was unplugged), Hyper-V leaves the switch unbound and does not
rebind it when the adapter returns.
vlanctl downthenvlanctl applyrecovers it. - Setting the driver keyword restarts the adapter: the link drops for a
few seconds on apply and again on
down. - A peer that has cached the address's MAC keeps using it. The two backends put the same address on different MACs (the physical adapter's under the driver setting, a virtual adapter's under the switch). A device that resolves a peer's MAC address once and keeps streaming to it will not follow the address to the other adapter after a switch between backends; it goes on sending to the old MAC until it restarts that stream. vlanctl cannot detect this, so plan a restart of the peer's stream into any such switch.
- The Hyper-V cmdlets and the keyword run through
powershell.exe -Command, which is not subject to script execution policy (no script file is involved), andnetshis a plain executable, so nothing here needs signing or a policy change.
vlanctl is also a library. A consumer supplies a CommandRunner and a
Platform:
use vlanctl::{commands, config::Profile, net::RecordingRunner, plan::MacOs};
let profile = Profile::load("profiles/example.toml".as_ref())?;
let mut runner = RecordingRunner::default(); // or SystemRunner to execute
// 4th argument overrides the parent device. Profiles do not pin one — it is
// host-local — so a consumer supplies it, or passes `None` to auto-detect
// against the live system.
commands::apply(&mut runner, &MacOs, &profile, Some("en7"), &state_path, true)?;
for cmd in &runner.commands {
println!("{}", cmd.display());
}Depend on it without the CLI's argument parser:
vlanctl = { version = "0.1.0", default-features = false }Profiles live in profiles/*.toml, one file per profile. Each profile describes
one or more interfaces with their IP address and routes:
name = "example"
description = "Sample two-VLAN sensor profile"
# device = "en10" # optional and discouraged: the parent adapter is
# host-local, so prefer `--device` on the command line.
# Omit to auto-detect.
[[interface]]
vlan = 100
address = "192.168.10.2/24"
[[interface.route]]
destination = "192.168.20.0/24"
gateway = "192.168.10.1"
[[interface]]
vlan = 200
address = "10.0.0.5/24"
mtu = 1500Each [[interface]] takes:
vlan— the 802.1Q tag. Optional: omit it for an untagged interface, and vlanctl configures the parent device directly instead of creating a VLAN sub-interface. On macOS and Linux an untagged interface is left configured ondown, since vlanctl did not create the device; on Windows it is a virtual adapter vlanctl created, and is removed.address— required, in CIDR form.mtu— optional.[[interface.route]]— zero or more routes, described below.
Each [[interface.route]] is one of two kinds:
- Gateway route — has a
gateway, and is emitted as a next-hop route. - Interface-scoped route — omit
gateway, and the route is bound to that interface instead: a single host (/32) is emitted as a host route, anything else as a network route.
The exact commands are the host backend's business — macOS renders
route add -host <ip> -interface vlan11, Linux ip route add <ip> dev eth0.11,
Windows netsh interface ipv4 add route <ip>/32 "vEthernet (vlan11)".
vlanctl show <profile> prints what would run on the current host.
Interface-scoped routes are needed when several VLANs share a subnet (so a sensor's traffic is pinned to the right interface) and for per-interface multicast (e.g. SOME/IP-SD discovery):
[[interface]]
vlan = 11
address = "192.168.10.87/24"
[[interface.route]] # reach the sensor via this VLAN's interface
destination = "192.168.10.151/32"
[[interface.route]] # SOME/IP-SD multicast on this interface
destination = "239.255.0.255/32"A gatewayless /32 route may also carry a mac, which adds a static ARP
entry (arp -s <host> <mac>) for an on-link host that does not answer ARP
itself:
[[interface.route]]
destination = "192.168.10.151/32"
mac = "00:00:5e:00:53:01"See profiles/example.toml, which uses a gateway route; the interface-scoped
form above is what real sensor profiles use.
- Mutating commands (
apply,down) modify network interfaces and must run undersudo, or from an elevated shell on Windows. Read-only commands (list,show,validate,status) do not. applytears down any currently-active profile first, then brings up the new one. If a step fails mid-way, it rolls back the interfaces it created.- State (the active profile and the interfaces vlanctl created) is tracked at
/usr/local/var/vlanctl/state.jsonon macOS and Linux and at%ProgramData%\vlanctl\state.jsonon Windows. macOS assigns arbitraryvlanNunit numbers, so vlanctl records what it created in order to tear it down later. --dry-runpreviews the exact commands without executing them and without touching the state file.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.