Watch
0
0
Fork
You've already forked hyperhive
0

gateway: per-vhost public flag, private listen addresses, and IPv6 listeners

This commit is contained in:
atlas 2026-11-09 21:38:36 +01:00
commit fbbdf26a19
2 changed files with 364 additions and 25 deletions

View file

@ -75,11 +75,43 @@ let
};
# The controller behind a gateway on a non-default TLS port, with the
# firewall opened. Its collector URL and its listener are read off it.
# firewall opened, a private IPv4 and IPv6 address, and one vhost kept off the
# public address. Its collector URL and its listeners are read off it.
controllerAltPort = hive {
deploy.swarm-controller.enable = true;
gateway.httpsPort = 8443;
gateway.openFirewall = true;
gateway.privateListenAddresses = [
"10.100.0.1"
"fd00:100::1"
];
gateway.vhosts."internal.t.local".public = false;
};
# A private address that is the wildcard, beside a vhost that asks to be
# private: the listen set is well-formed, the privacy it promises is not.
privateWildcard = hive {
gateway.privateListenAddresses = [ "0.0.0.0" ];
gateway.vhosts."internal.t.local".public = false;
};
# Only its `networking.enableIPv6` default is read.
noIPv6 = hiveWith [ { networking.enableIPv6 = false; } ] { };
gatewayListen = import ../host-modules/hive-gateway/listen.nix { inherit lib; };
# nginx fills defaults into each `listen` entry; compare the fields the
# gateway sets.
listenFields = map (l: {
inherit (l) addr port;
ssl = l.ssl or false;
});
httpAt = addr: port: {
inherit addr port;
ssl = false;
};
httpsAt = addr: port: {
inherit addr port;
ssl = true;
};
# A container the operator runs that hyperhive doesn't own, next to one it
@ -2176,6 +2208,120 @@ let
&& lib.length (checks "swarm-bao") == 1
&& checks "operator-box" == [ ];
}
{
# Presence controls: the IPv6 arm is on in this fixture, and the private
# list is empty, so what is absent below is absent by configuration.
name = "a gateway with no private addresses listens on both ports of 0.0.0.0 and [::] and nowhere else";
ok =
let
gw = allLocal.services.hyperhive.gateway;
l = allLocal.services.nginx.virtualHosts."_".listen;
in
gw.listenIPv6
&& gw.privateListenAddresses == [ ]
&&
listenFields l == [
(httpAt "0.0.0.0" 80)
(httpsAt "0.0.0.0" 443)
(httpAt "[::]" 80)
(httpsAt "[::]" 443)
]
&& listenFields gw.lib.listen == listenFields l
&& gw.lib.privateListen == [ ];
}
{
# `listenIPv6` is the escape for a host whose kernel cannot open an IPv6
# socket, and follows `networking.enableIPv6` unless set.
name = "the IPv6 listener can be turned off and follows networking.enableIPv6";
ok =
let
off = {
port = 80;
httpsPort = 443;
listenIPv6 = false;
privateListenAddresses = [ ];
};
in
listenFields (gatewayListen.public off) == [
(httpAt "0.0.0.0" 80)
(httpsAt "0.0.0.0" 443)
]
&& !noIPv6.services.hyperhive.gateway.listenIPv6
&& allLocal.services.hyperhive.gateway.listenIPv6;
}
{
# An IPv6 literal is bracketed whichever way the operator wrote it, and
# listed once: nginx refuses a duplicate `listen`. The wildcard test is
# paired with addresses that are not wildcards.
name = "private addresses render IPv6 in brackets, once, and the wildcards are recognised";
ok =
let
cfg = {
port = 80;
httpsPort = 443;
listenIPv6 = true;
privateListenAddresses = [
"10.100.0.1"
"fd00:100::1"
"[fd00:100::1]"
];
};
in
listenFields (gatewayListen.private cfg) == [
(httpAt "10.100.0.1" 80)
(httpsAt "10.100.0.1" 443)
(httpAt "[fd00:100::1]" 80)
(httpsAt "[fd00:100::1]" 443)
]
&& lib.all gatewayListen.isWildcard [
"0.0.0.0"
"::"
"[::]"
"*"
]
&& !(lib.any gatewayListen.isWildcard cfg.privateListenAddresses);
}
{
# The private vhost binds the private addresses only; the catch-all and
# every vhost that stays public bind the wildcards AND the private
# addresses, because nginx gives a private address a server set of its
# own. The catch-all arm is the presence control for the wildcard check.
name = "a vhost with public = false listens on the private addresses only, the catch-all on those and the wildcards";
ok =
let
vh = controllerAltPort.services.nginx.virtualHosts;
private = [
(httpAt "10.100.0.1" 80)
(httpsAt "10.100.0.1" 8443)
(httpAt "[fd00:100::1]" 80)
(httpsAt "[fd00:100::1]" 8443)
];
in
listenFields vh."internal.t.local".listen == private
&& !(lib.any (l: l.addr == "0.0.0.0" || l.addr == "[::]") vh."internal.t.local".listen)
&&
listenFields vh."_".listen == [
(httpAt "0.0.0.0" 80)
(httpsAt "0.0.0.0" 8443)
(httpAt "[::]" 80)
(httpsAt "[::]" 8443)
]
++ private
&& vh."_".default;
}
{
# Both arms: the wildcard refusal fires on the wildcard, and the
# "needs an address" refusal stays quiet there because the list is not
# empty.
name = "a wildcard private address is refused";
ok =
let
fired = lib.filter (a: !a.assertion) privateWildcard.assertions;
mentions = s: lib.any (a: lib.hasInfix s a.message) fired;
in
mentions "privateListenAddresses holds"
&& !(mentions "while services.hyperhive.gateway.privateListenAddresses is empty");
}
]
# Starting a unit that stays `active (exited)` is a no-op, so a timer aimed
# at one fires and fetches nothing.

View file

@ -91,6 +91,8 @@ let
swarm.authelia.url = "https://auth.example.invalid";
# Declares a vhost that binds its own `listen`, which the gateway refuses.
gateway.vhosts."own-listen.t.local".listen = [ ];
# Asks to be private on a gateway with no private address to be private on.
gateway.vhosts."internal.t.local".public = false;
};
# Did ./host-modules/swarm-grafana.nix refuse this host, and for which of its
@ -1081,6 +1083,16 @@ let
&& lib.hasInfix "own-listen.t.local.listen" a.message
) grafanaNoIdentity.assertions;
}
{
# A private vhost with nowhere to listen would be silently unserved.
name = "a vhost with public = false is refused while there is no private address";
ok = lib.any (
a:
!a.assertion
&& lib.hasInfix "privateListenAddresses is empty" a.message
&& lib.hasInfix "internal.t.local" a.message
) grafanaNoIdentity.assertions;
}
];
in
runGroup "grafana" cases