fix(deploy): the home end dials the instance's server by its address (ADR 0032, D11)
All checks were successful
CI / check (push) Successful in 41m12s
CI / deploy (push) Successful in 56s

A tunnel that fails to come up takes Jellyfin's internet with it, as
linuxserver/wireguard's client mode deletes the default route. Named by
learn.halkyon.eu, the endpoint needed DNS to come up, which the home end
starting or waking hasn't always got yet; by its address the tunnel comes
up whatever the network does. Closes ADR 0032's open point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-31 15:10:46 +01:00
commit 82fa7aec1b
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 37 additions and 21 deletions

View file

@ -8,7 +8,11 @@ PrivateKey = <the home end's private key>
[Peer]
PublicKey = <the instance's public key>
Endpoint = learn.halkyon.eu:51820
# learn.halkyon.eu's server by its IPv4 address, not its name (ADR 0032, D11): a name
# needs DNS to come up, which a machine starting or waking hasn't always got yet, and a
# tunnel that fails to come up takes Jellyfin's internet with it. A new address of the
# server's goes here too.
Endpoint = 89.58.53.154:51820
# The instance's end alone, through which Caddy's requests come (masqueraded by Docker
# on the instance's server): nothing else of the internet goes through the tunnel.
AllowedIPs = 10.61.96.1/32

View file

@ -191,7 +191,8 @@ home machine.
`sudo wg show wg-jellyfin` on the instance's server now shows a handshake.
Whenever `wg0.conf` fails to come up (a placeholder or a wrong key, an endpoint that
doesn't resolve), the image's client mode deletes the namespace's default route
doesn't resolve: why it names the server by its address, ADR 0032, D11, and a new
address of the server's goes there too), the image's client mode deletes the namespace's default route
(`ip route del default` in its `svc-wireguard/run`, ls126), and Jellyfin with it loses
the internet: TMDb's metadata and images fail with *Network is unreachable*, and a
library scanned then gets frames of its videos for posters. A tunnel that comes up

View file

@ -131,6 +131,26 @@ as the sidecar isn't switched on. With `JELLYFIN_URL` unset, the instance sugges
online, not only at home, as long as outgoing UDP passes. Asleep, the tunnel stops; on
waking, the next keepalive brings it back within 25 s.
- **D11 The home end dials the server by its address** (the developer's, 31 Oct, found
setting the home end up): `Endpoint = 89.58.53.154:51820` in `wg0.conf`, not
`learn.halkyon.eu`. Whenever `wg0.conf` fails to come up, linuxserver/wireguard's client
mode deletes the namespace's default route (`ip route del default` in its
`svc-wireguard/run`, ls126: a kill switch, for the VPN clients it is made for), so
Jellyfin, in that namespace, reaches neither TMDb nor anything else, and a library
scanned then gets frames of its videos for posters, until both containers are
recreated. Bringing an interface up needs no network, but resolving a name does
(`wg` retries it for some seconds, then fails), so on the home end (D10), starting or
waking before its network and DNS, the name made that failure recur. By address it
comes up whatever the network does, the handshake following once it is back; what
is left is a placeholder or a wrong key, met once while setting it up, which the
sidecar README's step 4 says how to recover from. IPv4, which every home network has.
A new address of the server's has to be written in by hand. Over a custom service
in the image's `/custom-services.d` putting the route back (working against the
image's kill switch and resting on its scripts as ls126 has them, which an update
may change unnoticed), and over turning the namespace round, Jellyfin owning it and
a WireGuard container without a kill switch joining it (a new D2 for a failure the
address all but removes).
## Consequences
- People reaching `jellyfin.halkyon.eu` see the instance's server alone. The instance's
@ -186,22 +206,3 @@ as the sidecar isn't switched on. With `JELLYFIN_URL` unset, the instance sugges
its client mode brings up `wg0.conf` with the host's kernel module, and its `wg`
makes the home end's keys, so nothing is installed on the machine. Jellyfin's image
is the sidecar's, and so is its user ID, 61096.
## Open (31 Oct 2026, found setting the home end up; for the developer to decide)
- **A failed tunnel takes Jellyfin's internet with it.** The image's client mode deletes
the namespace's default route whenever `wg0.conf` fails to come up (`ip route del
default` in its `svc-wireguard/run`, ls126: a placeholder or wrong key, an endpoint
that doesn't resolve), so Jellyfin, in that namespace, reaches neither TMDb nor
anything else, and a library scanned then gets frames of its videos for posters. A
tunnel that comes up keeps the route, before its first handshake too. Noted in
`deploy/sidecars/README.md`, step 4. Ways out:
- keep it, documented (as now): a broken tunnel then also stops new metadata, which
is loud rather than silent;
- a custom service (the image's `/custom-services.d`; `/custom-cont-init.d` runs
before `svc-wireguard`, too early) that puts the route back once the tunnel has
failed, so Jellyfin keeps the internet whatever the tunnel does;
- Jellyfin on `outside` itself rather than in WireGuard's namespace, which changes
how Caddy's requests reach it (D8) and so reopens this record.
A test in `test/deploy/jellyfin_home_test.dart` for whichever is chosen.

View file

@ -146,6 +146,16 @@ void main() {
expect(wireguard['ports'], ['127.0.0.1:8096:8096']);
});
test("dials the instance's server by its address, so that the tunnel "
'comes up before the network does (D11)', () {
final config = File('deploy/home-jellyfin/wg0.conf.example')
.readAsLinesSync();
final endpoint = config.singleWhere(
(line) => line.startsWith('Endpoint'),
);
expect(endpoint, 'Endpoint = 89.58.53.154:51820');
});
test('tells Jellyfin its public address, never a home one', () {
final jellyfin = services['jellyfin'] as YamlMap;
final environment = jellyfin['environment'] as YamlMap;