Per mara on #3725: the on/off toggle is removed, and required env vars unset lead to a crash. HIVE_NETWORK_ISOLATION is gone from hive-network.nix -- it was the toggle. Validation happens once at daemon startup rather than per container. The variables are process-global, so a bad value breaks every container rather than one: failing at boot gives a single diagnostic naming the bad value, and cannot reach a state where some containers were configured before it was noticed. Option<NetworkIsolation> collapses to NetworkIsolation through the wire type, client and helper, which deletes the branch instead of leaving it unreachable. serde(default) is dropped on that field deliberately: a request omitting isolation is now rejected rather than defaulting to a container sharing the host's network namespace. What this replaces was a silent security downgrade. Of the four ways into the old fallback, two logged nothing at all -- a container came up without isolation and the journal agreed it was fine. Doc comments that still described the removed branch are updated (argus's note on #3723 scoped that to this issue). The hive-priv one is a minimal edit inside the block #3723 rewrites; de-splicing is that PR's job.
283 lines
11 KiB
Nix
283 lines
11 KiB
Nix
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.network;
|
|
|
|
# IPv4 helpers for the DHCP-pool computation below — nix integers
|
|
# are 64-bit so all /0-/32 values are safe.
|
|
ipToInt =
|
|
ip:
|
|
builtins.foldl' (acc: x: acc * 256 + x) 0 (
|
|
map lib.strings.toIntBase10 (lib.strings.splitString "." ip)
|
|
);
|
|
intToIp =
|
|
n:
|
|
let
|
|
a = n / 16777216;
|
|
b = (n - a * 16777216) / 65536;
|
|
c = (n - a * 16777216 - b * 65536) / 256;
|
|
d = n - a * 16777216 - b * 65536 - c * 256;
|
|
in
|
|
"${toString a}.${toString b}.${toString c}.${toString d}";
|
|
# 2^n via recursion (nix has no pow builtin).
|
|
pow2 = n: if n == 0 then 1 else 2 * (pow2 (n - 1));
|
|
hostCount = pow2 (32 - cfg.bridgePrefixLength);
|
|
# Mask off host bits to get the network base address.
|
|
networkBase = builtins.bitAnd (ipToInt cfg.bridgeIp) (4294967295 - hostCount + 1);
|
|
in
|
|
{
|
|
# Hive-internal network — host-side bridge + per-agent DNS resolver.
|
|
# Always active when hyperhive is enabled: agent containers run in
|
|
# private netns behind the bridge. Full design: docs/network.md.
|
|
|
|
imports = [
|
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "enable" ] ''
|
|
The hive network (bridge + dnsmasq resolver + private-netns
|
|
isolation) is always on whenever hyperhive is enabled. Remove the
|
|
setting.
|
|
'')
|
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "isolateContainers" ] ''
|
|
Network isolation is the only mode and is always on whenever
|
|
hyperhive is enabled; the shared-netns path was removed. Remove
|
|
the setting.
|
|
'')
|
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "upstreamDns" ] ''
|
|
The hive resolver always follows the host's resolvers now
|
|
(dnsmasq runs on the host and reads its /etc/resolv.conf
|
|
directly). Configure upstream DNS on the host itself instead.
|
|
'')
|
|
];
|
|
|
|
options.services.hyperhive.network = {
|
|
bridgeName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "hive-br0";
|
|
example = "h0";
|
|
description = ''
|
|
Name of the host-side bridge interface the hive uses for
|
|
inter-container traffic. Kept short so it survives the
|
|
IFNAMSIZ (15-char) cap, and prefixed so it's obviously
|
|
hive-managed in `ip link` output.
|
|
'';
|
|
};
|
|
|
|
bridgeIp = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "10.42.0.1";
|
|
example = "172.30.0.1";
|
|
description = ''
|
|
IPv4 address assigned to the bridge interface on the host
|
|
side. Agents use this address as their DNS server (the hive's
|
|
dnsmasq binds here). Default `10.42.0.1`
|
|
is in RFC 1918 space and unlikely to clash with operator's
|
|
existing setup; override if a different range is already in
|
|
use.
|
|
'';
|
|
};
|
|
|
|
bridgePrefixLength = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 24;
|
|
example = 16;
|
|
description = ''
|
|
Netmask prefix length for the bridge subnet. Default `/24`
|
|
gives 254 usable per-agent addresses, enough for any
|
|
single-host hive. Operator with a larger swarm or a tighter
|
|
addressing scheme overrides.
|
|
'';
|
|
};
|
|
|
|
exposeHostPorts = lib.mkOption {
|
|
type = lib.types.listOf lib.types.port;
|
|
default = [ ];
|
|
example = [ 5432 ];
|
|
description = ''
|
|
TCP ports on the host that agent containers may reach at the bridge
|
|
IP (`bridgeIp`). Each listed port `P` is opened on the bridge-interface
|
|
firewall, so an agent can connect to `''${bridgeIp}:P` (default
|
|
`10.42.0.1:P`).
|
|
|
|
Use this to let agents reach a host-local service you run yourself —
|
|
a database, a scratch HTTP endpoint, anything listening on
|
|
`''${bridgeIp}:P`.
|
|
|
|
**The host service must bind an address reachable from the bridge** —
|
|
`0.0.0.0` or the bridge IP (`bridgeIp`) — not loopback-only. The
|
|
bridge→`127.0.0.0/8` DROP rule (defence-in-depth) is unchanged: this
|
|
only opens the firewall, it does not bridge loopback. A service that
|
|
binds `127.0.0.1` only is still unreachable; rebind it to `0.0.0.0`.
|
|
|
|
The exposed port is reachable by EVERY agent on the bridge subnet
|
|
(same as DNS/gateway), so only expose services safe for any agent to
|
|
reach.
|
|
'';
|
|
};
|
|
|
|
# DHCP pool covering all usable host addresses on the bridge
|
|
# subnet, computed from bridgeIp/bridgePrefixLength: .2 (first
|
|
# usable after the .1 gateway) to .(hostCount-2) (last usable
|
|
# before broadcast). All containers — agents and service
|
|
# containers alike — receive their IPs dynamically from this pool;
|
|
# there are no hash-derived static assignments. Consumed by the
|
|
# hive's dnsmasq (hive-gateway module, host-side).
|
|
dhcpRangeStart = lib.mkOption {
|
|
type = lib.types.str;
|
|
internal = true;
|
|
readOnly = true;
|
|
default = intToIp (networkBase + 2);
|
|
defaultText = lib.literalMD "first usable bridge address after the gateway";
|
|
description = ''
|
|
Read-only computed first address of the bridge DHCP pool.
|
|
'';
|
|
};
|
|
|
|
dhcpRangeEnd = lib.mkOption {
|
|
type = lib.types.str;
|
|
internal = true;
|
|
readOnly = true;
|
|
default = intToIp (networkBase + hostCount - 2);
|
|
defaultText = lib.literalMD "last usable bridge address before broadcast";
|
|
description = ''
|
|
Read-only computed last address of the bridge DHCP pool.
|
|
'';
|
|
};
|
|
|
|
};
|
|
|
|
config = lib.mkMerge [
|
|
# The hive network + container isolation are unconditional whenever
|
|
# hyperhive is enabled: the shared-netns mode was removed, so there
|
|
# is one mode (private netns behind the bridge).
|
|
(lib.mkIf config.services.hyperhive.enable {
|
|
# This message is only useful if an operator can actually reach
|
|
# it, and an assertion competes with every eager default that
|
|
# reads the value it guards: option defaults that interpolate the
|
|
# domain (`forge.<domain>`, `matrix.<domain>`) throw while the
|
|
# assertion list is being evaluated, so the operator sees
|
|
# `cannot coerce null to a string` naming an unrelated option
|
|
# instead of the sentence below. Those defaults therefore stay
|
|
# total, falling back to a name under the reserved `.invalid` TLD
|
|
# (RFC 2606) — a value this assertion then refuses to let out the
|
|
# door, and one that fails loudly at resolution rather than
|
|
# quietly working if it somehow did.
|
|
assertions = [
|
|
{
|
|
assertion = config.services.hyperhive.domain != null;
|
|
message = ''
|
|
hyperhive requires services.hyperhive.domain to be set — the
|
|
hive resolver is authoritative for `<hive-domain>` and its
|
|
sub-domains, and agents reach the forge/matrix through the
|
|
gateway by that domain.
|
|
|
|
It is read from this hive's entry in the swarm directory, so
|
|
what is actually missing is that entry:
|
|
|
|
services.hyperhive.swarm.hives."<hiveName>" = { };
|
|
|
|
whose `domain` defaults to `<hiveName>.<swarm.domain>`. The
|
|
assertion in ./swarm.nix names it precisely; this one is the
|
|
backstop.
|
|
'';
|
|
}
|
|
{
|
|
assertion = config.services.hyperhive.swarm.domain != null;
|
|
message = ''
|
|
hyperhive requires services.hyperhive.swarm.domain to be
|
|
set — the DNS domain of the swarm this hive belongs to,
|
|
of which this hive occupies one sub-domain. There is no
|
|
fallback: a guessed value would be a wrong hostname that
|
|
evaluates cleanly and deploys. Set it
|
|
(`services.hyperhive.swarm.domain = "example.com";`) —
|
|
with `hiveName` it also derives
|
|
`services.hyperhive.domain` for you.
|
|
'';
|
|
}
|
|
{
|
|
assertion = config.services.hyperhive.hiveName != null;
|
|
message = ''
|
|
hyperhive requires services.hyperhive.hiveName to be set —
|
|
it is this hive's label within the swarm, and the leftmost
|
|
part of the domain it is addressed by
|
|
(`<hiveName>.<swarm.domain>`), not only a display name.
|
|
Set it (`services.hyperhive.hiveName = "pr1ma";`).
|
|
'';
|
|
}
|
|
];
|
|
|
|
# Virtual bridge — each agent container attaches a veth pair (isolation
|
|
# is unconditional now).
|
|
networking.bridges.${cfg.bridgeName}.interfaces = [ ];
|
|
|
|
# Bridge IP — the hive's dnsmasq binds here.
|
|
networking.interfaces.${cfg.bridgeName}.ipv4.addresses = [
|
|
{
|
|
address = cfg.bridgeIp;
|
|
prefixLength = cfg.bridgePrefixLength;
|
|
}
|
|
];
|
|
|
|
# DNS + DHCP on the bridge interface only — no external amplification
|
|
# surface. UDP 67 is required for the dnsmasq DHCP pool: dnsmasq
|
|
# receives DHCPDISCOVER via a regular UDP socket (no netfilter-bypassing
|
|
# raw socket like ISC dhcpd), so without this hole the host INPUT chain
|
|
# drops the broadcasts and every container falls back to IPv4LL.
|
|
networking.firewall.interfaces.${cfg.bridgeName} = {
|
|
allowedUDPPorts = [
|
|
53
|
|
67
|
|
];
|
|
allowedTCPPorts = [ 53 ];
|
|
};
|
|
})
|
|
|
|
# Container isolation overlay — now unconditional (the shared-netns
|
|
# mode was removed). See docs/network.md#container-isolation.
|
|
(lib.mkIf config.services.hyperhive.enable {
|
|
|
|
# Agents route internet traffic via the bridge; NAT masquerades their RFC-1918 IPs.
|
|
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
|
|
networking.nat = {
|
|
enable = true;
|
|
internalInterfaces = [ cfg.bridgeName ];
|
|
};
|
|
|
|
# Defence-in-depth: DROP bridge→loopback so compromised agents can't
|
|
# reach host-loopback services even via routing table leaks.
|
|
networking.firewall.extraInputRules = ''
|
|
ip saddr ${cfg.bridgeIp}/${toString cfg.bridgePrefixLength} ip daddr 127.0.0.0/8 drop
|
|
'';
|
|
|
|
# Allow isolated agents to reach the gateway (nginx on the host, shared
|
|
# netns). Port 80 covers `http://forge.<domain>`, per-agent UI proxies,
|
|
# and any other HTTP services the gateway fronts. Port 443 for HTTPS.
|
|
# (`exposeHostPorts` opens its own ports in its dedicated block below,
|
|
# co-located with the proxies so the firewall hole + listener can't drift.)
|
|
networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = [
|
|
80
|
|
443
|
|
];
|
|
|
|
# Tells hive-c0re to pass PRIVATE_NETWORK + bridge settings to each
|
|
# container. HIVE_NETWORK_SUBNET is host-bridge IP/prefix, not canonical
|
|
# network address — the Rust side normalises before subnet arithmetic.
|
|
systemd.services.hive-c0re.environment = {
|
|
HIVE_NETWORK_BRIDGE = cfg.bridgeName;
|
|
HIVE_NETWORK_SUBNET = "${cfg.bridgeIp}/${toString cfg.bridgePrefixLength}";
|
|
};
|
|
})
|
|
|
|
# Host port exposure: open each `exposeHostPorts` entry on the bridge
|
|
# firewall so agents can reach a host service at `<bridgeIp>:P`. The host
|
|
# service must bind `0.0.0.0` or the bridge IP (a loopback-only bind stays
|
|
# unreachable — the bridge→127.0.0.0/8 DROP rule above is unchanged). This
|
|
# is firewall-only by design: a host service that binds `0.0.0.0` already
|
|
# serves the bridge IP, so an extra bridge-IP proxy would only collide
|
|
# (EADDRINUSE) with it. Merges with the [ 80 443 ] gateway ports above.
|
|
(lib.mkIf (config.services.hyperhive.enable && cfg.exposeHostPorts != [ ]) {
|
|
networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = cfg.exposeHostPorts;
|
|
})
|
|
];
|
|
}
|