TechnicalCloud Hub2026-10-02

The Cloud's First Mile
— Putting a Radio on the Internet, and Why It Once Showed a Black Screen

A radio amateur rarely has a public IP: carrier NAT, double NAT, no port forwarding, no UPnP. Yet a radio that should be usable remotely needs an entry on the internet. This is how we gave it one without asking anyone to touch their network - and why, on the first day, installing the app produced a black screen.

1. The constraints

Constraint Consequence
No public IP, no port forwarding, no UPnP on the instance side The entry cannot live at the client - the tunnel must be outbound
Some networks allow only 80/443, others something else Exactly one control port, and it must be configurable
The user is an operator, not a sysadmin Connecting has to be a few clicks inside the app
One device belongs to one callsign The entry is the callsign: https://bg9aaa.mrrc.vlsc.net/

2. Four invariants

1. The instance dials out; the hub never dials in

An frpc on the instance connects to tunnel.mrrc.vlsc.net:8989 - the only port that stands on its own - and maps the radio's local 8888 to a private loopback port on the hub (127.0.0.1:18803). The hub's nginx proxies bg9aaa.mrrc.vlsc.net to that port. The client needs zero inbound access, just one outbound TCP.

2. The hub verifies each instance against its own certificate

The instance's certificate is self-signed at first run, with the entry name as its CN/SAN. The hub's proxy_ssl_trusted_certificate points at a bundle generated from the registry: system roots plus each instance's public key. Renewals need no cross-machine synchronisation, and only registered instances are trusted.

The inverse also holds: a re-signed instance certificate with a stale trust bundle means a failed upstream check and a 502 at the entry. We hit it twice; regenerating the bundle is now a step in the deployment script.

3. Routes are generated, not hand-written

The registry instances.tsv holds label loopback-port entry-name per line; one command generates nginx's map and the wildcard vhost. Adding an instance is one line and a regeneration - never an edit to the site's configuration.

4. The portal performs no root actions

The self-service portal runs on loopback as an unprivileged service account. It can record an application and sign an instance certificate, but it cannot reload nginx or change a firewall. The two steps that need root are printed as commands for the operator. The privilege boundary is explicit: a compromised portal gains no machine.

3. Why installing produced a black screen

Default What happened on a customer's machine
Certificate path inside the packaged directory C:\Program Files is read-only for a normal user ⇒ no certificate ⇒ the server quietly fell back to plain HTTP while the launcher opened an https:// URL ⇒ protocol error ⇒ a black screen
Log directory inside the packaged directory [WinError 5] ⇒ no logs at all
Serial port defaulting to macOS's /dev/cu.SLAB_USBtoUART On Windows the radio was on COM5; the app looked for a port that cannot exist
Login password defaulting to the value in the source Anyone on the LAN could log in until it was changed

Three structural fixes, not three resolutions to be careful:

1. Everything written at runtime defaults into the user's data directory
2. A missing certificate is signed on the spot - never a silent fallback to HTTP
3. A gate test: a writable default inside the program directory fails the suite

   def test_runtime_paths_are_outside_the_program_directory(self): ...

The third is the most valuable thing to come out of the day: it turned "be careful" into "red next time". It also caught a fifth case nobody had noticed (the memory-channels file).

4. Two timing problems only real hardware shows

1. A new certificate, an old process

The TLS context is built once, at start-up. Connecting enrols a new certificate and writes it to disk; the running process keeps serving the previous one, the hub verifies against the new one, and the entry answers 502. The nastiest ordering is restart first, enrol second. The criterion is now a fact:

certificate mtime > process start time  =>  a restart is required

2. The tunnel started lazily

The tunnel used to start only when the settings dialog was opened (a call to /api/cloud/state), so after a reboot the entry answered 502 until somebody clicked. Found during the end-to-end test; the app now starts the tunnel at boot (a failure is logged and does not stop the server).

5. Four rules the field wrote for us

1. Do not prove a build with exit codes. Exit 0, Successful compile and a freshly written version.txt can all be true of an old package. The reliable combination is version, today's timestamp, and a hash different from the previous release - then install and actually run it.

2. Never rebuild under the same version number. The upgrade channel compares version strings: anyone who installed that number will never see the new package.

3. One driver per release train. Two writers in parallel overwrote a manifest into "new label, old numbers" - an updater comparing hashes that do not match the file.

4. Every failure on real hardware becomes a test or an invariant that day. Otherwise it returns under another name.

6. For users: three steps

1. Install MRRC Modern (Windows installer / macOS DMG)
2. Drawer menu (top bar ☰) -> Cloud Hub -> enter your callsign -> Apply
3. The operator approves it in the portal (or hands you a one-time enrolment
   secret that the same dialog accepts)

That is the whole list: once approval lands the app connects by itself - the server
checks every 30 seconds (v1.25.0), so there is nothing left to click.

Your entry afterwards:  https://<callsign>.mrrc.vlsc.net/
   - the callsign itself: no port, no path, no product suffix

When something is wrong, the app's 🐞 button uploads a diagnosis bundle; if a machine's environment has accumulated leftovers, the site offers a a cleanup script that backs up your configuration and certificates first and then removes every generation of it (what it does not back up: guide §6.2).

If the launcher will not start - a black or flashing window - the server runs without it: MRRC Modern Server in the Start Menu, then https://127.0.0.1:8888/ in a browser (accept the self-signed certificate warning), with the password printed in the window. See the install guide §6.1 for the details.

Written 2026-10-02, the day the MRRC Cloud Hub went live. Every detail comes from that day's logs, commits and artifact checks. Companion essay: Highbrow and Lowbrow — On Muscle and Mind.
中文版:云端的第一公里