Skip to content

Repository files navigation

switchos-client

A Go client for MikroTik SwOS-managed switches (both the "Lite" and "classic" protocol dialects), generated directly from each device's own engine.js web-UI script — no hand-authored intermediate schema.

Why

SwOS devices don't expose a documented HTTP API. Their web UI ships a minified engine.js that itself contains a complete, authoritative description of every field the device supports (labels, types, enums, read-only flags, etc.). Rather than hand-transcribing that into a schema (which drifts from reality and is error-prone), this repo executes the device's actual engine.js in a Node vm sandbox and introspects its live field registry to generate an OpenAPI 3.0 document, which is then fed to oapi-codegen to produce a readable Go client.

Two supported protocol dialects, each a separate generator because closure-compiler minification assigns arbitrary, per-build different identifiers to the same concepts. Within a dialect, each BOARD also gets its own generated openapi.json and Go client package (kept fully separate, never overwriting one another) since it isn't yet confirmed that the schema/minified identifiers are identical across boards of the same dialect:

Dialect Board Generator Client package
Lite CSS610PI boards/css610pi/engine-to-openapi-css610pi.js boards/css610pi
Lite CSS610G boards/css610g/engine-to-openapi-css610g.js boards/css610g
Classic CSS106P (also covers RB260GS) boards/css106p/engine-to-openapi-css106p.js boards/css106p
Classic CSS318G boards/css318g/engine-to-openapi-css318g.js boards/css318g
Classic CSS328P boards/css328p/engine-to-openapi-css328p.js boards/css328p
Classic CSS354 boards/css354/engine-to-openapi-css354.js boards/css354

CSS318G and CSS328P ship a genuinely different closure-compiler build than CSS106P (different minified identifiers and object property keys throughout, though confirmed identical to EACH OTHER except one type-constructor letter), so each gets its own dedicated generator (engine-to-openapi-css318g.js / engine-to-openapi-css328p.js) rather than reusing engine-to-openapi-css106p.js - see each script's header comment for the full rationale and the specific caveats (each board's fixed port layout is hardcoded since it's normally hardware-detected at runtime; fields/pages gated on PoE/fan/ temperature-sensor capability come from each board's own engine.js build).

CSS354 looks like the same "CSS3xx family" structurally (same page registry shape, same V()/W()/Tb()/Ub() entrypoints, same stable field-object-literal keys) but turned out to be a genuinely DIFFERENT closure-compiler build than CSS318G/CSS328P: nearly every single-letter identifier is shifted by one further down the alphabet (verified by direct inspection), so it gets its own dedicated generator, engine-to-openapi-css354.js, rather than reusing engine-to-openapi-css318g.js / engine-to-openapi-css328p.js

  • see that script's header comment for the full letter-mapping. CSS354's port layout (28 total, 24 SFP+) is inferred from its known hardware spec (CSS354-4G-24S+2Q+RM: 4x GbE combo + 24x SFP+ + 2x QSFP+ uplinks), not independently confirmed against a live device.

CSS610G looked like it might share CSS610PI's exact build (both Lite, both expose the same wire ids, e.g. i01/i0a/...) but turned out to be a genuinely different closure-compiler build too: it inserts one extra type-constructor function (used only by a "Cable Pairs" field CSS610PI's build doesn't have at all), which shifts several subsequent single-letter type/property identifiers - confirmed by direct diff of the two engine.js files. It gets its own dedicated generator, boards/css610g/engine-to-openapi-css610g.js, rather than reusing boards/css610pi/engine-to-openapi-css610pi.js - see that script's header comment for the full letter-mapping. Since the wire protocol itself (ids, endpoints) is identical to CSS610PI, its Go-name override table is reused as-is.

Layout

  • boards/<board>/ — one directory per board, each containing the captured engine.js file(s) (per firmware version) used as ground truth input to that board's dedicated engine-to-openapi*.js generator, plus the generated output - the OpenAPI 3.0 document (swos-<board>.openapi.json) and the Go client (swos_client.gen.go, package name = the board slug). Do not hand-edit the generated files; re-run boards/generate-clients.sh instead. See "Extracting engine.js" below for how the engine.js files are (re)produced.
  • boards/generate-clients.sh — single script that regenerates every board's OpenAPI document and Go client in one run. See "Regenerating the clients" below.
  • boards/extract-engine-from-firmware.sh — a single script that extracts any board's engine.js from its firmware .bin image (the board name is derived from the firmware filename itself).
  • boards/build/ — shared build tooling used by the generators and scripts above, kept out of any single board's folder: extraction helpers used by boards/extract-engine-from-firmware.sh (find-gzip-offsets.sh, gunzip-at-offset.sh, extract-inline-script.js), the shared wire-kinds-go.js helper used by every board's engine-to-openapi*.js generator to emit that board's wire_kinds_*.gen.go file, and oapi-codegen-config.yaml, the single shared oapi-codegen config used to generate every board's client (both dialects). The generated per-board OpenAPI documents themselves live in boards/<board>/.
  • boards/common/digestauth/ — a minimal HTTP Digest Authentication (RFC 2617/7616) http.RoundTripper, since SwOS devices require Digest auth.
  • boards/common/swoswire/ — an http.RoundTripper (Transport) that bridges SwOS's non-standard wire format and the generated JSON-based client:
    • GET responses aren't JSON: the device returns Content-Type: application/x-javascript with a JS object-literal body (unquoted keys, single-quoted strings, hex-literal numbers). Transport rewrites this into standard JSON before the generated client parses it.
    • Certain fields (MAC addresses, hex-encoded ASCII strings, packed IPv4 addresses) are further decoded into human-usable Go values (e.g. "AA:BB:CC:DD:EE:FF" instead of a raw hex blob), driven by a generated per-board Kinds map living alongside that board's client package (e.g. boards/css610pi/wire_kinds_css610pi.gen.go exposes css610pi.Css610piWireKinds).
    • Writes are symmetric: outgoing JSON request bodies (from generated Put* client calls) are re-encoded back into the device's proprietary text/plain wire format.
  • boards/<board>/test/main.go — a standalone smoke-test program per board that connects to a real device and dumps every endpoint's data. See "Testing against a real device" below.
  • boards/common/apitest/ — a small reflection-based helper (apitest.DumpAll) shared by every boards/<board>/test/main.go, which calls every Get*WithResponse method the board's generated client exposes and prints its status code and JSON body, without hand-listing each board's endpoints.

Usage

httpClient := &http.Client{
    Transport: &swoswire.Transport{
        Base: &digestauth.Transport{
            Username: "admin",
            Password: "qw",
        },
        Kinds: css610pi.Css610piWireKinds,
    },
}
client, err := css610pi.NewClientWithResponses("http://192.168.40.22", css610pi.WithHTTPClient(httpClient))
if err != nil {
    panic(err)
}

sys, err := client.GetSysWithResponse(context.Background())
if err != nil {
    panic(err)
}
fmt.Println("Identity:", *sys.JSON200.Identity)

// Writes are full-page replaces (the device has no partial-update
// semantics) - start from a GET result and mutate it.
newIdentity := "My Switch"
sys.JSON200.Identity = &newIdentity
if _, err := client.PutSysWithResponse(context.Background(), *sys.JSON200); err != nil {
    panic(err)
}

See boards/css610pi/test/main.go for a runnable version.

Testing against a real device

Each board has its own standalone smoke-test program at boards/<board>/test/main.go, which connects to a real device and dumps every endpoint's data (via boards/common/apitest's reflection-based helper, so it stays in sync automatically as a board's schema changes):

go run ./boards/css610pi/test -host 192.168.88.1 -username admin -password ''

All three flags default as shown above (admin/blank password is SwOS's factory default). Substitute the board's directory (css610g, css106p, css318g, css328p, css354) to test a different board.

Regenerating the clients

A single script regenerates every board's OpenAPI document and Go client:

boards/generate-clients.sh

Set CSS106P_DEVICE_BOARD=RB260GS (defaults to "CSS106-1G-4P-1S", the PoE variant) to switch the CSS106P board variant passed into the sandbox.

For each board, the script parses that board's engine.js directly into an OpenAPI document (boards/<board>/*.openapi.json), emits a boards/<board>/wire_kinds_*.gen.go map of fields needing semantic decoding, and runs oapi-codegen; go mod tidy runs once at the end.

Requires node (to execute engine.js in a sandbox) and go (oapi-codegen itself is fetched on demand via go run).

Extracting engine.js from a firmware image

A single script extracts any board's engine.js from its firmware .bin image - the board name is derived automatically from the firmware filename (swos-<slug>-<version>.bin, with a couple of known naming exceptions handled internally, e.g. css106 -> CSS106P):

boards/extract-engine-from-firmware.sh swos-css106-2.18.bin    # -> boards/css106p/2.18-CSS106P.js
boards/extract-engine-from-firmware.sh swos-css318g-2.18.bin   # -> boards/css318g/2.18-CSS318G.js
boards/extract-engine-from-firmware.sh swos-css328p-2.18.bin   # -> boards/css328p/2.18-CSS328P.js
boards/extract-engine-from-firmware.sh swos-css354-2.17.bin    # -> boards/css354/2.17-CSS354.js
boards/extract-engine-from-firmware.sh swos-css610g-2.21.bin   # -> boards/css610g/2.21-CSS610G.js
boards/extract-engine-from-firmware.sh swos-css610pi-2.21.bin  # -> boards/css610pi/2.21-CSS610PI.js

Output path is always boards/<board, lowercased>/<version>-<BOARD>.js, with <version> parsed from the firmware filename's trailing X.Y. binwalk locates the offset of the gzip blob(s) embedded in the firmware .bin, and the last one found is decompressed. Most firmwares wrap engine.js in a full index.html with a single inline <script>...</script> tag, which is unwrapped automatically; some (e.g. CSS610G, CSS610PI) embed engine.js directly as their own gzip member with no HTML wrapper at all - the script detects this and skips the unwrap step accordingly.

Note that a newly extracted engine.js is not automatically generator-ready - see the caveat below.

Caveats

  • Each generator's sandbox driver hardcodes exact minified identifier names (entrypoint function, type-constructor function letters, etc.) from ONE specific engine.js build. A different closure-compiler build - even for the same board family/dialect - reassigns those single/double-letter names, so a newly captured engine.js is not guaranteed to work with either generator out of the box; it may need the hardcoded names re-derived for that build (or, for some newer firmware, the sandbox may need to emulate live device XHR responses used for runtime hardware-capability detection, which neither generator currently supports). Always run the generator against a new engine.js and check for errors before assuming it's supported - don't assume "it's in boards/" implies "the generator understands it".
  • Property names in the generated structs are the device's own wire keys (i01, i02, ...); each has an x-go-name giving it a readable Go field name, but the JSON tag itself stays as the wire key.
  • Only tested against the specific firmware/board combinations captured under boards/. A different firmware version could map fields differently - always regenerate against your own device's engine.js if in doubt.
  • Some boards/*/*.js files were extracted from firmware images without access to the physical device, so their exact board name (as reported by the device itself) isn't independently confirmed - the filename reflects either a literal internal fallback string found in the JS (e.g. CSS318G/CSS328P, from n="css318g"/n="css328p" fallback assignments) or the firmware image's own filename suffix (CSS354, CSS610G - these boards report their name via runtime hardware detection with no matching string literal in the JS itself).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages