Translate a network config from one vendor to another — and get a list of everything that didn’t.

Netcanon parses one vendor’s running-config, renders another’s, and declares every field it translates as supported, lossy, or unsupported — with the reason.

Cisco IOS-XE · Cisco NX-OS · Cisco IOS-XR · Juniper Junos · Arista EOS · Aruba AOS-S · Aruba AOS-CX · FortiGate · MikroTik RouterOS · OPNsense · VyOS

Handling something sensitive? Don’t paste it. Run the container yourself — it is one command, and it is the same image the demo runs.

INPUT (cisco_iosxe_cli)

hostname access-sw-01
!
vlan 10
 name DATA
!
vlan 20
 name VOICE
!
interface GigabitEthernet1/0/1
 description Server-A
 switchport mode access
 switchport access vlan 10
 no shutdown
!
interface GigabitEthernet1/0/2
 description Desk-Phone
 switchport mode access
 switchport access vlan 20
 no shutdown
!
interface GigabitEthernet1/0/24
 description Uplink-to-core
 switchport mode trunk
 switchport trunk allowed vlan 10,20
!
snmp-server community public RO
ip name-server 192.168.1.10
ip name-server 192.168.1.11
ntp server 192.168.1.20
!
ip access-list extended MGMT-PROTECT
 permit tcp 192.168.99.0 0.0.0.255 any eq 22
 deny   ip any any log
!
ip nat inside source list MGMT-PROTECT interface GigabitEthernet1/0/24 overload
!
ip route 0.0.0.0 0.0.0.0 192.168.1.1

OUTPUT (juniper_junos)

set system host-name access-sw-01
set system name-server 192.168.1.10
set system name-server 192.168.1.11
set system ntp server 192.168.1.20
set interfaces ge-1/0/1 description "Server-A"
set interfaces ge-1/0/1 unit 0 family ethernet-switching interface-mode access
set interfaces ge-1/0/1 unit 0 family ethernet-switching vlan members DATA
set interfaces ge-1/0/2 description "Desk-Phone"
set interfaces ge-1/0/2 unit 0 family ethernet-switching interface-mode access
set interfaces ge-1/0/2 unit 0 family ethernet-switching vlan members VOICE
set interfaces ge-1/0/24 description "Uplink-to-core"
set interfaces ge-1/0/24 unit 0 family ethernet-switching interface-mode trunk
set interfaces ge-1/0/24 unit 0 family ethernet-switching vlan members DATA
set interfaces ge-1/0/24 unit 0 family ethernet-switching vlan members VOICE
set vlans DATA vlan-id 10
set vlans VOICE vlan-id 20
set routing-options static route 0.0.0.0/0 next-hop 192.168.1.1
set snmp community public authorization read-only

This pane is the unedited output of:

docker run --rm --entrypoint netcanon ghcr.io/netcanon/netcanon:latest demo --pair cisco__junos

Installed via pip instead? netcanon demo --pair cisco__junos. Image is amd64/x86-64 only today.

INPUT (fortigate_cli)

config system global
    set hostname "branch-fw-01"
end
config system dns
    set primary 192.168.1.10
    set secondary 192.168.1.11
end
config system interface
    edit "internal1"
        set ip 192.168.10.1 255.255.255.0
        set allowaccess ping
    next
    edit "internal2"
        set ip 192.168.20.1 255.255.255.0
        set allowaccess ping
    next
end
config system dhcp server
    edit 1
        set interface "internal1"
        set default-gateway 192.168.10.1
        set netmask 255.255.255.0
        config ip-range
            edit 1
                set start-ip 192.168.10.100
                set end-ip 192.168.10.200
            next
        end
    next
end
config firewall policy
    edit 1
        set name "lan-to-wan"
        set srcintf "internal1"
        set dstintf "wan1"
        set srcaddr "all"
        set dstaddr "all"
        set action accept
        set schedule "always"
        set service "ALL"
        set nat enable
    next
end

OUTPUT (mikrotik_routeros)

/system identity
set name=branch-fw-01

/interface ethernet
set [ find default-name=ether1 ] disabled=no
set [ find default-name=ether2 ] disabled=no

/ip address
add address=192.168.10.1/24 interface=ether1
add address=192.168.20.1/24 interface=ether2

/ip pool
add name=dhcp_pool1 ranges=192.168.10.100-192.168.10.200

/ip dhcp-server network
add address=192.168.10.0/24 gateway=192.168.10.1

/system dns
set servers=192.168.1.10,192.168.1.11

Advisories: port ‘internal1’ / ‘internal2’ are hardware-switch (L2 fabric) members; the mapping to ‘ether1’ / ‘ether2’ is positional (name-shape only) and does NOT carry the L2-switch membership — verify the target port’s physical cabling and L2 role.

This pane is the unedited output of:

docker run --rm --entrypoint netcanon ghcr.io/netcanon/netcanon:latest demo --pair fortigate__mikrotik

Image is amd64/x86-64 only today.

More pairs — Aruba AOS-S → Arista, OPNsense → Junos: walkthroughs →

Every visitor gets a real, isolated instance — and when they run out, the demo refuses new sessions rather than quietly sharing one. The command above is the no-wait path.

What translates. What refuses. On purpose.

Translates: hostnames, interfaces (names, addresses, VRF bindings, MTU), VLANs and L2 membership, static routes, DNS/NTP, SNMP, LAGs, local users, DHCP pools, VRFs — the boring 80% of a migration. Cross-vendor interface naming included: GigabitEthernet1/0/1 becomes ge-1/0/1.

Detects and lists, deliberately does not translate: firewall rules, NAT, VPN, QoS, route-maps, dynamic routing stanzas. These are vendor-specific stateful policy that does not translate cleanly across vendors — so netcanon surfaces them by name in a banner instead of pretending.

Netcanon is the right tool for the router portion of a migration — and explicitly the wrong tool to claim it does the firewall portion.

Netcanon is built and maintained by one person. The audit machinery below exists because you shouldn’t have to take one person’s word for fidelity.

What works for your vendor: Cisco IOS-XE · NX-OS · IOS-XR · Junos · Arista EOS · Aruba AOS-S · Aruba AOS-CX · FortiGate · MikroTik · OPNsense · VyOS

Every field has a declared fate

An excerpt from the capability matrix — the fortigate_cli panel, verbatim:

Canonical pathClassReason
…/vrrp-groups/groupsupported Nested-edit form: config system interface / edit X / config vrrp / edit N / set vrip Y …
…/interface/config/descriptionlossy FortiOS limits the interface alias to 25 characters; longer descriptions from other vendors are truncated.
…/vrrp-groups/group/virtual-ipslossy FortiOS config vrrp / edit N accepts a single set vrip per group. Multi-IP canonical groups (IOS-XE repeated vrrp N ip X, Junos virtual-address [ X Y Z ]) emit the first VIP and drop the tail with a # review: line — operator must split into multiple groups manually.
…/filter/ruleunsupported config firewall policy is Tier 3 — session-based, zone-aware, UTM-enabled semantics don’t translate cleanly.
…/nat/ruleunsupported FortiGate NAT lives inside firewall policy and address / VIP objects — not auto-translatable.

Round-trip validated against real configs. The output is a draft for your review, not a deploy artifact.

The full matrix is in the docs — and queryable from the running app: GET /api/v1/migration/adapters/{name}/capabilities

Because you are being asked to paste a network config into someone else’s website

  • Your session is one throwaway container — destroyed when you close the tab or after 15 minutes, whichever comes first.
  • No network egress at all — the container holding your config cannot reach the internet.
  • Nothing written to disk — read-only root, tmpfs-only writes, swap off, core dumps discarded.

None of that is a promise you have to take on trust: the demo whitepaper states each claim, and VERIFY gives you the commands to check them yourself.

Demo bundle demo-v0.1.3 · deployed 2026-07-28.

Your session is a throwaway container. Your config is never stored. Sensitive? Don’t paste it — run it locally instead.

Run it on your machine

docker run --rm -p 8000:8000 -e NETCANON_ALLOW_INSECURE_BIND=1 \
    ghcr.io/netcanon/netcanon:latest

then open http://localhost:8000. The image refuses an unauthenticated non-loopback bind by default; the flag acknowledges you’re running it locally. For anything reachable by other hosts, set NETCANON_API_KEY instead.

Image is amd64/x86-64 only today.

Prefer pip? pip install netcanon — same app, same CLI. Windows? There’s an MSI on Releases.

No telemetry, no phone-home, no update checks — on any install path (pip, Docker, MSI). The only network connections netcanon makes are the SSH sessions you configure for backups.

The audit underneath

Every covered vendor pair is audited cell-by-cell for silent translation loss — the kind that produces output that looks valid but quietly drops a field. The methodology is published. So is the list of cells currently open: every residual is enumerated and triaged, nothing swept under the rug.

The honest follow-up: the audit only covers cells we have fixtures for. Real-world configs reach paths synthetic fixtures haven’t. That’s where you come in — see Contribute.

Also in the box

Config backup over SSH. Netcanon pulls running-configs on a schedule and stores them verbatim — its backup half exists to feed translation. If backup and diff is your only need, Oxidized and RANCID are purpose-built and more mature on device-driver breadth; we say so ourselves.

Config sanitizer. Sharing a config in a ticket or a forum post? Sanitize it first:

netcanon sanitize -i my-config.txt --source-vendor cisco_iosxe_cli --dry-run

Every substitution is shown for your review.

Where it fits

Most adjacent tools occupy a different slot — netcanon’s niche is bidirectional translation between vendors’ native running-config formats. Batfish analyses what you translated; NAPALM deploys it. Complement, not alternative.

ToolWhat it doesTranslates native config?
NetcanonMulti-vendor config translation (parse + render) Yes — bidirectional
BatfishConfig analysis + routing simulation No — parse-only; complements netcanon
Capirca / Aerleon Firewall ACL DSL → vendor syntax No — render-only from a DSL; competes (firewall scope)
NAPALM / Netmiko Device get/set + SSH transport No — no translation; complements (deploy side)
Oxidized / RANCID Multi-vendor config backup + diff No — backup-only; overlaps netcanon’s backup half

Cisco IOS-XE → Junos · FortiGate → MikroTik · Aruba AOS-S → Arista EOS · OPNsense → Junos — each walkthrough ends in a manual-review checklist.

Drive it from your stack

Run netcanon as a container beside your stack; drive it over the versioned HTTP API or the CLI. It is an app with an API, not an embeddable library — process isolation keeps its dependency tree out of yours.

curl -s -H "Authorization: Bearer $NETCANON_API_KEY" \
  -H "Content-Type: application/json" \
  -X POST http://localhost:8000/api/v1/migration/plan \
  -d '{"source":"cisco_iosxe_cli","target":"juniper_junos","raw_text":"..."}'

The response carries the rendered config and the declared losses in the same payload:

{
  "status": "completed",
  "validation": { "severity": "ok", "lossy_paths": [], "unsupported_paths": [] },
  "rendered": "set system host-name access-sw-01\n...",
  "port_renames": { "GigabitEthernet1/0/1": "ge-1/0/1" },
  "dropped_tier3_sections": [ "ip access-list extended MGMT-PROTECT" ]
}

Response excerpt — bookkeeping fields elided; the full shape is in the running app’s own /docs (OpenAPI).

v0.x promise: /api/v1 request/response shapes don’t break; changes ship as new endpoints; the CHANGELOG names every break.

Found a config it gets wrong?

That’s the contribution this project values most. Sanitize it first — the sanitizer shows every substitution for your review — then submit it as a fixture.

Sanitize + submit