nix/ec-node: auto-bootstrap web bridge from local control peers

This commit is contained in:
every.channel 2026-02-22 23:38:17 -08:00
parent 2778715304
commit c9996dd5ad
No known key found for this signature in database
4 changed files with 236 additions and 4 deletions

View file

@ -144,6 +144,51 @@ in
default = [ ];
description = "Optional iroh endpoint addresses to seed control gossip joins.";
};
bridgeWeb = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Run `ec-node control-bridge-web` to upsert announced streams into the web directory.";
};
directoryUrl = lib.mkOption {
type = lib.types.str;
default = "https://every.channel";
description = "Directory base URL used by control-bridge-web for `/api/stream-upsert`.";
};
authTokenFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Optional file containing bearer token for `/api/stream-upsert`.";
};
timeoutMs = lib.mkOption {
type = lib.types.ints.nonnegative;
default = 30000;
description = "Bridge run timeout; service restarts and refreshes gossip peers after this window (0 = run forever).";
};
maxAgeMs = lib.mkOption {
type = lib.types.ints.positive;
default = 60000;
description = "Maximum accepted staleness for control announcements consumed by the web bridge.";
};
streamPrefix = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Optional stream-id prefix filter for web upserts.";
};
discovery = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "dht,mdns,dns";
description = "Optional discovery mode override for the web bridge; falls back to `control.discovery` when unset.";
};
};
};
broadcasts = lib.mkOption {
@ -195,7 +240,7 @@ in
];
systemd.tmpfiles.rules = [
"d /run/every-channel 0755 root root - -"
"d /run/every-channel 1777 root root - -"
];
systemd.services =
@ -227,6 +272,7 @@ in
"cmd+=(${lib.concatStringsSep " " (map lib.escapeShellArg cfg.extraArgs)})";
explicitInputStr = if b.input == null then "" else b.input;
channelStr = if b.channel == null then "" else b.channel;
controlEndpointOutPath = "/run/every-channel/control-peer-${sanitizeUnitName b.name}.json";
controlDiscoveryStr = if cfg.control.discovery == null then "" else cfg.control.discovery;
controlIrohSecretStr = if cfg.control.irohSecret == null then "" else cfg.control.irohSecret;
controlGossipPeerLines = lib.concatMapStrings (peer: "cmd+=(--gossip-peer ${lib.escapeShellArg peer})\n") cfg.control.gossipPeers;
@ -356,6 +402,7 @@ in
if [[ -n "$control_iroh_secret" ]]; then
cmd+=(--iroh-secret "$control_iroh_secret")
fi
cmd+=(--control-endpoint-addr-out ${lib.escapeShellArg controlEndpointOutPath})
${controlGossipPeerLines}
''}
${extraArgsLine}
@ -407,11 +454,125 @@ in
RestrictSUIDSGID = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
ReadWritePaths = lib.optionals cfg.control.enable [ "/run/every-channel" ];
};
environment = cfg.environment;
};
})
cfg.broadcasts);
cfg.broadcasts)
// lib.optionalAttrs (cfg.control.enable && cfg.control.bridgeWeb.enable)
(let
bridgeUnit = "every-channel-control-bridge-web";
bridgePeerFiles = map (b: "/run/every-channel/control-peer-${sanitizeUnitName b.name}.json") cfg.broadcasts;
bridgeDiscoveryStr =
if cfg.control.bridgeWeb.discovery != null then cfg.control.bridgeWeb.discovery
else if cfg.control.discovery != null then cfg.control.discovery
else "";
bridgeStreamPrefixStr = if cfg.control.bridgeWeb.streamPrefix == null then "" else cfg.control.bridgeWeb.streamPrefix;
bridgeAuthTokenFile = if cfg.control.bridgeWeb.authTokenFile == null then "" else cfg.control.bridgeWeb.authTokenFile;
staticGossipPeerLines = lib.concatMapStrings (peer: ''
cmd+=(--gossip-peer ${lib.escapeShellArg peer})
have_peer=1
'') cfg.control.gossipPeers;
peerFileLoopLines = lib.concatMapStrings (peerFile: ''
if [[ -s ${lib.escapeShellArg peerFile} ]]; then
peer="$(tr -d '\r\n' < ${lib.escapeShellArg peerFile})"
if [[ -n "$peer" ]]; then
cmd+=(--gossip-peer "$peer")
have_peer=1
fi
fi
'') bridgePeerFiles;
bridgeRunner = pkgs.writeShellApplication {
name = bridgeUnit;
runtimeInputs = [
pkgs.coreutils
cfg.package
];
text = ''
set -euo pipefail
directory_url=${lib.escapeShellArg cfg.control.bridgeWeb.directoryUrl}
bridge_discovery=${lib.escapeShellArg bridgeDiscoveryStr}
stream_prefix=${lib.escapeShellArg bridgeStreamPrefixStr}
auth_token_file=${lib.escapeShellArg bridgeAuthTokenFile}
while true; do
cmd=(
${lib.escapeShellArg "${cfg.package}/bin/ec-node"}
control-bridge-web
--directory-url "$directory_url"
--timeout-ms ${toString cfg.control.bridgeWeb.timeoutMs}
--max-age-ms ${toString cfg.control.bridgeWeb.maxAgeMs}
)
if [[ -n "$bridge_discovery" ]]; then
cmd+=(--discovery "$bridge_discovery")
fi
if [[ -n "$stream_prefix" ]]; then
cmd+=(--stream-prefix "$stream_prefix")
fi
if [[ -n "$auth_token_file" && -r "$auth_token_file" ]]; then
token="$(tr -d '\r\n' < "$auth_token_file")"
if [[ -n "$token" ]]; then
cmd+=(--auth-token "$token")
fi
fi
have_peer=0
${peerFileLoopLines}
${staticGossipPeerLines}
if [[ "$have_peer" -eq 0 ]]; then
sleep 2
continue
fi
"''${cmd[@]}" || true
sleep 2
done
'';
};
in
{
"${bridgeUnit}" = {
description = "every.channel control bridge to web directory";
wantedBy = [ "multi-user.target" ];
after =
[ "network-online.target" ]
++ map (b: "every-channel-wt-publish-${sanitizeUnitName b.name}.service") cfg.broadcasts;
wants =
[ "network-online.target" ]
++ map (b: "every-channel-wt-publish-${sanitizeUnitName b.name}.service") cfg.broadcasts;
unitConfig = {
StartLimitIntervalSec = 0;
};
serviceConfig = {
Type = "simple";
ExecStart = "${bridgeRunner}/bin/${bridgeUnit}";
Restart = "always";
RestartSec = 2;
DynamicUser = true;
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictSUIDSGID = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
};
environment = cfg.environment;
};
});
};
}