diff --git a/docs/swarm/README.md b/docs/swarm/README.md index 0d3376e2..e9c4666c 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -179,7 +179,9 @@ holds it in memory only. The one thing to configure is where the queue is: It reaches each container at start, as `HIVE_AGENT_NATS_URL` in `/etc/hyperhive-hive-facts`, so no agent build carries it. On every hive, `agentNatsUrl` defaults to the same -`tls://:` as the hive's own. On the queue's +`tls://:` as the hive's own. The option takes one +`://:` (a hostname or IPv4 address), not an IPv6 literal or a +list: evaluation refuses anything else. On the queue's host the name resolves inside a container to the bridge address, where the firewall opens the port; on any other hive it resolves through the host's DNS, like the store's name. ⚠️ **Never a loopback address here**: an agent has its diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index 92987475..175c3f63 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -477,7 +477,7 @@ in # Only the address lives here. Each agent reads its own credential from the # store (`nix/agent-modules/queue-identity.nix`). options.services.hyperhive.deploy.hive-controller.queue.agentNatsUrl = lib.mkOption { - type = lib.types.nullOr lib.types.str; + type = lib.types.nullOr (lib.types.strMatching "[a-z]{1,10}://[A-Za-z0-9._-]{1,200}:[0-9]{1,5}"); default = "tls://${swarmCfg.nats.domain}:${toString swarmCfg.nats.port}"; defaultText = lib.literalExpression ''"tls://''${swarm.nats.domain}:''${swarm.nats.port}"''; example = "tls://nats.example.com:4222"; @@ -492,6 +492,10 @@ in agent's network namespace `127.0.0.1` is the agent, not this host, and the queue's certificate names no address anyway. + One `://:`: a hostname or IPv4 address, no IPv6 literal and no + list. The value reaches the container as `HIVE_AGENT_NATS_URL`, whose + character set hive-priv enforces at container start. + Null means this hive's agents have not been given the queue's address, and the harness is handed none. ''; diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index eb5ab353..38d66ad8 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -308,6 +308,25 @@ let name = "a hive with no agent queue address forwards none"; ok = !(noAgentQueue.systemd.services.hive-c0re.environment ? HIVE_AGENT_NATS_URL); } + { + # hive-priv refuses a value outside the facts' character set at container + # start, so the option refuses an IPv6 literal or a list at evaluation. + # Only the option's own value is forced, not the rest of either host. The + # default (`allLocal`) is the control that the lookup accepts an address. + name = "the agents' queue address option takes one scheme://host:port and nothing else"; + ok = + let + accepts = + host: (builtins.tryEval host.services.hyperhive.deploy.hive-controller.queue.agentNatsUrl).success; + in + accepts allLocal + && !(accepts (hiveWithController { + deploy.hive-controller.queue.agentNatsUrl = "tls://[fd00::1]:4222"; + })) + && !(accepts (hiveWithController { + deploy.hive-controller.queue.agentNatsUrl = "tls://a.t.local:4222,tls://b.t.local:4222"; + })); + } { # Control: `bare` sets none of them, and its serve.json carries the # dashboard port, so the key lookup reads a real document.