feat(secubox-lyrion): LAN UI + LMS prefs + slimproto DNAT (closes #248) (#251)

* feat(secubox-lyrion): expose LMS web UI on LAN IP + lyrion.maegia.tv (closes #248)

Operator asked "make lyrion available with local ip ui". Ships
nginx/lyrion-vhost.conf to /etc/nginx/sites-available/lyrion.conf with
two server blocks:

1. **LAN direct** — `listen 0.0.0.0:9000`, accepts the LAN IP / .local /
   localhost / catch-all. No TLS, no WAF. Operator hits
   `http://192.168.1.200:9000/` and lands on LMS at root.

2. **Public chain** — `listen 0.0.0.0:9080`, server_name lyrion.maegia.tv.
   Ships with the mandatory `^~ /.well-known/acme-challenge/` block per
   the #246 lesson; operator wires HAProxy ACL + cert + mitmproxy route
   separately when ready.

Why two blocks instead of one: LMS uses absolute asset paths (/skin/EN/...,
/html/..., …) so it cannot live under a subpath without rewriting every
asset reference. Each block proxies to LXC `10.100.0.100:9000` at root.

Operator enables with:
    ln -s ../sites-available/lyrion.conf /etc/nginx/sites-enabled/
    systemctl reload nginx

Board-validated 2026-05-20:
    $ curl -I http://192.168.1.200:9000/
    HTTP/1.1 302 Found
    Server: nginx/1.22.1
    Location: /settings/server/wizard.html
    Set-Cookie: Squeezebox-albumView=; path=/

Closes #248

* fix(secubox-lyrion): disable LMS protectSettings on install (ref #248)

Board-validated v1.0.2 LAN UI returned 403 on /settings/server/wizard.html
because LMS's `protectSettings: 1` (Slim/Web/HTTP.pm:1040) rejects any
peer that is the LXC bridge gateway OR on a "different network" — both
true for traffic arriving through nginx reverse-proxy.

install-lxc.sh now writes two LMS prefs after the .deb install:
  protectSettings: 0
  allowedHosts: 127.0.0.1,10.100.0.*,192.168.*

Access control is enforced upstream (HAProxy + mitmproxy + Authelia
SSO), so disabling LMS's local guard is safe. Idempotent — re-running
`lyrionctl install` just rewrites both prefs.

Diagnostic that surfaced the bug:
  /var/log/squeezeboxserver/server.log →
    Access to settings pages is restricted to the local network or
    localhost: 10.100.0.1 -> 10.100.0.100 (settings/server/wizard.html)

* feat(secubox-lyrion): slimproto DNAT for LAN/WiFi players (ref #248)

LMS slimproto (TCP+UDP :3483) lives inside the LXC at 10.100.0.100, on
br-lxc which is not routed from the LAN. WiFi players (e.g. on
192.168.8.x) reaching the board via the default gateway can talk to
the web UI on :9000 (nginx already binds 0.0.0.0) but not to slimproto,
so they can't actually play.

install-lxc.sh now drops a persistent rule file at
/etc/nftables.d/secubox-lyrion-dnat.nft loaded by nftables.service:

    table inet secubox-lyrion {
        chain prerouting {
            type nat hook prerouting priority dstnat; policy accept;
            iifname "lan0" tcp dport 3483 dnat ip to 10.100.0.100:3483
            iifname "lan0" udp dport 3483 dnat ip to 10.100.0.100:3483
        }
    }

LAN interface defaults to `lan0`; override via SECUBOX_LAN_IFACE.

Idempotent: re-running `lyrionctl install` deletes the live table and
re-applies from the file (verified by removing both on the board and
running the install verb).

Board-validated 2026-05-20:
    $ nc -zv 192.168.1.200 3483
    Connection to 192.168.1.200 3483 port [tcp/*] succeeded!

Note: UDP player auto-discovery broadcasts do NOT cross subnets
without router-side IP-helper. Players on a different VLAN/SSID
must be told the board's LAN IP manually as the server.

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
This commit is contained in:
CyberMind 2026-05-20 12:53:50 +02:00 committed by GitHub
parent 322809d2a6
commit 6ad9d7262a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 210 additions and 4 deletions

View File

@ -1,3 +1,50 @@
secubox-lyrion (1.0.3-1~bookworm1) bookworm; urgency=medium
* install-lxc.sh: new ensure_slimproto_dnat() step writes a persistent
nftables file /etc/nftables.d/secubox-lyrion-dnat.nft with a
PREROUTING DNAT rule routing host LAN traffic on TCP+UDP :3483 to
the Lyrion LXC. Lets WiFi/LAN players (Squeezelite, iPeng, …) reach
the LMS server without bridging the LXC network into the LAN.
Default LAN interface is `lan0` — override via SECUBOX_LAN_IFACE.
Idempotent: re-running `lyrionctl install` deletes the live table
and re-applies from the file.
* Note: UDP player auto-discovery broadcasts do NOT cross subnets
without router-side IP-helper. Players on a different VLAN/SSID
must be configured manually with the board's LAN IP as the server.
-- Gerald KERMA <devel@cybermind.fr> Wed, 20 May 2026 21:00:00 +0200
secubox-lyrion (1.0.2-1~bookworm1) bookworm; urgency=medium
* Ship nginx/lyrion-vhost.conf to /etc/nginx/sites-available/lyrion.conf
so the operator can expose the LMS web UI on the LAN IP directly.
Two server blocks:
1. LAN direct — listen :9000, accepts 192.168.1.200 / lyrion.local /
localhost / _ ; proxies to LXC root. No TLS, no WAF.
2. Public chain — listen :9080, server_name lyrion.maegia.tv, with
the mandatory ^~ /.well-known/acme-challenge/ block (cf. #246).
Operator enables with:
ln -s ../sites-available/lyrion.conf /etc/nginx/sites-enabled/
systemctl reload nginx
After the symlink, http://192.168.1.200:9000/ serves the LMS web UI
directly. Closes #248.
* Note: subpath proxy (/lyrion/ on the canonical hub) is unsuitable
for LMS because LMS uses absolute asset paths (/skin/EN/..., /html/..., …).
LMS must live at vhost root.
* install-lxc.sh: new configure_lms_for_proxy() step writes two LMS prefs
after the .deb install so the LAN UI works on first hit:
- protectSettings: 0 (LMS otherwise 403s every /settings/* page
when the peer is the bridge gateway / a "different network" —
both true through nginx reverse-proxy; access control is enforced
upstream by HAProxy + mitmproxy + Authelia).
- allowedHosts: 127.0.0.1,10.100.0.*,192.168.*
Was discovered post-board-validation when the wizard.html redirect
landed on a 403 with `Access to settings pages is restricted to the
local network or localhost: 10.100.0.1 -> 10.100.0.100` in
/var/log/squeezeboxserver/server.log.
-- Gerald KERMA <devel@cybermind.fr> Wed, 20 May 2026 20:00:00 +0200
secubox-lyrion (1.0.1-1~bookworm1) bookworm; urgency=medium
* debian/rules: chmod 755 install-lxc.sh after cp -r (cp does not always

View File

@ -18,6 +18,9 @@ override_dh_auto_install:
# nginx snippet — install to BOTH secubox.d/ and secubox-routes.d/ (per v2.11.1 lesson)
install -d debian/secubox-lyrion/etc/nginx/secubox.d
[ -f nginx/lyrion.conf ] && cp nginx/lyrion.conf debian/secubox-lyrion/etc/nginx/secubox.d/ && (install -d debian/secubox-lyrion/etc/nginx/secubox-routes.d && cp nginx/lyrion.conf debian/secubox-lyrion/etc/nginx/secubox-routes.d/) || true
# Public + LAN vhost (sites-available — operator opts in via ln -s)
install -d debian/secubox-lyrion/etc/nginx/sites-available
[ -f nginx/lyrion-vhost.conf ] && cp nginx/lyrion-vhost.conf debian/secubox-lyrion/etc/nginx/sites-available/lyrion.conf || true
# Config example
install -d debian/secubox-lyrion/etc/secubox
[ -f conf/lyrion.toml.example ] && cp conf/lyrion.toml.example debian/secubox-lyrion/etc/secubox/lyrion.toml.example || true

View File

@ -60,6 +60,37 @@ ensure_masquerade() {
fi
}
# Slimproto (the LMS↔player protocol) lives on TCP+UDP :3483 inside the LXC.
# Without a DNAT on the host's LAN interface, LAN/WiFi players can't reach it
# (the LXC bridge 10.100.0.0/24 is not routed from the LAN). We drop a
# persistent rule file in /etc/nftables.d/ so it survives reboots — the
# systemd nftables.service loads everything in that directory.
ensure_slimproto_dnat() {
local nft_file="/etc/nftables.d/secubox-lyrion-dnat.nft"
local lan_iface="${SECUBOX_LAN_IFACE:-lan0}"
log "Ensuring slimproto DNAT ${lan_iface}:3483 → ${LXC_IP}:3483 ..."
install -d -m 0755 /etc/nftables.d
cat > "$nft_file" <<NFT
# /etc/nftables.d/secubox-lyrion-dnat.nft
# DNAT slimproto (TCP+UDP :3483) from LAN to the Lyrion LXC.
# Lets WiFi/LAN players (Squeezelite, iPeng, …) reach the LMS server
# without bridging the LXC network into the LAN.
# Generated by secubox-lyrion install-lxc.sh (#248).
table inet secubox-lyrion {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "${lan_iface}" tcp dport 3483 dnat ip to ${LXC_IP}:3483 comment "lyrion-slimproto-tcp"
iifname "${lan_iface}" udp dport 3483 dnat ip to ${LXC_IP}:3483 comment "lyrion-slimproto-udp"
}
}
NFT
chmod 0644 "$nft_file"
# Re-apply just this table (idempotent — flushes our own table first).
nft 'delete table inet secubox-lyrion' 2>/dev/null || true
nft -f "$nft_file" 2>&1
}
lxc_state() {
lxc-info -n "$LXC_NAME" -P "$LXC_PATH" 2>/dev/null \
| awk -F: '/^State:/ { gsub(/ /,"",$2); print tolower($2) }'
@ -164,6 +195,39 @@ INNER
}
configure_lms_for_proxy() {
# LMS has a `protectSettings: 1` default that 403s the settings pages
# whenever the request peer is the LXC bridge gateway (10.100.0.1) or on
# a "different network" — both true when traffic arrives through nginx
# reverse-proxy from the LAN. We disable it here because access control
# is enforced upstream (HAProxy + mitmproxy WAF + Authelia SSO).
# Also widen `allowedHosts` so the LAN can reach the UI even before
# the settings-protection lift takes effect.
log "Configuring LMS prefs for reverse-proxy access ..."
local prefs=/var/lib/squeezeboxserver/prefs/server.prefs
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl stop lyrionmusicserver 2>/dev/null || true
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- bash -e <<INNER
prefs=$prefs
if [ -f "\$prefs" ]; then
# protectSettings off — see comment in install-lxc.sh
if grep -q '^protectSettings' "\$prefs"; then
sed -i 's/^protectSettings:.*/protectSettings: 0/' "\$prefs"
else
echo 'protectSettings: 0' >> "\$prefs"
fi
# Widen allowedHosts to cover the LAN + bridge + localhost
if grep -q '^allowedHosts' "\$prefs"; then
sed -i 's|^allowedHosts:.*|allowedHosts: 127.0.0.1,10.100.0.*,192.168.*|' "\$prefs"
else
echo 'allowedHosts: 127.0.0.1,10.100.0.*,192.168.*' >> "\$prefs"
fi
fi
INNER
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl start lyrionmusicserver 2>/dev/null \
|| lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl start logitechmediaserver 2>/dev/null || true
}
mark_provisioned() {
install -d -m 0755 -o secubox -g secubox "$STATE_DIR"
date -Iseconds > "$SENTINEL"
@ -180,12 +244,17 @@ main() {
wait_for_network
ensure_resolv
install_lyrion_in_lxc
configure_lms_for_proxy
ensure_slimproto_dnat
mark_provisioned
log "OK — LXC '$LXC_NAME' at $LXC_IP, Lyrion provisioned + running."
log " · Web admin : http://$LXC_IP:$LYRION_HTTP_PORT/"
log " · JSON-RPC : http://$LXC_IP:9090/"
log " · slimproto : tcp://$LXC_IP:3483/ (players auto-discover)"
log " · Reverse proxy: /lyrion/ on the canonical hub vhost"
log " · Web admin (LXC) : http://$LXC_IP:$LYRION_HTTP_PORT/"
log " · JSON-RPC : http://$LXC_IP:9090/"
log " · slimproto (LXC) : tcp://$LXC_IP:3483/"
log " · slimproto (LAN) : tcp://<board-ip>:3483/ (DNAT'd; WiFi players OK)"
log " · LAN UI : http://<board-ip>:9000/ (enable nginx vhost)"
log " ln -s ../sites-available/lyrion.conf /etc/nginx/sites-enabled/"
log " systemctl reload nginx"
}
main "$@"

View File

@ -0,0 +1,87 @@
# /etc/nginx/sites-available/lyrion.conf — Lyrion Music Server vhost
# Installed by secubox-lyrion (#248). Operator enables with:
# ln -s ../sites-available/lyrion.conf /etc/nginx/sites-enabled/
# systemctl reload nginx
#
# Two server blocks:
# 1. LAN direct (port 9000) — http://192.168.1.200:9000/ → LXC
# No TLS, no WAF. For internal use only.
# 2. Public chain (port 9080) — Browser → HAProxy:443 (cert) → mitmproxy WAF
# → nginx:9080 (lyrion.maegia.tv) → LXC. Operator wires cert + HAProxy
# ACL + mitmproxy route separately (see #246 yacy for the recipe).
#
# Lyrion Music Server uses absolute paths internally (/skin/EN/..., /html/..., …)
# so it MUST live at vhost root — never proxy it under a subpath without
# rewriting the assets, or skins will 404 / 502.
#
# Player support (Squeezelite, iPeng, …) needs slimproto on TCP:3483. That is
# OUTSIDE this nginx vhost — operator adds nftables DNAT 192.168.1.200:3483 →
# 10.100.0.100:3483 (and matching FORWARD rule for the LXC bridge).
# ── 1. LAN direct (no TLS) ─────────────────────────────────────────────────
server {
listen 0.0.0.0:9000;
server_name 192.168.1.200 lyrion.local localhost _;
# Shared assets — banner JS, error pages
location /shared/ {
add_header Access-Control-Allow-Origin "*" always;
alias /usr/share/secubox/www/shared/;
}
# LMS web UI proxied to the LXC at root
location / {
proxy_pass http://10.100.0.100:9000/;
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 http;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 300s;
}
access_log /var/log/nginx/lyrion_lan_access.log;
error_log /var/log/nginx/lyrion_lan_error.log;
}
# ── 2. Public lyrion.maegia.tv (via HAProxy + mitmproxy) ───────────────────
server {
listen 0.0.0.0:9080;
server_name lyrion.maegia.tv;
# ACME HTTP-01 challenge — MUST come before the catch-all proxy so
# acme.sh renewals win (see #246 yacy fix for the same reason).
location ^~ /.well-known/acme-challenge/ {
root /usr/share/secubox/www;
try_files $uri =404;
}
# Health Banner injection (consistent with yacy.maegia.tv / auth.maegia.tv)
sub_filter_types text/html;
sub_filter_once on;
sub_filter "</body>" '<script src="/shared/health-banner.js"></script></body>';
# Shared assets — banner JS, error pages
location /shared/ {
add_header Access-Control-Allow-Origin "*" always;
alias /usr/share/secubox/www/shared/;
}
# LMS web UI proxied to the LXC at root
location / {
proxy_pass http://10.100.0.100:9000/;
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 https;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 300s;
}
access_log /var/log/nginx/lyrion_access.log;
error_log /var/log/nginx/lyrion_error.log;
}