feat(#1971): network.exposeHostPorts to reach host-loopback services from agents
This commit is contained in:
parent
baa5ce3e1a
commit
0172106559
2 changed files with 94 additions and 1 deletions
|
|
@ -100,6 +100,27 @@ When `isolateContainers = true`, `allowedTCPPorts` is extended with
|
||||||
shared host netns) for the forge sub-domain, per-agent UI proxies,
|
shared host netns) for the forge sub-domain, per-agent UI proxies,
|
||||||
and any other HTTP services.
|
and any other HTTP services.
|
||||||
|
|
||||||
|
### Reaching host-loopback services (`exposeHostPorts`)
|
||||||
|
|
||||||
|
Agents are deliberately cut off from the host loopback (the bridge→
|
||||||
|
`127.0.0.0/8` DROP rule below), so a service that only binds
|
||||||
|
`127.0.0.1` on the host — e.g. a dev OTEL collector for
|
||||||
|
`services.hyperhive.otel.endpoint` — is unreachable by default.
|
||||||
|
|
||||||
|
`services.hyperhive.network.exposeHostPorts = [ 4318 ];` opens a
|
||||||
|
controlled path for each listed TCP port `P`:
|
||||||
|
|
||||||
|
- a socket-activated `systemd-socket-proxyd` (`hive-hostport-<P>`)
|
||||||
|
listens on `<bridgeIp>:P` and forwards to `127.0.0.1:P`;
|
||||||
|
- `P` is added to the bridge-interface `allowedTCPPorts`.
|
||||||
|
|
||||||
|
The agent then points at `http://<bridgeIp>:P` (default
|
||||||
|
`http://10.42.0.1:4318`). This keeps the loopback DROP rule intact:
|
||||||
|
agents only ever connect to the bridge IP, and the host's own proxy
|
||||||
|
process is what dials `127.0.0.1`. The port is reachable by **every**
|
||||||
|
agent on the bridge subnet (like DNS/gateway), so only expose services
|
||||||
|
safe for any agent to reach.
|
||||||
|
|
||||||
## Container isolation
|
## Container isolation
|
||||||
|
|
||||||
`services.hyperhive.network.isolateContainers` (default `false`) flips
|
`services.hyperhive.network.isolateContainers` (default `false`) flips
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,30 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exposeHostPorts = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.port;
|
||||||
|
default = [ ];
|
||||||
|
example = [ 4318 ];
|
||||||
|
description = ''
|
||||||
|
TCP ports on the host's loopback (`127.0.0.1`) to expose to agent
|
||||||
|
containers at the bridge IP (`bridgeIp`). For each port `P`, a
|
||||||
|
socket-activated `systemd-socket-proxyd` listens on
|
||||||
|
`''${bridgeIp}:P` and forwards to `127.0.0.1:P`, and `P` is opened
|
||||||
|
on the bridge-interface firewall.
|
||||||
|
|
||||||
|
Use this to let agents reach a host-local service that only binds
|
||||||
|
loopback — e.g. an OpenTelemetry collector for
|
||||||
|
`services.hyperhive.otel.endpoint`. The agent points at
|
||||||
|
`http://''${bridgeIp}:P` (default `http://10.42.0.1:P`).
|
||||||
|
|
||||||
|
This does NOT weaken the bridge→loopback DROP rule (defence-in-depth):
|
||||||
|
agents never reach `127.0.0.0/8`; they connect to the bridge IP and
|
||||||
|
the host's own proxy process dials loopback. The exposed port is
|
||||||
|
reachable by EVERY agent on the bridge subnet (same as DNS/gateway),
|
||||||
|
so only expose services that are safe for any agent to reach.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
isolateContainers = lib.mkOption {
|
isolateContainers = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
|
|
@ -205,10 +229,12 @@ in
|
||||||
# Allow isolated agents to reach the gateway (nginx on the host, shared
|
# Allow isolated agents to reach the gateway (nginx on the host, shared
|
||||||
# netns). Port 80 covers `http://forge.<domain>`, per-agent UI proxies,
|
# netns). Port 80 covers `http://forge.<domain>`, per-agent UI proxies,
|
||||||
# and any other HTTP services the gateway fronts. Port 443 for HTTPS.
|
# and any other HTTP services the gateway fronts. Port 443 for HTTPS.
|
||||||
|
# `exposeHostPorts` adds any operator-declared host-loopback proxies.
|
||||||
networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = [
|
networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = [
|
||||||
80
|
80
|
||||||
443
|
443
|
||||||
];
|
]
|
||||||
|
++ cfg.exposeHostPorts;
|
||||||
|
|
||||||
# Tells hive-c0re to pass PRIVATE_NETWORK + bridge settings to each
|
# Tells hive-c0re to pass PRIVATE_NETWORK + bridge settings to each
|
||||||
# container. HIVE_NETWORK_SUBNET is host-bridge IP/prefix, not canonical
|
# container. HIVE_NETWORK_SUBNET is host-bridge IP/prefix, not canonical
|
||||||
|
|
@ -220,6 +246,52 @@ in
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Host-loopback port exposure: one socket-activated systemd-socket-proxyd
|
||||||
|
# per `exposeHostPorts` entry, listening on the bridge IP and forwarding to
|
||||||
|
# the host's loopback. This is how agents reach a host-local service (e.g. a
|
||||||
|
# dev OTEL collector on 127.0.0.1) without weakening the bridge→loopback
|
||||||
|
# DROP rule above — agents connect to the bridge IP, and the host's own
|
||||||
|
# proxy process is what dials 127.0.0.1. See docs/network.md.
|
||||||
|
(lib.mkIf (config.services.hyperhive.enable && cfg.exposeHostPorts != [ ]) {
|
||||||
|
systemd.sockets = lib.listToAttrs (
|
||||||
|
map (
|
||||||
|
p:
|
||||||
|
lib.nameValuePair "hive-hostport-${toString p}" {
|
||||||
|
description = "Host-loopback proxy socket for port ${toString p} (bridge→127.0.0.1)";
|
||||||
|
wantedBy = [ "sockets.target" ];
|
||||||
|
socketConfig.ListenStream = "${cfg.bridgeIp}:${toString p}";
|
||||||
|
}
|
||||||
|
) cfg.exposeHostPorts
|
||||||
|
);
|
||||||
|
|
||||||
|
systemd.services = lib.listToAttrs (
|
||||||
|
map (
|
||||||
|
p:
|
||||||
|
lib.nameValuePair "hive-hostport-${toString p}" {
|
||||||
|
description = "Host-loopback proxy for port ${toString p} (bridge→127.0.0.1)";
|
||||||
|
requires = [ "hive-hostport-${toString p}.socket" ];
|
||||||
|
after = [ "hive-hostport-${toString p}.socket" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${config.systemd.package}/lib/systemd/systemd-socket-proxyd 127.0.0.1:${toString p}";
|
||||||
|
SyslogIdentifier = "hive-hostport-${toString p}";
|
||||||
|
# Pure userspace TCP proxy: no filesystem, no privileges, no extra
|
||||||
|
# address families beyond inet/unix. DynamicUser keeps it unprivileged.
|
||||||
|
DynamicUser = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
RestrictAddressFamilies = [
|
||||||
|
"AF_INET"
|
||||||
|
"AF_INET6"
|
||||||
|
"AF_UNIX"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) cfg.exposeHostPorts
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
# Deprecation surface for the removed toggles. Both options are kept so
|
# Deprecation surface for the removed toggles. Both options are kept so
|
||||||
# existing configs that set them to `true` still eval cleanly; setting
|
# existing configs that set them to `true` still eval cleanly; setting
|
||||||
# either to `false` no longer does anything (network + isolation are
|
# either to `false` no longer does anything (network + isolation are
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue