mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
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:
1 parent
f2c5c6b31b
commit
a4a8f21d50
3 files changed
+194
-2
No files matched your search
@@ -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
|
||||
assets/
|
||||
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
|
||||
tools/ run, screenshot, inject keys, probe state
|
||||
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
|
||||
|
||||
`--host ::` makes it reachable off-box, which with no authentication means the
|
||||
port must be filtered by source address. `tools/dn42_firewall.sh` restricts it to
|
||||
The deployment here runs the server on loopback and puts nginx in front of it for
|
||||
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:
|
||||
|
||||
tools/dn42_firewall.sh apply 8080 # DN42 + loopback only
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user