158 lines
5 KiB
Dart
158 lines
5 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:test/test.dart';
|
||
|
|
import 'package:yaml/yaml.dart';
|
||
|
|
|
||
|
|
/// Jellyfin at home, published by the instance through WireGuard (ADR 0032):
|
||
|
|
/// the tunnel reaches Jellyfin from Caddy alone, and nothing else of the
|
||
|
|
/// instance's server; nothing CI's checks would ever run, so held here.
|
||
|
|
void main() {
|
||
|
|
YamlMap compose(String path) =>
|
||
|
|
loadYaml(File(path).readAsStringSync()) as YamlMap;
|
||
|
|
|
||
|
|
group('the instance publishes it (D7, D9)', () {
|
||
|
|
final home = compose('deploy/compose.jellyfin-home.yaml');
|
||
|
|
final services = home['services'] as YamlMap;
|
||
|
|
|
||
|
|
test('runs no Jellyfin of its own', () {
|
||
|
|
expect(services.keys, isNot(contains('jellyfin')));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('has Caddy proxy to the home end over the tunnel', () {
|
||
|
|
final caddy = services['caddy'] as YamlMap;
|
||
|
|
final environment = caddy['environment'] as YamlMap;
|
||
|
|
expect(environment['JELLYFIN_UPSTREAM'], '10.61.96.2:8096');
|
||
|
|
expect(
|
||
|
|
caddy['volumes'],
|
||
|
|
contains(
|
||
|
|
'./sidecars/jellyfin.caddy:/etc/caddy/sidecars/jellyfin.caddy:ro',
|
||
|
|
),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('never sets JELLYFIN_URL, so the instance suggests it only while it '
|
||
|
|
'answers', () {
|
||
|
|
final server = services['server'] as YamlMap?;
|
||
|
|
final environment = server?['environment'] as YamlMap?;
|
||
|
|
expect(environment?.keys ?? const [], isNot(contains('JELLYFIN_URL')));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('has the server look at Jellyfin through Caddy on edge', () {
|
||
|
|
final server = services['server'] as YamlMap;
|
||
|
|
expect(
|
||
|
|
server['extra_hosts'],
|
||
|
|
contains(
|
||
|
|
r'${JELLYFIN_DOMAIN:?set JELLYFIN_DOMAIN in deploy/.env}:'
|
||
|
|
'172.30.80.2',
|
||
|
|
),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group("the sidecar's site", () {
|
||
|
|
final site = File('deploy/sidecars/jellyfin.caddy').readAsStringSync();
|
||
|
|
|
||
|
|
test('proxies to the sidecar unless told otherwise', () {
|
||
|
|
expect(
|
||
|
|
site,
|
||
|
|
contains(r'reverse_proxy {$JELLYFIN_UPSTREAM:jellyfin:8096}'),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('still refuses changes to users and the setup wizard', () {
|
||
|
|
expect(site, contains('respond @userChanges 403'));
|
||
|
|
expect(site, contains('respond /Startup/* 403'));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group("the server's end of the tunnel (D4, D5)", () {
|
||
|
|
final config = File('deploy/home-jellyfin/server/wg-jellyfin.conf')
|
||
|
|
.readAsStringSync();
|
||
|
|
final rules = File('deploy/home-jellyfin/server/wg-jellyfin-rules.sh')
|
||
|
|
.readAsStringSync();
|
||
|
|
|
||
|
|
test('lets the home end send from its own address alone', () {
|
||
|
|
expect(
|
||
|
|
RegExp(
|
||
|
|
r'^AllowedIPs = (.+)$',
|
||
|
|
multiLine: true,
|
||
|
|
).allMatches(config).map((match) => match[1]),
|
||
|
|
['10.61.96.2/32'],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('holds no endpoint for the home end', () {
|
||
|
|
expect(config, isNot(contains('Endpoint')));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('sets its rules as it comes up and takes them away as it goes', () {
|
||
|
|
expect(
|
||
|
|
config,
|
||
|
|
contains('PostUp = /etc/wireguard/wg-jellyfin-rules.sh up'),
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
config,
|
||
|
|
contains('PreDown = /etc/wireguard/wg-jellyfin-rules.sh down'),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('forwards only Caddy to Jellyfin into the tunnel, and only replies '
|
||
|
|
'out of it', () {
|
||
|
|
expect(
|
||
|
|
rules,
|
||
|
|
contains(
|
||
|
|
r'-o "$interface" -s 172.30.80.2 -d 10.61.96.2 -p tcp --dport 8096 '
|
||
|
|
'-j RETURN',
|
||
|
|
),
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
rules,
|
||
|
|
contains(
|
||
|
|
r'-i "$interface" -m conntrack --ctstate ESTABLISHED,RELATED '
|
||
|
|
'-j RETURN',
|
||
|
|
),
|
||
|
|
);
|
||
|
|
expect(rules, contains(r'iptables -A "$forward" -j DROP'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('accepts no new connection from the tunnel on the host', () {
|
||
|
|
expect(rules, contains(r'iptables -A "$input" -j DROP'));
|
||
|
|
expect(rules, contains(r'iptables -I INPUT -i "$interface" -j "$input"'));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("the CI runner can't reach the tunnel (D6)", () {
|
||
|
|
final unit = File('deploy/runner/forgejo-runner.service')
|
||
|
|
.readAsStringSync();
|
||
|
|
final denied = RegExp(
|
||
|
|
r'^IPAddressDeny=(.+)$',
|
||
|
|
multiLine: true,
|
||
|
|
).firstMatch(unit)![1]!.split(' ');
|
||
|
|
expect(denied, contains('10.61.96.0/30'));
|
||
|
|
});
|
||
|
|
|
||
|
|
group('the home end (D2)', () {
|
||
|
|
final home = compose('deploy/home-jellyfin/compose.yaml');
|
||
|
|
final services = home['services'] as YamlMap;
|
||
|
|
|
||
|
|
test("has Jellyfin inside the tunnel's network namespace", () {
|
||
|
|
final jellyfin = services['jellyfin'] as YamlMap;
|
||
|
|
expect(jellyfin['network_mode'], 'service:wireguard');
|
||
|
|
expect(jellyfin.keys, isNot(contains('ports')));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("publishes Jellyfin's port on this machine's loopback alone", () {
|
||
|
|
final wireguard = services['wireguard'] as YamlMap;
|
||
|
|
expect(wireguard['ports'], ['127.0.0.1:8096:8096']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('tells Jellyfin its public address, never a home one', () {
|
||
|
|
final jellyfin = services['jellyfin'] as YamlMap;
|
||
|
|
final environment = jellyfin['environment'] as YamlMap;
|
||
|
|
expect(
|
||
|
|
environment['JELLYFIN_PublishedServerUrl'],
|
||
|
|
'https://jellyfin.halkyon.eu',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|