This project runs Ruckus ZoneDirector 1200 software in a local QEMU virtual machine so it can manage real Ruckus access points without a physical ZD1200 appliance. It is intended for owners of compatible APs who have legitimately obtained a ZoneDirector software download and want a repeatable lab, replacement-controller, compatibility, or recovery setup.
This is an experimental community project, not a Ruckus product. It does not include Ruckus software, firmware, keys, or licenses; you must provide those yourself and remain responsible for complying with the Ruckus terms.
- A Linux x86-64 Docker host.
- A dedicated USB Ethernet adapter.
- 10.5.1.0.282, the recommended controller release. Its vendor target list includes C110, H320, H510, R310, R320, R500, R510, R550, R600, R610, R650, R710, R720, R730, R750, R850, R350, H550, H350, E510, T300, T300e, T301n, T301s, T310c, T310d, T310n, T310s, T610, T610s, T710, T710s, T750, T750SE, T350c, T350d, and T350se. Check the Ruckus release documentation for your AP's exact support status.
- An older ZD release if you need legacy models such as the R700 or H500; see Choose a ZoneDirector release.
- Linux x86-64 with Docker Engine, Docker Compose, and KVM available at
/dev/kvm. - One Ethernet adapter that can be dedicated to the controller/AP network. Do not use the adapter that carries the host's normal Internet/default route.
- A management computer on the same isolated switch, plus the APs to manage.
Linux ARM64 is experimental. macOS and Windows Docker Desktop are not supported for physical AP management because this project has no validated equivalent to the required Linux TAP/bridge networking.
ZoneDirector software is available for download on the Ruckus website, and requires account registration.
Clone this repository, create a vendor/ directory, and copy your original
ZD1200 download into it. Do not commit that directory.
git clone https://github.com/dbro/zd1200.git
cd zd1200
git switch public-release-candidate
mkdir vendor
# Copy your Ruckus ZD1200 software download into ./vendor/
cp .env.example .envNow edit .env. The only required change is ZD_VENDOR_ARCHIVE: set it to
the exact filename you placed in vendor/. Other options include enabling
a root shell and other conveniences.
# Example for the recommended release. Use the actual filename you downloaded.
ZD_VENDOR_ARCHIVE=zd1200_10.5.1.0.282.ap_10.5.1.0.282.img
# Keep this as tap-zd unless you deliberately changed the bridge-helper config.
ZD_TAP_IF=tap-zd
# Optional: leave both commented out to generate a persistent local identity.
# If you set one, set both before the first start.
# ZD_SERIAL=123456000789
# ZD_MAC1=02:52:54:12:00:01Install and configure the bridge helper. Substitute the Ethernet interface name and MAC address of the adapter that is connected to the isolated switch.
sudo install -m 0755 host/zd1200-bridge /usr/local/sbin/zd1200-bridge
sudo install -m 0644 host/zd1200-bridge.service /etc/systemd/system/
sudo install -m 0644 host/zd1200-bridge-watch.service /etc/systemd/system/
sudo install -m 0600 host/zd1200-bridge.env.example /etc/default/zd1200-bridge
sudoedit /etc/default/zd1200-bridgeSet these values in /etc/default/zd1200-bridge:
ZD_NETWORK_PROFILE=dedicated
ZD_USB_IF=enx0123456789ab
ZD_USB_MAC=01:23:45:67:89:ab
ZD_TAP_IF=tap-zdThe helper refuses an adapter or bridge with the host's default route. Check the proposed setup, then enable it:
sudo systemctl daemon-reload
sudo /usr/local/sbin/zd1200-bridge check
sudo systemctl enable --now zd1200-bridge.service
sudo systemctl enable --now zd1200-bridge-watch.servicedocker compose up -d --build
docker compose logs -fOn its first build, Docker locally decrypts and validates your download,
prepares the controller runtime, and starts QEMU. It also builds the patched
unsigned R600 AP image only for 10.5.1. Your original download stays read-only
in vendor/ and is never copied into a Docker image layer.
Without DHCP on the isolated network, browse to
https://192.168.0.2/ from the management computer.
First give that computer a temporary static address such as 192.168.0.3/24
on its Ethernet adapter; no gateway is needed for this step.
Complete the ZoneDirector wizard and assign the controller a permanent static
address suitable for the isolated network. Then restart the controller once:
docker compose restart zd1200Log in at https://<your-controller-address>/admin10/login.jsp and adopt an
AP. The full physical-AP acceptance procedure is in
VALIDATION.md.
Only the exact builds below are recognized. Set ZD_VENDOR_ARCHIVE to the
filename of the matching download; the preparation step identifies the build
by SHA-256, not by filename alone.
| Choose this build | Choose it when | Important note |
|---|---|---|
| 10.5.1.0.282 | You want the recommended, newest supported controller path. | Automatically creates the repaired R600 mesh image. R600 is validated; related shared-payload models remain experimental. |
| 10.3.1.0.42 | You need the final ZD release family that supports the R700. | |
| 10.2.1.0.232 | You need its historical/vendor unsigned-image compatibility behavior. | This is distinct from the project-generated unsigned R600 image used only by the 10.5.1 mesh repair. |
| 10.1.2.0.318 | You need H500 support. |
The three older builds are compatibility paths, not upgrades over 10.5.1. Their AP firmware is delivered as signed FSI images; they do not need or receive the 10.5.1 mesh repair.
ZoneDirector version 10.5.1.0.276 introduced a mesh receive-path bug for some AP models. A wired "Root" AP (RAP) can appear normal while a wireless "Mesh" AP (MAP) shows as connected but does not pass ordinary Layer-2 traffic between the wired and mesh sides. Typical signs are unresolved ARP entries and failed bidirectional management pings; a static management address does not prevent the fault.
This bug exists in the software version recommended here (10.5.1.0.282) that is downloadable from the Ruckus website. This project applies a fix for this bug during the installation process.
R600 is the validated repaired model. R500, R310, T300, T300e, T301n, and T301s are experimental targets only when the selected ZD payload resolves to the exact same shared firmware image. Validate a specific model before relying on the repair in a production-like deployment.
IMPORTANT! You must manually prepare these APs to receive UNSIGNED firmware when running ZD version 10.5.1.0.282 !
An AP currently running a fully signed (FSI) image will reject the patched
unsigned (UI) image used by the 10.5.1 path. Before adopting one of these
APs to the patched 10.5.1 controller, install a compatible intermediate-signed
(ISI) image for that exact AP model through its standalone upgrade
interface. For example, an R600 can use the standalone/ISI release
110.0.0.0.675; confirm the exact filename and model compatibility from the
legitimate Ruckus download.
An AP already running an ISI or UI image does not need this preparation.
Never install firmware for a different model.
Steps to flash ISI firmware on an AP:
- download the ISI image from Ruckus website and save it on your laptop
- isolate the AP from the network, and connect it to your laptop with a USB ethernet adapter set up with a static IP address 192.168.0.xxx
- factory reset the AP and let it reboot
- visit the AP's admin webpage at 192.168.0.1 and login with the default username "super" and password "sp-admin"
- update the firmware using the local method and select the ISI image saved on your laptop
- after the AP restarts, visit 192.168.0.1 and check that it runs the ISI version. Assign its IP as needed (DHCP or static) to connect with the ZoneDirector.
- Let the ZD discover, adopt, accept, and upgrade the firmware to the unsigned+patched version.
# Start or update after changing Compose/project code.
docker compose up -d --build
# Inspect startup and guest messages.
docker compose logs -f
# Stop the controller without deleting its configuration.
docker compose down
# Check whether Docker considers the guest ready.
docker compose psThe named state volume retains the controller configuration, AP database, generated serial number, and generated MAC address. Back it up before making large configuration changes. Container stop can take up to two minutes while the guest performs its normal repository and filesystem flush; do not force QEMU or Docker to exit during that interval.
To factory-reset the virtual controller, stop the stack and remove only its state volume:
docker compose down
state_volume="$(sed -n 's/^ZD_STATE_VOLUME=//p' .env | head -n 1)"
state_volume="${state_volume:-zd1200-state}"
docker volume rm "$state_volume"
docker compose up -d --buildTo deliberately use a different controller download, stop the stack and
remove only the runtime volume named by ZD_RUNTIME_VOLUME; leave the state
volume intact unless you also want a factory reset.
docker compose down
runtime_volume="$(sed -n 's/^ZD_RUNTIME_VOLUME=//p' .env | head -n 1)"
runtime_volume="${runtime_volume:-zd1200-runtime}"
docker volume rm "$runtime_volume"
docker compose up -d --build| Setting | Purpose |
|---|---|
ZD_SERIAL and ZD_MAC1 |
Set a chosen, stable controller identity. Set both before the first start, or leave both unset for a generated persistent identity. |
ZD_WEB_PROBE |
Controls launcher readiness only. Leave it at auto: it disables in-container HTTP probing for the normal TAP or macvlan network and enables a local probe for user-mode networking. on is not supported with TAP or macvlan. |
ZD_ENABLE_ECDSA_SSH=1 |
Adds an ECDSA host key to the ordinary ZoneDirector administrative SSH service while retaining RSA. |
ZD_ENABLE_ROOT_CLI=1 |
Enables a local root shell through the Ruckus CLI hidden command !v54! and through the series of commands enable; debug; script; exec .root.sh. It does not add a network listener. |
ZD_ROOT_SSH_PUBLIC_KEY |
Enables public-key-only root SSH on TCP 2222 for 10.5.1.0.282. RSA and ECDSA keys are accepted; Ed25519 is not. |
ZD_SUPPORT_ENTITLEMENT_END |
Creates a finite support-entitlement record ending on the supplied YYYY-MM-DD date. |
ZD_PING_INTERVAL_SECONDS |
Legacy-compatible initial 30–3600 second collection interval. The page stores one shared interval for pings and snapshots; monitoring remains disabled until it is enabled there. |
ZD_VIRTUAL_BUILD_ID |
Optional override for the seven-character revision shown after the ZoneDirector version. Git checkouts detect this automatically; Portainer deployments may leave it unset (they show virtual 0000000). |
The regular admin console appends virtual <revision> to the stock
ZoneDirector version. The revision is resolved from the checked-out Git commit
when the Compose runtime is prepared, so it identifies the source used to
build that running virtual controller. If the project was copied without its
.git metadata, set ZD_VIRTUAL_BUILD_ID to the source commit and
ZD_GIT_DIR=/dev/null before running Compose.
After the setup wizard and first restart, Network Monitor appears as the last item under Troubleshooting. It records individual ICMP observations and retains raw ZoneDirector AP, client, and mesh snapshots for comparison.
Ping polling and configuration snapshots are disabled together on a fresh controller. Enable monitoring and choose the shared 30–3600 second collection interval on the Network Monitor page. The settings are stored through ZoneDirector's normal authenticated preference mechanism. Snapshot collection uses the controller's root-local vendor statistics socket, so no additional ZoneDirector role, user, or password is required.
Each ping round parses the current client XML once, refreshes discovered clients in one SQLite transaction, and sends raw ICMP requests in bounded groups of 512. This avoids both a process per ping and a one-second serial delay per failed target. Ten one-second timeout windows cover 5,000 targets, and the scheduler accounts for collection time so a 30-second setting remains a start-to-start interval. The collector obtains all current clients in one stock bulk request and all AP LEVEL=2 telemetry in a second bulk request. Each ping observation retains the client's contemporaneous SNR, received signal level, and noise floor. Per-radio AP samples retain channel, client count, noise/SNR, and the four airtime values (total, busy, RX, and TX) in the vendor's original tenths of a percentage point. Mesh uplink/downlink SNR is retained separately. These two live XML responses are replaced atomically and are not historical data; only their compact extracted fields are retained in SQLite.
Event ingestion has been investigated, but it is deliberately not scheduled or shown in this prototype. AP, client, and mesh configuration snapshots are independent of the compact live telemetry. Actual PSK material is removed before a snapshot reaches retained storage; device, serial, and DPSK identifiers are preserved for diagnosis.
Snapshot availability is published separately as a small manifest and one timestamp-only index per UTC day. The page loads the current and previous day initially and fetches older daily indexes only when a longer chart range is selected. Active-day XML captures are individually compressed. After UTC rollover they are reframed and compressed together into one immutable daily gzip bundle, allowing repeated XML structure to compress across captures. The larger bodies are fetched only when selected history moments need configuration evidence.
Browser-facing ping history is published as gzip-compressed UTC daily chunks. Completed days are immutable and cacheable; only the current day is regenerated after a ping round. Each observation uses one byte and daily files share one timestamp axis across their MAC-sorted targets. The server publishes identity metadata but no derived ping counts, averages, maxima, loss rates, or timeline rollups. A browser worker derives every displayed metric directly from the daily raw chunks and discards each decoded day after scanning it. Version 2 daily files add raw client SNR, association state, per-band AP airtime, and mesh-uplink SNR alongside ping outcomes. When upgrading an existing installation, retained full-precision SQLite history is backfilled into immutable daily chunks once, so switching formats does not hide earlier observations. The rows-by-time history supports instant name/MAC and network filtering, sortable summaries, 10/25/50-row pagination, AP topology context, and selectable A/B moments. It displays mean and maximum latency, loss, SNR, association state, and selected-band AP airtime from raw observations. The page can package the selected ping evidence and scoped complete XML snapshots into a downloadable manual-analysis prompt; nothing is sent off the controller by that action.
Potential follow-up work for deployments that retain millions of observations
includes an operator-configurable database size/retention limit. The storage
format, browser processing architecture, UX work, measured baseline, and
compatibility tests are tracked in
PING_MONITOR_SCALABILITY_PLAN.md.
The dedicated-adapter profile above is recommended because it keeps the
Docker host unnumbered on the controller/AP network. The bridge helper can
also attach tap-zd to an existing Linux bridge for an intentionally shared
management LAN:
ZD_NETWORK_PROFILE=existing-bridge
ZD_BRIDGE_IF=br0
ZD_TAP_IF=tap-zdThis advanced profile never changes the existing bridge's members, addresses,
routes, or default route. Do not enable zd1200-bridge-watch.service for it.
Do not put an unconfigured factory controller on a production LAN.
Use macvlan when the Docker host has an existing wired management VLAN and you cannot dedicate a physical adapter. The QEMU guest receives Layer-2 access by bridging the container's macvlan interface to its private TAP; the guest, not the Docker container, owns the management address.
Add these settings to .env, using the host interface and subnet of the VLAN
where the controller is intentionally allowed to appear:
ZD_MACVLAN_PARENT=eno1
ZD_MACVLAN_SUBNET=192.168.222.0/24
ZD_MACVLAN_GATEWAY=192.168.222.1Then start the alternate Compose profile:
docker compose -f docker-compose.macvlan.yml up -d --buildDo not run the dedicated-adapter bridge helper for this profile. Docker hosts normally cannot communicate directly with their own macvlan containers, so use another management station on the selected VLAN for the setup wizard and ZD administration. This profile has not yet completed physical AP validation; use it only on an intentionally isolated or managed VLAN.
To remove the dedicated network path, stop Compose and both helper services.
For the existing-bridge profile, stop Compose and only
zd1200-bridge.service; the pre-existing bridge is left untouched. Detailed
recovery and AP-adoption instructions are in VALIDATION.md.
Missing image/bootinitramfs.gz— the preparation service did not complete. Rundocker compose logs zd1200-prepareand correct the archive filename or unsupported-build error before starting again.- The setup wizard does not appear at
192.168.0.2— make sure the management computer is connected to the isolated switch and has an address in the same temporary subnet. Checkdocker compose logs -fand the bridge helper withsudo /usr/local/sbin/zd1200-bridge check. - An AP loops during a 10.5.1 update — confirm that it was first moved from FSI to a compatible ISI image. See “Fixing mesh operation for R600 and experimental shared-payload APs” above.
- The dedicated adapter does not recover after reconnecting — verify its
MAC still matches
ZD_USB_MAC, then reviewsystemctl status zd1200-bridge-watch.service.
The Docker workflow is the normal installation method. The repository also contains standalone local tools for inspecting or building a ZIP from a recognized vendor download:
python3 verify_release_archive.py /path/to/decrypted-zd1200.img.tgz
python3 build_zd1200_bundle.py /path/to/zd1200.img /path/to/bundle.zipbuild_zd1200_bundle.py accepts either the original encrypted download or a
recognized decrypted archive. The generated ZIP includes transformed runtime
files but not the original encrypted input. Its overall hash changes between
builds because it receives a fresh bootstrap TLS identity.
The artifact-specific patch definitions are in binary_patch_catalog.json and
the exact supported-download manifests are in release_manifest.json. The
full validation matrix, known limitations, test evidence, and roadmap are in
VALIDATION.md, PROVENANCE.md, and
ROADMAP.md. Run python3 check_repository_hygiene.py before
contributing or publishing changes.
The NAR5520 LED and watchdog probes can call the global kernel halt path while
probing hardware that QEMU does not provide. The narrowly documented
kernel_halt return keeps those boot-time probes from stopping the VM; it does
not implement restart. Normal ZD admin restart still runs the stock flush and
PID-1 shutdown sequence, then the separate machine_restart_qemu patch asks
QEMU's emulated i8042 controller to reset the guest. Exercise both pieces with:
python3 tests/qemu_restart_smoke.pyThis is not a hardened appliance. Keep the controller, its web UI, SSH, FTP,
and the Docker host API off untrusted networks. Use a dedicated management VLAN
and firewall rules. Do not commit .env, vendor/, generated runtime files,
state volumes, captures, passwords, or private keys.
The repository's MIT License applies only to this project's glue code and documentation. It grants no rights to Ruckus material. The local TAC decryption implementation is derived from the permissively licensed aioruckus project; see THIRD_PARTY_NOTICES.md.
Do not use the ZoneDirector web-upgrade workflow inside this VM. QEMU boots an external kernel and initramfs, so an in-guest upgrade creates a mixed version unless this project is updated and rebuilt for that exact release.