Recon/diagnostic tooling for the "Goalake 8 Port Gigabit Ethernet Managed
Switch" (Amazon: B0D7PVBJ68), reverse-engineered from its own web UI.
Device tested: 192.0.2.10, firmware 6.0.250111, model 8GE.
The device. "Goalake" is a house brand of a Shenzhen OEM (jingsongke.com).
It's built on the same class of Realtek switch-SoC reference
design/firmware that a bunch of other Amazon-brand switches share
(Hasivo, MokerLink, Horaco, Sodola, etc — all "Easy Smart Managed" clones
with near-identical web GUIs). Only TCP port 80 is exposed — no
telnet/SSH/SNMP found in a scan of ~50 common ports (see portscan.sh).
The HTTP server identifies as lwIP/1.3.1, a minimal embedded TCP/IP
stack — this is not a general-purpose Linux box, it's a small RTOS-style
firmware.
The API. The whole UI is a single-page app. Every action POSTs JSON to
a numeric path that matches a callcmd id, e.g. POST /101 with body
{"data": {"callcmd": 101}}. We pulled every authenticated HTML page and
extracted every callcmd reference to build a full map of the switch's
RPC surface — see CALLCMD_CATALOG in switch_client.py for all ~25
documented commands (port state, VLANs, STP, port mirroring, storm
control, DHCP snooping, LLDP, MAC table, firmware upgrade, factory reset,
config backup/restore, etc). IDs 110-113,119,121,122,125,128-130,134,135
are never referenced by the UI JS at all — see Unmapped IDs below.
Auth. POST /123 with {"password": "..."} sets a random per-login
session cookie; every other call requires it. Two quirks worth knowing:
- The device only allows one active session at a time. A second login attempt while a session is live doesn't get a clean "already logged in" error — the TCP connection is just silently dropped, which looks identical to a network fault. If a script's login keeps failing, that's the first thing to suspect (another browser tab, or a previous run that didn't log out).
- A failed/unauthenticated call gets the same silent-connection-drop treatment rather than a proper 401/error JSON.
- The embedded httpd appears to only handle one request at a time — hit it too fast and connections start dropping. Scripts here add small delays and retries.
Interesting/notable findings (not necessarily "bugs", but worth knowing):
- Everything is plaintext HTTP — no HTTPS option at all. The login
password and the "change password" (
callcmd 120,Original_Key/New_Key) both go over the wire in clear text. callcmd 114with no calldata returns the switch's live learned MAC address table — every device MAC currently known via that switch's ports. On the test unit this returned 70 entries, i.e. it's seeing traffic from a much larger network segment behind one port, not just a handful of directly-attached devices.- The config export (
callcmd 201, "Backup Configuration" button) is a full JSON dump of the running config — no admin password/hash appears in it. loop_detect(115) andlink_agg/LACP (116) exist as API endpoints wired into the frontendurlMap, but both return"parameter error"when queried, and their menu entries are hidden/commented out in the dashboard HTML — these features look incomplete/disabled in this firmware build even though the plumbing exists.callcmd 101/200also return the device serial number and MAC address alongside the firmware version.
Unmapped IDs — now probed (probe_unmapped.py). We tested all 13
IDs not referenced by the UI, one at a time, diffing port/link state
before and after each call and health-checking the device in between.
None caused a reboot or any visible state change. Two different
behaviors turned up:
-
110,111,112,113,119,121,122,125,128,134,135— the connection gets silently dropped, the same failure mode as an unauthenticated or unrecognized request. These IDs don't appear to be implemented at all. -
129and130— these are implemented: they return a proper{"errmsg": "parameter error", "errcode": -1}JSON response (the same shapeloop_detect/link_agggive). That means they're real, functioning server-side commands with no UI page anywhere calling them - genuine hidden/unused API surface.The language file (
langs/en.js) has several strings with no matching page or menu item in this switch's UI at all:AI_Function_Config("AI Function Configuration"),AI_AnAuto("Port Watchdog"),AI_PortRestart("Automatic Port Extension"),poeSet("POE Setting"),Voltage/Power("POE Voltage"/"POE Power"), andrebootPort/reboottext("Port Reboot Setting") - plus amonitor("Monitor") top-level menu label. This model has no PoE output (it's a PD/powered-device input, not PoE-out), so these are very likely leftover strings from a PoE sibling model in the same firmware/OEM family that shares this language pack and API - a solid theory for what129/130are gated behind.We tested this theory with a handful of low-risk, targeted guesses (all against port 1, which was link-down at the time, so nothing live would be disrupted):
{"AI_AnAuto":[0,0,0,0,0,0,0,0]},{"AI_PortRestart":[0,0,0,0,0,0,0,0]},{"port":0},{"AI_AnAuto":0,"AI_PortRestart":0}, and (after research below turned up the OEM's internal name for this feature){"poedog":0}/{"poedog":[0,0,0,0,0,0,0,0]}. All attempts still returned the sameparameter errorwith zero state change - the exact field names remain unconfirmed.We also checked for unlinked static files this switch might still serve even without a menu item pointing at them (candidate paths like
page/Monitor/monitor.html,page/Port_Management/ai_function.html,page/System_Management/poe_set.html, plus a few leftover-icon guesses likeimg/poe.png) - none exist. Useful side-finding: this device answers any unrecognized GET path (not just failed/unauthenticated calls) by silently dropping the connection rather than a proper 404, so "connection dropped" vs "200 with content" is a reliable exists/doesn't-exist signal for this kind of path probing, as long as you pace requests (its httpd struggles with back-to-back connections).Public research (searching for a PoE-output sibling model's leaked firmware/web-UI source, GitHub code search for the exact language-file strings, and the OpenWrt "Hasivo switches" forum thread) found no concrete JSON schema or file path. It did turn up one solid new fact worth recording: Hasivo (a sibling brand on the same firmware family) calls this feature internally "poedog" (PoE-watchdog), per a ServeTheHome review comment - which also claims this "generic Realtek" web UI ships with hidden menus for changing branding/default settings across the whole rebadged product line, without further detail on how to reach them. We tried
poedogas a calldata key (above) without success. GitHub code search for the literal stringsAI_AnAuto/AI_PortRestart/poeSet/rebootPortreturned nothing relevant, and the OpenWrt thread is entirely serial-console/u-boot content with no mention of the JSON API.Update - manufacturer manuals obtained. The
manuals.pluspages are Cloudflare-gated against automated fetches (confirmed: a plain HTTP GET gets a bare403at Cloudflare's edge, before it even reaches the real server - a real browser passes the same challenge transparently). With saved copies of two manuals in hand (Goalake 10-Port Managed PoE Switch, and the 16-Port Gigabit Easy Smart Managed Switch - the non-PoE sibling closest to our actual unit), we now have real documentation instead of just language-file guesses:- The companion mobile app (not the web UI) has a documented,
non-hypothetical "reboot individual ports or schedule reboots"
feature, confirmed in both manuals - a POE Settings screen shows a
per-port table with
ExtendandTimingtoggle columns alongside the PoE on/off switch. This is a real, named feature in the shared OEM firmware family, not wired into our specific switch's web UI menu. - We retried
129with array-shaped field-name guesses matching this codebase's established per-port-array convention (every other per-port setting we've mapped -AdminState,tx/rx,up/down,trust_port,flow- is an 8-element array, not a single port index; our earlier{"port":0}guess didn't match that pattern):{"reboot":[0]*8},{"rebootPort":[0]*8},{"port_reboot":[0]*8},{"PortReboot":[0]*8}. Stillparameter errorevery time. - While doing this, port 8's link dropped between two of our checks. We confirmed this wasn't us: a real reboot would zero every port's TX/RX counters, but ports 4/5/7 kept counting live traffic throughout, and the serial/MAC/firmware were unchanged - so this looks like a real-world cable/device disconnection on that one port, coincidentally timed, not something our (all rejected) guesses caused.
Resolved. A community-maintained open-source Home Assistant integration for this exact device family, slydiman/sscpoe, independently implements the identical protocol we reverse-engineered (same numeric
callcmd-as-URL-path scheme, same login/cookie mechanism, same JSON envelope) - strong external validation of everything in this README. Its GitHub issues include a firmware dump the maintainer extracted from a sibling PoE model (GPS204V3, firmware6.0.24...), and that firmware's bundled web UI JS contains the actual, unobfuscated source for both commands:function set_PortWatchdog() { swcallDatas.data.callcmd = 130; var AI_Wdog = Number($("#AI_Wdog").val()); updateValueAndSession("AI_Wdog", "AI_Wdog", AI_Wdog, "AI_Wdog"); } function set_portAutoExtend() { swcallDatas.data.callcmd = 129; var AI_AnAuto = Number($("#AI_AnAuto").val()); updateValueAndSession("AI_AnAuto", "AI_AnAuto", AI_AnAuto, "AI_AnAuto"); }
So:
129=AI_AnAuto("Automatic Port Extension" - a global on/off toggle, calldata{"AI_AnAuto": 0|1}), and130=AI_Wdog("Port Watchdog" - calldata{"AI_Wdog": 0|1}). Both are simple global scalars, not per-port arrays - which is exactly why our earlier array-shaped guesses never landed.We retried both with this exact schema (value
0, for safety) and still gotparameter erroron our unit. "Port Watchdog" pings a connected device on PoE ports and power-cycles it if it stops responding; "Automatic Port Extension" is a PoE-range-extension mode. Both are meaningless without PoE output hardware, which this switch genuinely doesn't have (PD/input only, not PSE/output - seesys_info: model8GE). The most likely explanation: the commands are registered in the shared firmware's dispatch table (hence a real JSON error instead of the dropped-connection "unregistered" behavior we saw on the other unmapped IDs), but a hardware-capability check rejects them before even validating our field - i.e. these two are genuine, fully-identified dead code on this specific non-PoE model, not a gap in our schema guess.This closes out the
129/130investigation: their real names, purpose, and exact JSON schema are now known with certainty (not inferred), and we understand precisely why they don't work on this hardware.(A parallel search for a standalone firmware download for Goalake/Hasivo/MokerLink specifically came up empty, but did turn up real firmware files for other sibling OEM brands - Sodola, Horaco, GoodTop. Those turned out to be an older CGI-based firmware generation
web_maintenance.cgi, ClearSilver templates - with nocallcmdJSON scheme at all, so a different branch of this OEM family's history, not useful for this particular question.)
- The companion mobile app (not the web UI) has a documented,
non-hypothetical "reboot individual ports or schedule reboots"
feature, confirmed in both manuals - a POE Settings screen shows a
per-port table with
Script for repeating this yourself, one ID at a time:
SWITCH_PASSWORD='...' python3 probe_unmapped.py --host <ip> --cmd 129Session timeout. While probing, a script bug left a session open without logging out. Reasserting a fresh login then failed (silently dropped, same single-session symptom as before) for almost exactly 2 minutes before succeeding on its own — so that's the device's idle-session timeout when nobody explicitly logs out.
UDP scan (udpscan.py). Probed SNMP (161, with a real SNMPv1
GetRequest for sysDescr.0), TFTP (69), mDNS/SSDP/NTP/IKE, and the
discovery ports used by TP-Link's Easy Smart (29808/29809) and Netgear
ProSAFE (63321/63322) protocols, in case this OEM cloned one of those.
Every port came back closed (ICMP port-unreachable) — no SNMP agent,
no proprietary LAN discovery beacon, nothing on UDP at all. Combined with
the earlier TCP sweep, TCP port 80 genuinely appears to be the switch's
only exposed service.
Pre-authentication config reset via serial number. The login page's
"RESET" button (callcmd 132, calldata {Snvr: <last 6 chars of serial>}) requires no login at all — it's called straight from the
login screen. The result strings it uses (RestoreConfig_success/
RestoreConfig_fail, "Restore the configuration...") are distinct from
the authenticated factory-reset's strings (resetSuccess/resetFail,
"Reset to factory..."), confirming it's a separate, unauthenticated
config-restore path — not just an alias for the same authenticated
action. We did not trigger it (would actually reset the device), but the
security model is worth being clear-eyed about: anyone who can reach the
web UI on the LAN and knows/reads 6 characters of the device's serial
number can reset its configuration without ever knowing the admin
password. Brute-forcing that over the network isn't practical (a 6-char
alphanumeric suffix is on the order of 10^9 combinations, and this
device's httpd can barely sustain back-to-back requests without dropping
connections), so the realistic exposure is anyone with a photo of the
serial-number label — which, being a physical sticker, is a fairly
standard "recovery code on the bottom of the device" trust model, not a
remote vulnerability by itself. Worth knowing about if that label is ever
photographed for a return, a resale listing, or a support ticket.
Serial-console debug backdoor (unverified on this unit). Community
reverse-engineering of the same Realtek reference firmware family (see
the "Hasivo switches" OpenWrt forum thread) documents a hidden debug
prompt reachable via Ctrl+T/F4 at a serial-console login, password
switchrtk, extracted from a defdb.ko kernel module — it drops into a
menu with Diag/Engineering/Shell options that can pivot to a full Linux
shell. This switch only exposes port 80 over the network (no telnet, and
the UDP scan above found nothing either), so this would require physical
access to a serial header on the board. Not tested here — noted for
completeness if you ever open the case.
switch_client.py— stdlib-only Python client (no pip deps). Handles login/logout, the JSON envelope, cookie/session handling, and retries around the device's fragile connection handling.CALLCMD_CATALOGis the full command reference.enumerate.py— logs in and pulls every command confirmed safe to call with empty calldata (SAFE_GET_CMDS) into one JSON report. Never touches destructive/action commands.portscan.sh— dependency-free bash TCP port sweep (/dev/tcp, no nmap needed).udpscan.py— dependency-free UDP probe (SNMPGetRequest, common service ports, known vendor discovery-tool ports).probe_unmapped.py— safely test one not-yet-understood callcmd ID at a time, diffing port/link state before and after.
./portscan.sh 192.0.2.10
# password via env var so it doesn't end up in shell history
SWITCH_PASSWORD='...' python3 enumerate.py --host 192.0.2.10 --out report.jsonOr use the client directly:
from switch_client import SwitchClient
c = SwitchClient("192.0.2.10")
c.login("...")
print(c.get_detail()) # firmware version, serial, MAC, port counters
print(c.backup_config()) # full config export
c.logout()enumerate.py never calls: 102 (firmware upload), 104 (reboot),
105 (factory reset), 120 (change password), 132 (SN-based reset),
or 201 with calldata (config restore/overwrite) — all state-changing
or destructive. It also doesn't brute-force the unmapped ID gaps (see
above). These are all one-line client.call(...) invocations if you
deliberately want to exercise them, but they're not something to run
automatically.