Watch
0
0
Fork
You've already forked hyperhive
0

semi-spread example: serve internal vhosts on the VPN addresses only, over IPv4 and IPv6

This commit is contained in:
atlas 2026-11-10 00:07:38 +01:00 • committed by mara
commit 7800f92425

View file

@ -95,6 +95,72 @@ let
vhosts = m: builtins.attrNames m.services.nginx.virtualHosts;
# The `listen` entries of one vhost, as `addr:port`.
listens =
m: vhost: map (l: "${l.addr}:${toString l.port}") m.services.nginx.virtualHosts.${vhost}.listen;
# Both ports on each address.
bothPorts =
addrs:
lib.concatMap (a: [
"${a}:80"
"${a}:443"
]) addrs;
# The private addresses of a machine as nginx spells them: its VPN IPv4 and
# bracketed IPv6 address, and the hive bridge.
privateAddrs = name: m: [
mesh.${name}.address
"[${mesh.${name}.address6}]"
m.services.hyperhive.network.bridgeIp
];
# The vhosts that listen on the wildcard, and those that listen on private
# addresses only. The catch-all `_` is neither: it is not a service.
publicVhosts =
m:
lib.sort (a: b: a < b) (
lib.filter (n: n != "_" && lib.elem "0.0.0.0:443" (listens m n)) (vhosts m)
);
privateVhosts =
m:
lib.sort (a: b: a < b) (
lib.filter (n: n != "_" && !lib.elem "0.0.0.0:443" (listens m n)) (vhosts m)
);
sorted = lib.sort (a: b: a < b);
# What each machine is meant to serve to everyone, and what only inside.
expectedPublic = {
swarm-central = [ (swarmOf central).domain ];
swarm-services = [
(swarmOf services).authelia.domain
(swarmOf services).forge.domain
(swarmOf services).matrix.gatewayHost
];
swarm-heavyduty = [ ];
hive-one = [ ];
hive-two = [ ];
};
expectedPrivate = {
swarm-central = [ (swarmOf central).bao.ui.domain ];
swarm-services = with swarmOf services; [
grafana.domain
nats.domain
otel.domain
victorialogs.domain
victoriametrics.domain
];
swarm-heavyduty = [ (swarmOf heavyduty).attic.ownDomain ];
hive-one = [
(swarmOf hiveOne).dashboardDomains.one
(swarmOf hiveOne).hives.one.domain
];
hive-two = [
(swarmOf hiveTwo).dashboardDomains.two
(swarmOf hiveTwo).hives.two.domain
];
};
cases = [
# Every machine evaluates with its assertions holding. `tryEval`
# turns a refusal into a failed case that names the machine.
@ -340,6 +406,100 @@ let
# Control: the file is not one every machine shares.
&& !lib.hasInfix (swarmOf central).bao.domain (resolverTable central);
}
# Which vhosts answer where. Public means the wildcard, IPv4 and IPv6;
# private means the machine's VPN addresses and its bridge, and nothing
# else.
{
name = "only the swarm UI, SSO login, forge and matrix answer on the public address";
ok = lib.all (n: publicVhosts all.${n} == sorted expectedPublic.${n}) (builtins.attrNames all);
}
{
# Presence control for the case above: the same read finds every vhost
# of the private set, so an empty public list is not a misread.
name = "every other vhost answers on the private addresses only";
ok = lib.all (
n:
let
m = all.${n};
private = privateAddrs n m;
in
privateVhosts m == sorted expectedPrivate.${n}
&& lib.all (v: sorted (listens m v) == sorted (bothPorts private)) expectedPrivate.${n}
) (builtins.attrNames all);
}
{
name = "public vhosts listen on 0.0.0.0 and [::] and also on the private addresses";
ok =
lib.all (
n:
let
m = all.${n};
want = bothPorts (
[
"0.0.0.0"
"[::]"
]
++ privateAddrs n m
);
in
lib.all (v: sorted (listens m v) == sorted want) expectedPublic.${n}
) (builtins.attrNames all)
# Control: the lists above are not empty.
&& expectedPublic.swarm-services != [ ];
}
{
name = "the catch-all listens on the private addresses too, and the wildcard";
ok = lib.all (
n:
let
m = all.${n};
in
sorted (listens m "_") == sorted (
bothPorts (
[
"0.0.0.0"
"[::]"
]
++ privateAddrs n m
)
)
) (builtins.attrNames all);
}
{
name = "the private addresses hold the VPN's IPv4 and IPv6 address of the machine, and the bridge";
ok = lib.all (
n:
all.${n}.services.hyperhive.gateway.privateListenAddresses == [
mesh.${n}.address
mesh.${n}.address6
all.${n}.services.hyperhive.network.bridgeIp
]
) (builtins.attrNames all);
}
{
name = "the VPN carries IPv6: each machine has its address and a /128 per peer";
ok = lib.all (
n:
let
wg = all.${n}.networking.wireguard.interfaces.wg0;
in
lib.elem "${mesh.${n}.address6}/64" wg.ips
&& lib.all (p: lib.any (a: lib.hasSuffix "/128" a) p.allowedIPs) wg.peers
) (builtins.attrNames all);
}
{
name = "a hive resolves the internal names to the IPv6 addresses as well";
ok =
let
t = hostsOf hiveOne;
in
lib.elem (swarmOf hiveOne).bao.domain (t.${mesh.swarm-central.address6} or [ ])
&& lib.elem (swarmOf hiveOne).grafana.domain (t.${mesh.swarm-services.address6} or [ ])
&& lib.hasInfix "${mesh.swarm-heavyduty.address6} ${(swarmOf hiveOne).attic.ownDomain}" (
resolverTable hiveOne
);
}
];
in
runGroup "semi-spread" cases