semi-spread example: serve internal vhosts on the VPN addresses only, over IPv4 and IPv6
This commit is contained in:
parent
538da5a13a
commit
7800f92425
1 changed files with 356 additions and 89 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue