The Edgecore ES4308-PoE is a 2008-era 8-port PoE switch. Its web UI downloads and uploads a
switch.cfgbinary — 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.
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 + leakThe 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.
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.
- Framing.
CONFmagic, then TLV records:BA BE | type | flag | len(u16 be) | data. - The bytes at
0x0c..0x0fread01 15 73 88and 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 at0x06:07(which itself reads4D 57, ASCII "MW": a second red herring) and a two's-complement 8-bit sum at0x0d. - Obfuscated strings. SNMP communities are stored as
char + index; subtract the position and70 76 64 6f 6d 68becomespublic. - 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.) - 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.
| Element | Status | Evidence |
|---|---|---|
| Record framing (18 records) | ✅ proven | serialize(parse(x)) == x — roundtrip_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 samples — test_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.
| 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/… |
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`)
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.
