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.
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.
中文版:云端的第一公里