276 lines
12 KiB
Nix
276 lines
12 KiB
Nix
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.network;
|
|
in
|
|
{
|
|
# Hive-internal network — host-side bridge + per-agent DNS resolver.
|
|
# Containers stay on shared host netns at v1; this module stands the
|
|
# bridge + resolver up so the endpoint is in place before network
|
|
# isolation flips containers to private netns. Full design: docs/network.md.
|
|
|
|
options.services.hyperhive.network = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = config.services.hyperhive.enable;
|
|
defaultText = lib.literalExpression "config.services.hyperhive.enable";
|
|
example = false;
|
|
description = ''
|
|
Stand up the hive-internal bridge + dnsmasq resolver.
|
|
Defaults to `config.services.hyperhive.enable` so it comes
|
|
on automatically with the rest of hyperhive. Requires
|
|
`services.hyperhive.domain` to be set — the dnsmasq resolver
|
|
is authoritative for `<hive-domain>` and its sub-domains.
|
|
When enabled: a bridge interface (`bridgeName`) appears on the
|
|
host with `bridgeIp` assigned, and the hive-gateway container
|
|
runs a dnsmasq listening on that IP for `<hive-domain>` +
|
|
sub-domains. Agent containers still default to shared host
|
|
netns — the endpoint is up but only used once
|
|
`isolateContainers = true` flips containers to private netns
|
|
+ veth peers on this bridge.
|
|
'';
|
|
};
|
|
|
|
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. Becomes the DNS server address agents point at (and
|
|
the upstream the gateway proxies to once netns isolation
|
|
lands). 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.
|
|
'';
|
|
};
|
|
|
|
upstreamDns = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [
|
|
"1.1.1.1"
|
|
"9.9.9.9"
|
|
];
|
|
example = [
|
|
"192.168.1.1"
|
|
"8.8.8.8"
|
|
];
|
|
description = ''
|
|
Upstream DNS servers dnsmasq forwards non-hive queries to.
|
|
Defaults to Cloudflare + Quad9. Override for operators on
|
|
private networks who need a specific resolver (corporate
|
|
DNS, pi-hole, etc.). The hive resolver itself stays
|
|
authoritative for `<hive-domain>` and its sub-domains
|
|
regardless of upstream choice.
|
|
'';
|
|
};
|
|
|
|
isolateContainers = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
example = true;
|
|
description = ''
|
|
Flip agent containers from shared host netns to private netns
|
|
(#14). When true, each agent container gets a dedicated veth
|
|
pair attached to `bridgeName` and a deterministic IP from
|
|
the bridge subnet. The bridge (already up when `enable = true`)
|
|
becomes the sole routed path between the host and agent
|
|
containers.
|
|
|
|
The host-side nix effect (this option) is:
|
|
- Sets `HIVE_NETWORK_ISOLATION=1` in the c0re service env so
|
|
the Rust lifecycle knows to pass `--private-network` +
|
|
bridge settings when creating/updating containers.
|
|
- Enables IP forwarding + NAT so agents can reach the internet
|
|
through the host.
|
|
- Adds a firewall rule DROP'ing traffic from the bridge subnet
|
|
to the host's loopback addresses — defence-in-depth so a
|
|
compromised agent can't reach the c0re dashboard (already
|
|
bound to 127.0.0.1) or other host-loopback services even if
|
|
the routing table somehow leaks.
|
|
- Allows HTTP/HTTPS (80/443) traffic from the bridge subnet to
|
|
the host so agents can reach the gateway container (shared
|
|
host netns, proxies the operator's per-agent UI).
|
|
|
|
**Prerequisite**: all agents must have
|
|
`hyperhive.web.useUnixSocket = true` before enabling isolation.
|
|
Agents that still bind TCP on `0.0.0.0:<port>` will be
|
|
reachable at their bridge IP from other agents on the same
|
|
subnet — defeating the isolation goal. The gateway routes via
|
|
unix sockets so gateway reach still works regardless.
|
|
|
|
**Migration**: containers are destroyed and re-created when
|
|
the network isolation flag flips. Operator state under
|
|
`/agents/<name>/state/` is bind-mounted and survives; the
|
|
container rootfs (nix store paths) is recreated cleanly.
|
|
|
|
**Rust counterpart**: `hive-c0re` reads `HIVE_NETWORK_ISOLATION`
|
|
and `HIVE_NETWORK_BRIDGE` from its service env and uses them in
|
|
`lifecycle::set_nspawn_flags` to configure `PRIVATE_NETWORK`,
|
|
`LOCAL_ADDRESS`, and `HOST_BRIDGE` in each container's
|
|
`nixos-containers/<name>.conf`. See `docs/network.md` for the
|
|
full design.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkMerge [
|
|
(lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = config.services.hyperhive.domain != null;
|
|
message = ''
|
|
services.hyperhive.network.enable = true requires
|
|
services.hyperhive.domain to be set — the resolver needs a
|
|
domain to be authoritative for. Either pin a hostname
|
|
(`services.hyperhive.domain = "example.com";`) or leave
|
|
`network.enable` at its default of false.
|
|
'';
|
|
}
|
|
{
|
|
assertion = config.services.hyperhive.gateway.enable;
|
|
message = ''
|
|
services.hyperhive.network.enable = true requires
|
|
services.hyperhive.gateway.enable = true — the dnsmasq
|
|
resolver runs inside the hive-gateway container (single
|
|
front-door for both DNS and HTTP). Enable the gateway or
|
|
leave `network.enable` at its default of false.
|
|
'';
|
|
}
|
|
];
|
|
|
|
# Bridge interface on the host. Empty interfaces list = purely
|
|
# virtual bridge (no slave NICs attached). Per-agent veth pairs
|
|
# will join this bridge once #14 lands; at v1 it stands alone.
|
|
networking.bridges.${cfg.bridgeName}.interfaces = [ ];
|
|
|
|
# Host-side IP assignment on the bridge. This is what dnsmasq
|
|
# (inside the gateway container, shared host netns) binds on.
|
|
networking.interfaces.${cfg.bridgeName}.ipv4.addresses = [
|
|
{
|
|
address = cfg.bridgeIp;
|
|
prefixLength = cfg.bridgePrefixLength;
|
|
}
|
|
];
|
|
|
|
# Open the resolver port in the host firewall for traffic from
|
|
# the bridge subnet only. Other interfaces stay closed —
|
|
# external DNS-amplification surface is not exposed.
|
|
networking.firewall.interfaces.${cfg.bridgeName} = {
|
|
allowedUDPPorts = [ 53 ];
|
|
allowedTCPPorts = [ 53 ];
|
|
};
|
|
})
|
|
|
|
# Guard: isolateContainers requires the bridge to be up. This arm is
|
|
# unconditional on cfg.enable so the assertion fires even when an
|
|
# operator sets isolateContainers=true but forgets enable=true (the
|
|
# combined-condition arm below would silently do nothing in that case).
|
|
(lib.mkIf cfg.isolateContainers {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.enable;
|
|
message = ''
|
|
services.hyperhive.network.isolateContainers = true requires
|
|
services.hyperhive.network.enable = true (the bridge and
|
|
resolver must be running before isolation is flipped on).
|
|
'';
|
|
}
|
|
];
|
|
})
|
|
|
|
# Container network isolation (#14 v1). Ships as a separate overlay
|
|
# on top of the base bridge config (which stays unconditional) so
|
|
# operators can stand the bridge + resolver up first, validate
|
|
# everything, then flip isolation on independently.
|
|
(lib.mkIf (cfg.enable && cfg.isolateContainers) {
|
|
|
|
# IP forwarding — agents need to route through the bridge to reach
|
|
# the internet. NixOS firewall's `nat.enable` sets this too, but
|
|
# making it explicit here keeps the intent visible alongside the
|
|
# NAT rule.
|
|
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
|
|
|
|
# NAT/masquerade: translate agent bridge IPs → host's outbound
|
|
# IP for internet-bound traffic. Without masquerade, packets from
|
|
# 10.42.0.X arrive at external servers with an RFC-1918 source
|
|
# that can't be routed back.
|
|
networking.nat = {
|
|
enable = true;
|
|
# `internalInterfaces` causes `MASQUERADE` on packets from the
|
|
# bridge leaving via any external interface. Only traffic from
|
|
# agents crosses the bridge — hive-gateway / forge / matrix
|
|
# stay on host netns and don't need NAT.
|
|
internalInterfaces = [ cfg.bridgeName ];
|
|
};
|
|
|
|
# DROP traffic from the bridge subnet to host loopback addresses.
|
|
# Defence-in-depth: the c0re dashboard already binds 127.0.0.1
|
|
# (not 0.0.0.0), so bridge-sourced traffic can't reach it via
|
|
# the bridge IP. But a misconfigured service that slips to
|
|
# 0.0.0.0 would otherwise be reachable. The DROP rule closes that
|
|
# window. Use `extraInputRules` (nftables `input` chain, priority
|
|
# 0, same ruleset as `allowedTCPPorts`) so it's processed before
|
|
# the accept rules for bridge-side DNS we added above.
|
|
#
|
|
# The rule fires only when an agent container has a bridge IP
|
|
# (i.e. after the Rust side also ships `PRIVATE_NETWORK=1`);
|
|
# until then all containers share host netns and no traffic
|
|
# originates from 10.42.0.0/24 so this is a dead letter.
|
|
networking.firewall.extraInputRules = ''
|
|
ip saddr ${cfg.bridgeIp}/${toString cfg.bridgePrefixLength} ip daddr 127.0.0.0/8 drop
|
|
'';
|
|
|
|
# Signal to the c0re Rust side that container isolation is
|
|
# enabled. c0re reads `HIVE_NETWORK_ISOLATION` from its service
|
|
# environment and uses it in `lifecycle::set_nspawn_flags` to set
|
|
# `PRIVATE_NETWORK=1`, `LOCAL_ADDRESS=<hash-ip>`, and
|
|
# `HOST_BRIDGE=<bridgeName>` in each agent's container config.
|
|
# Also forwards the bridge name + subnet so c0re can wire the
|
|
# veth without hardcoding.
|
|
#
|
|
# `systemd.services.hive-c0re.environment` is an attrset; NixOS
|
|
# merges contributions from all modules that set it, so this
|
|
# cross-module injection is idiomatic and doesn't require a
|
|
# dedicated option in hive-c0re.nix.
|
|
#
|
|
# Note: HIVE_NETWORK_SUBNET is the host-side bridge IP + prefix
|
|
# length (e.g. "10.42.0.1/24"), not the canonical network address
|
|
# ("10.42.0.0/24"). The Rust side must normalize (bitwise AND the
|
|
# IP with the mask) before using it for subnet membership checks or
|
|
# address arithmetic.
|
|
systemd.services.hive-c0re.environment = {
|
|
HIVE_NETWORK_ISOLATION = "1";
|
|
HIVE_NETWORK_BRIDGE = cfg.bridgeName;
|
|
HIVE_NETWORK_SUBNET = "${cfg.bridgeIp}/${toString cfg.bridgePrefixLength}";
|
|
};
|
|
})
|
|
];
|
|
}
|