Vendor-agnostic network config compiler.
netconv reads network device configurations, builds a vendor-neutral intermediate representation, and compiles it to any target platform — with an explicit, annotated report of every decision made.
netconv build router.cfg --target vrp
Like a compiler: source → IR → target. Unlike a converter: every approximation is documented, every risk is flagged, nothing is silently wrong.
Live demo: https://netconv.casablanque.com
Migrating network configs between vendors is painful:
- Manual migration — slow, error-prone, no audit trail
- Ansible/Terraform — manage desired state, can't read and explain what you already have
- Vendor tools — locked to their ecosystem, won't tell you what you'll lose migrating away
netconv occupies a different position: it reads your existing config, explains every translation decision, and makes every limitation explicit. A silent conversion that produces a wrong config is worse than no conversion at all.
Primary use case: Cisco → Huawei migration driven by import substitution requirements in Russia and CIS markets. Secondary use cases: pre-migration audit, config review, engineer onboarding.
Every output element carries a confidence level:
| Level | Meaning |
|---|---|
| ✓ Exact | Guaranteed correct — direct syntactic equivalent |
| ⚠ Approximate | Functional equivalent with caveats — report explains what to verify |
| ✗ Manual | No direct equivalent — commented block with recommended approach |
| Platform | Vendor-specific command with no cross-platform concept |
netconv never silently drops commands. Everything unrecognised is preserved as a commented block with context.
A switch is not a router, and no one replaces one with the other. netconv treats "convert a switch config" and "convert a router config" as two different jobs, not two branches of the same function:
- L2 profile — VLANs, access/trunk ports, STP, voice VLAN, storm-control. Targets a switch platform (e.g. Huawei S-series).
- L3 profile — IP addressing, static/OSPF/BGP routing, ACLs, NAT, HSRP/VRRP. Targets a router platform (e.g. Huawei AR/NE).
You pick the profile explicitly (CLI --profile l2|l3, web UI tabs) — netconv never guesses it from the config's contents. If the source config contains commands from the other domain (OSPF found while converting under --profile l2, for example), netconv reports it explicitly instead of either rendering nonsense or quietly dropping it.
netconv --input switch.cfg --to vrp --profile l2 # VLANs/switchport only
netconv --input router.cfg --to vrp --profile l3 # routing/ACL/NAT onlyThis is implemented per-vendor-pair, not as a blanket rule — see the coverage table below for which pairs already enforce it (currently ios → vrp and ios → eltex).
Open live demo, paste config, press convert or Ctrl+Enter.
Config is processed locally in the browser via WASM — never sent to a server.
git clone https://github.com/casablanque-code/netconv.git
cd netconv
# Build WASM
cargo install wasm-pack
wasm-pack build crates/netconv-wasm --target web --out-dir ../../web/wasm --release
# Serve locally
python3 -m http.server 8080 --directory web/
# Open http://localhost:8080
# "demo mode" badge disappears — full Rust parser activeWSL: ports forward automatically in WSL2. Use
http://localhost:8080in Windows browser.
cargo build --release -p netconv
# Convert (stdout)
./target/release/netconv --input router.cfg --from ios --to vrp
# Convert with explicit device profile — filters L2 vs L3 domain (see below)
./target/release/netconv --input switch.cfg --to vrp --profile l2
./target/release/netconv --input router.cfg --to vrp --profile l3
# Write to file
./target/release/netconv --input router.cfg --to vrp --output router_vrp.cfg
# Show warnings and manual items
./target/release/netconv --input router.cfg --warnings
# Full report as JSON (for scripts / CI gates)
./target/release/netconv --input router.cfg --json > report.jsonUse netconv as a network config linter in your pipeline:
# Fail pipeline if any manual intervention required
./netconv --input router.cfg --json | jq '.report.manual_required == 0'
# Fail if coverage drops below threshold
./netconv --input router.cfg --json | jq '.report.coverage_pct >= 95'| Source | Target | L2 profile | L3 profile |
|---|---|---|---|
| Cisco IOS | Huawei VRP | ✓ Implemented (S-series) | ✓ Implemented (AR/NE) |
| Cisco IOS | Eltex | ✓ Implemented (MES, switch) | ✓ Implemented (ESR, router/firewall) |
| Cisco IOS | VyOS | 🚧 Planned | 🚧 Planned |
| Cisco ASA | — | n/a (ASA has no L2 role) | 🚧 Planned |
--profile without a value (legacy VrpRenderer/EltexRenderer) still renders everything unfiltered — kept only for backward compatibility with existing scripts.
| Feature | Status | Notes |
|---|---|---|
| hostname | ✓ Exact | → sysname |
| NTP server | ✓ Exact | → ntp-service unicast-server |
| SNMP community | ✓ Exact | → snmp-agent community |
| SNMP reserved name conflict | ⚠ Manual | e.g. community "write" conflicts with VRP keyword |
| Logging buffered | ⚠ Approx | → info-center logbuffer size |
| SSH version | ⚠ Approx | → stelnet server enable |
| line vty exec-timeout | ⚠ Approx | → user-interface vty / idle-timeout |
| line vty transport input | ⚠ Approx | → protocol inbound |
| username + privilege | ✗ Manual | → local-user (password cannot be migrated) |
| enable secret | ✗ Manual | No VRP equivalent — AAA config required |
| aaa new-model | ✗ Manual | → authentication-scheme + domain |
| ip http server | ✗ Manual | → http server enable / undo |
| Feature | Status | Notes |
|---|---|---|
| vlan batch | ⚠ Approx | Auto-collected from all interface references |
| vlan name | ⚠ Approx | → description |
| spanning-tree mode rapid-pvst | ✗ Manual | HIGH RISK → rstp (one tree vs per-VLAN). MSTP starting point auto-generated |
| spanning-tree loopguard | ⚠ Approx | → stp loop-protection |
| spanning-tree portfast | ⚠ Approx | → stp edged-port enable |
| spanning-tree bpduguard | ⚠ Approx | → stp bpdu-protection |
| spanning-tree bpdufilter (access) | ⚠ Approx | → stp bpdu-filter enable |
| spanning-tree bpdufilter (trunk) | ✗ Manual | RISK — not auto-applied on trunk ports |
| spanning-tree vlan priority | ✗ Manual | No per-VLAN priority in RSTP |
| Feature | Status | Notes |
|---|---|---|
| ip address | ✓ Exact | |
| shutdown / no shutdown | ✓ Exact | → shutdown / undo shutdown |
| description | ✓ Exact | |
| switchport access vlan | ⚠ Approx | → port default vlan |
| switchport trunk allowed | ⚠ Approx | → port trunk allow-pass vlan |
| switchport voice vlan (access) | ⚠ Approx | → voice-vlan X enable + global voice-vlan enable |
| switchport voice vlan (trunk) | ⚠ Warn | Non-standard — applied with warning |
| storm-control level | ⚠ Approx | → percent (assumption flagged inline) |
| ip helper-address | ⚠ Approx | → dhcp relay server-ip |
| ip access-group | ⚠ Approx | → traffic-filter |
| ip nat inside/outside | ⚠ Approx | NAT moves to interface context |
| Feature | Status | Notes |
|---|---|---|
| ip default-gateway | ⚠ Approx | → ip route-static 0.0.0.0/0 |
| ip route (static) | ⚠ Approx | → ip route-static, AD → preference |
| OSPF process | ⚠ Approx | network inside area, log-peer-change |
| OSPF redistribute | ⚠ Approx | → import-route |
| OSPF passive-interface | ⚠ Approx | → silent-interface |
| BGP neighbors | ⚠ Approx | neighbor → peer, remote-as → as-number |
| BGP peer-groups | ⚠ Approx | → peer-group NAME |
| BGP address-family | ⚠ Approx | → ipv4-family unicast |
| BGP route-map | ⚠ Approx | → route-policy import/export |
| BGP next-hop-self | ⚠ Approx | → next-hop-local |
| HSRP → VRRP | ⚠ Approx | Protocols incompatible at wire level — MAC differs |
| HSRP track | ✗ Manual | → NQA/BFD required |
| EIGRP | ✗ Manual | Cisco proprietary — not supported on VRP |
| Feature | Status | Notes |
|---|---|---|
| ip access-list named/numbered | ⚠ Approx | acl name, rule, numbering remapped |
| NAT overload (PAT) | ⚠ Approx | → nat outbound on interface |
| NAT static | ⚠ Approx | Reversed global/inside order |
crates/
netconv-core/ # Vendor-neutral IR, ConfigParser/ConfigRenderer traits, ConversionReport,
# profile.rs — DeviceProfile (L2Switch/L3Router) + domain mismatch detection
# → published to crates.io as `netconv-core`
ios-config/ # Cisco IOS parser: pass1 structural tree, pass2 semantic analysis
# → published to crates.io as `ios-config` (own version, see below)
netconv-render-vrp/ # Huawei VRP renderer — VrpL2Renderer / VrpL3Renderer (domain-filtered) +
# legacy VrpRenderer (unfiltered, kept for backward compat)
netconv-render-eltex/# Eltex renderer — EltexL2Renderer (MES, switch) / EltexL3Renderer (ESR, router) +
# legacy EltexRenderer (= L3, kept for backward compat)
netconv-wasm/ # WASM bindings (wasm-bindgen): convert_config (legacy) + convert_config_profiled
cli/ # CLI binary (clap) — --profile l2|l3
web/
index.html # Single-file UI: L2/L3 tabs, WASM auto-load, demo fallback
worker.js # Cloudflare Worker
netconv-core and ios-config are general-purpose enough to be useful
standalone (e.g. just parsing IOS config into a typed IR without converting
anything), so they're published to crates.io straight from this repo. Unlike
every other crate here, they version independently of the netconv app —
ios-config continues its own pre-existing crates.io history (it used to
live in a separate casablanque-code/ios-config repository, alongside a
now-deprecated ios-config-core that has been folded into netconv-core),
and netconv-core is versioned on its own as a new crate. Tagging an app
release (see Releasing) publishes whichever of the two has a
manifest version not yet on crates.io — it does not force a bump of either
just because the app was tagged.
One new crate, implement ConfigRenderer. Parser and IR are untouched. If the platform has distinct switch and router product lines (most do — that's the point), split the renderer into two structs from the start rather than adding a "convert everything" renderer that has to be un-mixed later:
pub struct FooL2Renderer; // switch: VLANs, switchport, STP
pub struct FooL3Renderer; // router: addressing, routing, ACL, NAT
impl ConfigRenderer for FooL3Renderer {
fn render(&self, config: &NetworkConfig, report: &mut ConversionReport) -> Result<String, _> {
// render only L3-relevant IR fields to Foo router syntax
}
fn vendor_name(&self) -> &str { "Foo Router" }
}Register both in netconv-wasm/src/lib.rs::run_conversion_profiled and in cli/src/main.rs's profile match arms. Add to the web UI's dst-vendor select, gated per-profile the same way updateVendorOptionsForProfile() already gates Eltex for L2.
See crates/netconv-render-vrp/src/scope.rs for the pattern used to share code between the two renderers where a vendor's switch and router syntax overlap (e.g. common system settings).
netconv encodes cross-vendor knowledge that engineers learn the hard way:
- HSRP → VRRP: protocols are wire-incompatible. MAC address changes from
0000.0c07.acXXto0000.5e00.01XX. All nodes must be migrated simultaneously. - rapid-pvst → rstp: Cisco runs a separate STP tree per VLAN. Huawei RSTP runs one tree for all VLANs. Topology can change silently if different VLANs had different root bridges.
- BPDU filter on trunk: context-aware — not applied automatically on trunk ports where it can cause loops.
- SNMP community name conflicts: VRP reserves
read,write,trapas keywords — using them as community names produces ambiguous syntax. - storm-control level: Cisco
levelunits vary by platform (%, pps, kbps). Every generated command is flagged with an explicit assumption.
| Component | Version | When needed |
|---|---|---|
| Rust | 1.75+ | always |
| wasm-pack | 0.13+ | WASM build only |
| Python 3 | any | local HTTP server only |
| Node.js | 18+ | Cloudflare Workers deploy only |
Install Rust: https://rustup.rs
Install wasm-pack: cargo install wasm-pack
cargo test # all tests
cargo test -p ios-config # parser only (19 tests)The netconv app's own version (CLI, WASM bindings, both renderers,
integration tests) lives in exactly one place: [workspace.package] version in the root Cargo.toml, inherited via version.workspace = true — nothing else to bump by hand for an app release.
netconv-core and ios-config are the exception: they're published
libraries with their own independent version in their own Cargo.toml,
unrelated to the app version above. Bump one of those by hand (in its
Cargo.toml, following semver for the change you made) when you want
to ship a new release of that crate specifically.
To cut an app release: Actions → Prepare Release → Run workflow,
enter a version (X.Y.Z, no v prefix). It bumps Cargo.toml,
commits, tags, and pushes — which triggers release.yml:
- builds the CLI for linux/macos/windows and publishes a GitHub Release with the binaries
publish-cratesjob: checks crates.io's index fornetconv-core's andios-config's current manifest version and publishes whichever one isn't already there (in that dependency order). If you didn't bump either this time, both are skipped — tagging an app release never forces a library release. Requires aCARGO_REGISTRY_TOKENrepo secret.
The web app's WASM binary (web/wasm/*.wasm) is a committed build
artifact rebuilt automatically by rebuild-wasm.yml on every push to
main that touches a WASM-feeding crate — no manual wasm-pack step
required for that either.
See CHANGELOG.md for what shipped in each version.
MIT