Skip to content

dlorent/es4308cfg

Repository files navigation

es4308cfg — reverse-engineering an abandoned switch's config format

CI

The Edgecore ES4308-PoE is a 2008-era 8-port PoE switch. Its web UI downloads and uploads a switch.cfg binary — but the format is proprietary and I couldn't find any public byte-level documentation for it. This repo is a reproducible case study in cracking that format, with a byte-exact codec, a formal spec, and sanitized fixtures.

The atlas: an interactive dashboard rendered from a single config file

es4308ctl atlas turns one switch.cfg into an interactive dashboard — front panel, port×VLAN matrix, topology, security posture. (Rendered from a sanitized sample.)

# Rust: one static binary — decode to JSON, human summary, validate
cd es4308cfg && cargo run -qr -- show   ../samples/switch-port7.cfg
                cargo run -qr -- decode ../samples/switch-port7.cfg   # -> JSON
# Python: the same decode, plus the interactive atlas and config-as-code
python3 es4308ctl/cli.py atlas samples/switch-port7.cfg               # -> edgecore.html
just check          # run everything CI runs: rust + python + kaitai + cross-check + leak

The Python tool is stdlib-only (no pip install) on a bare Python 3.9+. The Rust codec needs cargo; compiling the Kaitai spec needs Java + kaitai-struct-compiler (the Kaitai Web IDE needs neither). If you have nix, nix develop gives you a shell with the whole toolchain and just check runs the full suite.

Scope note: verified against a single ES4308-PoE unit; other units/firmware revisions may differ. Everything runs on the sanitized samples/ — no hardware needed.

Two codecs, cross-checked. The Rust crate (es4308cfg/) is the shipped byte-exact codec + single-binary CLI (decode/show/verify/dump). The Python tool (es4308ctl/) is the RE + tooling layer (atlas, YAML, diffing) and carries its own decoder. They're independent implementations of the same spec, and CI asserts they produce identical decode output on every sample (tools/crosscheck.py) — so the duplication is a differential test, not dead weight.

How it was cracked

The format was recovered from a 1056-byte blob by differential analysis: change one thing in the web UI, re-download, diff, repeat. Full method in es4308cfg/docs/RE-NOTES.md; byte-level spec in es4308cfg/docs/FORMAT.md.

  1. Framing. CONF magic, then TLV records: BA BE | type | flag | len(u16 be) | data.
  2. The bytes at 0x0c..0x0f read 01 15 73 88 and look exactly like a CRC. They aren't one field: it's a constant, a checksum byte, and a device constant. The real integrity is two plain sums — a 16-bit body sum at 0x06:07 (which itself reads 4D 57, ASCII "MW": a second red herring) and a two's-complement 8-bit sum at 0x0d.
  3. Obfuscated strings. SNMP communities are stored as char + index; subtract the position and 70 76 64 6f 6d 68 becomes public.
  4. A permutation. VLAN membership is a bitmask whose port→bit map is not identity (p1→0, p2→1, p3→2, p4→7, …). The same permutation shows up again in the per-port PVID table (entry = PVID << 4), the speed/duplex/flow array, and the LLDP table, which is how we knew it was structure and not coincidence. (PoE's enable bitmask uses a different, nibble-split order; one vendor happily mixes conventions inside a single blob.)
  5. Finally, proof by reconstruction. The encoder regenerates a real switch-made change (switch.cfg + add VLAN 200) byte-for-byte identical to the switch's own export.

What's proven (and what isn't)

Element Status Evidence
Record framing (18 records) ✅ proven serialize(parse(x)) == xroundtrip_byte_exact
Both checksums ✅ proven recomputed == stored on all samples — checksums_valid_on_samples
VLAN membership + port permutation ✅ proven decode matches the switch's VidMasks across 11 VLANs — test_golden
Per-port PVID (pvid<<4, permuted) ✅ proven decoded VLAN set + PVID array checked against a hand-verified oracle on all 7 samplestest_pvid_and_vlans_on_every_sample
Add-a-VLAN encode ✅ proven reproduces a real switch change byte-identical — reproduce_switch_port7_byte_identical
Config ⇄ YAML ✅ proven byte-exact round-trip on all samples — test_yamlio
Rust decoder == Python decoder ✅ proven identical decode JSON on all 7 samples — crosscheck.py (CI)
verify rejects malformed input ✅ proven all-zero / overrun / trailing-garbage rejected — test_verify
Kaitai spec ✅ compiles + parses CI compiles spec/es4308.ksy and parses a sample
Per-port speed / duplex / flow ✅ proven 4 changes round-trip byte-exact — test_portcfg_poe_lldp
PoE enable + priority (nibble-split order) ✅ proven disabling p3/p5/p6 → mask 0xbc; p6→Critical lands on the nibble-split slot (not PORT_BIT's) — test_poe_priority_nibble_split
Per-port LLDP enable ✅ proven disabling p4/p6 tracks PORT_BIT tail — test_portcfg_poe_lldp
PoE per-port limit 🟡 partial 009a=15.4 W decoded; only the uniform 802.3af value sampled
STP timers 🟡 partial timers match the RSTP page; global-enable not located in binary
0x065 per-port array bit 🟡 partial located (toggles on membership change), meaning not decoded
Ingress frame-type (admit-tagged-only) 🟡 partial readable via web UI; binary location not yet isolated
Records 0x03/0x0d/0x10/0x11/0x13/0x14 ❔ unknown preserved verbatim
Live write / restore ⛔ unsafe corrupted NVRAM in testing — quarantined (docs/INCIDENT.md)

Only one add-VLAN edit is byte-reproduced end-to-end (VLAN 200, ports 7/8); that's a strong anchor, not exhaustive coverage.

The tools

Decode cfg → JSON (Rust or Python): VLANs, per-port tagged/untagged + PVID, speed/duplex/flow, LLDP, PoE, STP timers, mgmt, SNMP, identity
Single binary es4308cfg decode|show|verify|dump|vlans — zero-dependency Rust CLI
Visualize atlas → self-contained interactive edgecore.html
Config-as-code yamlio — byte-exact cfg ⇄ YAML round-trip
Generate generate --vid 205 --tagged 8 --untagged 5 → a config whose bytes self-check. Note: self-check proves encoder consistency, not that the switch will accept it on upload — validate before flashing.
Formal spec spec/es4308.ksy — open a sample in the Kaitai Web IDE or generate parsers in C++/Python/Go/…

Repo map

samples/       sanitized sample configs (safe to publish; tests run on these)
spec/          formal format spec — es4308.ksy (Kaitai) + usage
es4308cfg/     Rust codec + single-binary CLI (parse/edit/decode/verify) + docs/{FORMAT,RE-NOTES}.md
es4308ctl/     Python tool (decode/show/atlas/verify/diff/generate/yamlio + read-only web)
bindiff/       a generic differential-RE harness distilled from this project (secondary)
experimental/  the dangerous live write/restore path — quarantined; see docs/INCIDENT.md
docs/          INCIDENT.md (post-mortem) + design notes
tools/         sanitize.py (scrub captures), leakscan.py (repo-wide), crosscheck.py (rust↔python)
flake.nix · justfile   pinned toolchain (`nix develop`) + one-command checks (`just check`)

Safety

Config download and decode are read-only and safe. Writing to the switch corrupted the identity/MAC record during testing (the forms write fixed NVRAM blocks), so that entire path is quarantined in experimental/ with the post-mortem in docs/INCIDENT.md. Don't point it at a switch you can't recover.

Samples are sanitized (synthetic MAC / RFC-5737 mgmt IP; SNMP public/private are the vendor defaults, not a secret). The real→synthetic mapping lives in a gitignored manifest, not in tracked source, and tools/leakscan.py runs in CI to assert no real identifier appears in any tracked file (not just the samples). MIT licensed.

About

Reverse-engineering the proprietary NVRAM config format of an abandoned Edgecore ES4308-PoE switch: a byte-exact Rust codec + decoder, a Python tooling layer, a Kaitai spec, and sanitized fixtures — differentially cracked and cross-checked.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors