Document and version-control the HTTPS front end

The UI is served at https://k6v6.mckero.dn42/ with nginx terminating TLS and the
server itself now bound to loopback, so it is not directly reachable.

No new address and no new certificate: 443 is shared with the other vhosts on
these DN42 addresses and separated by SNI, and the existing *.mckero.dn42 wildcard
already covers the name. Only DN42 addresses are bound, so the public 443
listeners on this host are untouched.

docs/reverse-proxy.md records the settings that are not optional, because each has
a failure mode that is easy to misread:
  proxy_buffering off  -- otherwise the frame stream arrives in bursts
  X-Forwarded-For      -- otherwise every log line is attributed to 127.0.0.1
  long read timeout    -- a paused guest emits nothing at all
  tcp_nodelay          -- Nagle would delay exactly the latency-critical requests

Two pitfalls hit while setting it up are written down. "http2 on;" needs nginx
1.25.1+ and this host runs 1.22.1, and because nginx -t was run before the symlink
existed it passed, then reload failed and left nginx stopped, briefly taking the
other sites down. Separately, a newly added listen address needs a reload to be
bound: after the failed reload, v6 requests failed with nothing in the error log
until a second reload created the socket.

deploy/nginx-k6v6.conf keeps a copy in the repo, since nothing here
version-controls /etc.

Verified: HTTP 200 on both families with the certificate validating (no -k), 7
frames in a 20 KB stream sample, log entries attributed to real client addresses.
This commit is contained in:
mckero committed 2026-08-28 11:39:17 +01:00
1 parent f2c5c6b31b
commit a4a8f21d50
3 files changed
+194 -2

No files matched your search

+11 -2
View File
@@ -67,6 +67,8 @@ keypresses silently stop working. Run the test after touching that code;
armv7m_systick.*.patched SysTick with the poll-boost property added armv7m_systick.*.patched SysTick with the poll-boost property added
assets/ assets/
calibration.bin 512-byte dump from a real radio calibration.bin 512-byte dump from a real radio
deploy/ nginx vhost for the HTTPS front end
docs/reverse-proxy.md how https://k6v6.mckero.dn42/ is served
docs/screenshots/ LCD captures used in this README docs/screenshots/ LCD captures used in this README
tools/ run, screenshot, inject keys, probe state tools/ run, screenshot, inject keys, probe state
keypad_test.py keypad regression test, boots its own instance keypad_test.py keypad regression test, boots its own instance
@@ -187,8 +189,15 @@ Two constraints worth knowing before you use it:
### Reaching it from elsewhere ### Reaching it from elsewhere
`--host ::` makes it reachable off-box, which with no authentication means the The deployment here runs the server on loopback and puts nginx in front of it for
port must be filtered by source address. `tools/dn42_firewall.sh` restricts it to TLS, at `https://k6v6.mckero.dn42/`. See
[docs/reverse-proxy.md](docs/reverse-proxy.md) for the vhost, including the two
settings that matter for this app: `proxy_buffering off` (or the frame stream
arrives in bursts) and `X-Forwarded-For` (or every log line is attributed to
127.0.0.1).
Binding directly with `--host ::` also works, but with no authentication the port
then has to be filtered by source address. `tools/dn42_firewall.sh` restricts it to
DN42: DN42:
tools/dn42_firewall.sh apply 8080 # DN42 + loopback only tools/dn42_firewall.sh apply 8080 # DN42 + loopback only
+49
View File
@@ -0,0 +1,49 @@
# Reverse proxy for the UV-K5 emulator web UI (tools/webui.py).
#
# Shares 443 on the existing DN42 addresses via SNI, so no new IP is needed and it
# coexists with dns.mckero.dn42 on the same socket.
#
# Certificate is the existing *.mckero.dn42 wildcard, which already covers this
# name -- no new issuance required.
server {
listen 172.21.91.140:80;
listen [fd3c:3f9b:6424:2::5]:80;
server_name k6v6.mckero.dn42;
return 301 https://$host$request_uri;
}
server {
listen 172.21.91.140:443 ssl;
listen [fd3c:3f9b:6424:2::5]:443 ssl;
server_name k6v6.mckero.dn42;
ssl_certificate /etc/letsencrypt/live/mckero-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mckero-wildcard/privkey.pem;
# The emulator UI has no authentication of its own: anyone who reaches it can
# drive the radio. Reachability is limited by the DN42-only bind plus the
# iptables rules in uvk5-port/sim/tools/dn42_firewall.sh.
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# /stream is an endless multipart/x-mixed-replace response. Buffering it
# would hold frames back and the picture would arrive in bursts or stall
# outright, so buffering is off and the read timeout is long enough that an
# idle screen does not look like a dropped connection.
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Latency is the whole point of this UI; Nagle would add delay to the
# small, frequent keypress responses.
tcp_nodelay on;
}
}
+134
View File
@@ -0,0 +1,134 @@
# Serving the web UI over HTTPS
How `https://k6v6.mckero.dn42/` is set up on this host. The emulator UI itself
speaks plain HTTP on loopback; nginx terminates TLS and proxies to it.
## Why a proxy at all
`tools/webui.py` has no TLS and no authentication. Running it on loopback and
letting nginx face the network means the existing certificate and the existing
443 listener are reused, and the UI is not directly reachable at all.
## The vhost
Lives in `/etc/nginx/sites-available/k6v6`, symlinked into `sites-enabled/`. A copy
is kept in this repo at [`deploy/nginx-k6v6.conf`](../deploy/nginx-k6v6.conf), since
nothing else here version-controls `/etc`.
server {
listen 172.21.91.140:80;
listen [fd3c:3f9b:6424:2::5]:80;
server_name k6v6.mckero.dn42;
return 301 https://$host$request_uri;
}
server {
listen 172.21.91.140:443 ssl;
listen [fd3c:3f9b:6424:2::5]:443 ssl;
server_name k6v6.mckero.dn42;
ssl_certificate /etc/letsencrypt/live/mckero-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mckero-wildcard/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
tcp_nodelay on;
}
}
Start the server bound to loopback, since nginx is the only thing that needs to
reach it:
python3 tools/webui.py --frame-addr 0x200013DC --status-addr 0x2000175C \
--host 127.0.0.1
## The settings that are not optional
**`proxy_buffering off`.** `/stream` is an endless
`multipart/x-mixed-replace` response. With buffering on, nginx holds frames back
and the picture arrives in bursts or appears frozen. This is the single setting
most likely to be dropped when someone rewrites the vhost.
**`proxy_read_timeout` well above the idle frame interval.** The stream sends a
keepalive frame every `IDLE_FRAME_INTERVAL_S` (2 s), so the default 60 s would be
survivable -- but a paused guest produces nothing at all, and the default would
then drop the connection.
**`X-Forwarded-For`.** The log pane attributes each line to a client IP. Behind a
proxy `REMOTE_ADDR` is always 127.0.0.1, so without this header every entry reads
as if it came from the server itself. `webui.py` trusts only the first hop.
**`tcp_nodelay on`.** Keypress responses are small and frequent; Nagle would add
delay to exactly the requests where latency is the point.
## No new address, no new certificate
Both are deliberate:
- 443 is shared with the other vhosts on these addresses and separated by SNI, so
no additional IP is consumed.
- The existing `*.mckero.dn42` wildcard already covers this name, so nothing had
to be issued. Check it with:
openssl x509 -in /etc/letsencrypt/live/mckero-wildcard/fullchain.pem \
-noout -text | grep -A1 'Subject Alternative Name'
Only DN42 addresses are bound. The public addresses on this host also listen on
443, and they are untouched -- the exposure is decided by the `listen` address, so
the UI is not reachable from the internet.
## DNS
Records to point at it:
k6v6.mckero.dn42. A 172.21.91.140
k6v6.mckero.dn42. AAAA fd3c:3f9b:6424:2::5
## Pitfalls hit while setting this up
**`http2 on;` needs nginx 1.25.1+.** This host runs 1.22.1, where that directive
does not exist. Worse, `nginx -t` was run *before* the symlink was created, so it
passed, and the subsequent `systemctl reload` failed and left nginx **stopped** --
taking the other sites down until the line was removed. Create the symlink first,
then `nginx -t`, then reload. For HTTP/2 on 1.22 the syntax is
`listen ... ssl http2;`.
**A new `listen` address needs a reload to take effect.** After the failed reload
above, `systemctl start` brought nginx back but it had not bound
`[fd3c:3f9b:6424:2::5]:443`; v6 requests failed with no error in the log. A second
`systemctl reload nginx` created the socket. If a newly added address refuses
connections, check `ss -ltnp | grep 443` before looking anywhere else.
## Verifying
# both families, and check the certificate rather than skipping it with -k
curl -s -o /dev/null -w '%{http_code}\n' \
--resolve 'k6v6.mckero.dn42:443:172.21.91.140' \
https://k6v6.mckero.dn42/
curl -s -g -o /dev/null -w '%{http_code}\n' \
--resolve 'k6v6.mckero.dn42:443:[fd3c:3f9b:6424:2::5]' \
https://k6v6.mckero.dn42/
# the stream must deliver frames continuously, not in one burst at the end
curl -sk --resolve 'k6v6.mckero.dn42:443:172.21.91.140' \
https://k6v6.mckero.dn42/stream | head -c 20000 | grep -c PNG
# log attribution: entries should carry the real client address, not 127.0.0.1
curl -sk --resolve 'k6v6.mckero.dn42:443:172.21.91.140' \
https://k6v6.mckero.dn42/api/logs
Measured after setup: HTTP 200 on both families with the certificate validating,
first stream frame in 0.01 s, 7 frames in 12 s on an idle screen, and log entries
attributed to `172.21.91.140` and `fd3c:3f9b:6424:2::5` while firmware serial and
QEMU lines correctly show no client.