@forgezero/agent 0.1.79 → 0.1.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -6
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +21 -6
- package/dist/deploy-compiler.js +50 -2
- package/dist/deploy-plan-runner.js +50 -2
- package/dist/deploy-plan.js +50 -2
- package/dist/deploy.d.ts +37 -1
- package/dist/deploy.js +1 -0
- package/dist/deployment-connectivity.d.ts +26 -0
- package/dist/deployment-connectivity.js +128 -1
- package/dist/deployment-targets.js +15 -1
- package/dist/deployment-topology.d.ts +46 -0
- package/dist/deployment-topology.js +113 -0
- package/dist/deployment.d.ts +40 -0
- package/dist/fz-agent.js +368 -14
- package/dist/fz.js +75 -12
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +21 -6
- package/dist/platform-bootstrap-runtime.d.ts +4 -1
- package/dist/platform-bootstrap-runtime.js +18 -4
- package/dist/platform-fleet-verification.js +147 -6
- package/dist/provision.js +129 -2
- package/dist/software-helper.js +128 -1
- package/dist/version.d.ts +1 -1
- package/package.json +5 -1
package/dist/provision.js
CHANGED
|
@@ -1649,6 +1649,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
1649
1649
|
// src/deployment-connectivity.ts
|
|
1650
1650
|
import { createHash as createHash3 } from "node:crypto";
|
|
1651
1651
|
import { mkdirSync as mkdirSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1652
|
+
import { createConnection } from "node:net";
|
|
1652
1653
|
import { dirname as dirname4 } from "node:path";
|
|
1653
1654
|
|
|
1654
1655
|
// src/process-input.ts
|
|
@@ -1701,6 +1702,21 @@ var defaultHost = {
|
|
|
1701
1702
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
1702
1703
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
1703
1704
|
},
|
|
1705
|
+
probe: (address, port, timeoutMs) => new Promise((resolve3) => {
|
|
1706
|
+
const socket = createConnection({ host: address, port });
|
|
1707
|
+
let settled = false;
|
|
1708
|
+
const finish = (value) => {
|
|
1709
|
+
if (settled)
|
|
1710
|
+
return;
|
|
1711
|
+
settled = true;
|
|
1712
|
+
socket.destroy();
|
|
1713
|
+
resolve3(value);
|
|
1714
|
+
};
|
|
1715
|
+
socket.setTimeout(timeoutMs);
|
|
1716
|
+
socket.once("connect", () => finish(true));
|
|
1717
|
+
socket.once("timeout", () => finish(false));
|
|
1718
|
+
socket.once("error", () => finish(false));
|
|
1719
|
+
}),
|
|
1704
1720
|
sleep: (ms) => Bun.sleep(ms)
|
|
1705
1721
|
};
|
|
1706
1722
|
function validate(request) {
|
|
@@ -1715,7 +1731,7 @@ function validate(request) {
|
|
|
1715
1731
|
} else if (request.capabilities.public)
|
|
1716
1732
|
throw new Error("unexpected public deployment capability");
|
|
1717
1733
|
if (privateIntent?.mode === "cloudflare-warp") {
|
|
1718
|
-
if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
|
|
1734
|
+
if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !UUID.test(request.capabilities.private.siteRouteId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
|
|
1719
1735
|
throw new Error("private deployment connectivity is malformed");
|
|
1720
1736
|
}
|
|
1721
1737
|
} else if (request.capabilities.private)
|
|
@@ -1725,6 +1741,22 @@ async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
|
1725
1741
|
validate(request);
|
|
1726
1742
|
const id = idFor(request.key);
|
|
1727
1743
|
const evidence = { key: request.key };
|
|
1744
|
+
const topology = request.topology;
|
|
1745
|
+
if (topology) {
|
|
1746
|
+
const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
|
|
1747
|
+
const identities = [
|
|
1748
|
+
topology.nodeIdentity,
|
|
1749
|
+
...topology.localPeerIdentities,
|
|
1750
|
+
...topology.remoteRelayIdentities,
|
|
1751
|
+
...topology.memberIdentities
|
|
1752
|
+
];
|
|
1753
|
+
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
|
|
1754
|
+
throw new Error("deployment topology is malformed");
|
|
1755
|
+
}
|
|
1756
|
+
if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
|
|
1757
|
+
throw new Error("deployment topology transport differs from private connectivity intent");
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1728
1760
|
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
1729
1761
|
const capability = request.capabilities.public;
|
|
1730
1762
|
const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
|
|
@@ -1762,6 +1794,12 @@ WantedBy=multi-user.target
|
|
|
1762
1794
|
const capability = request.capabilities.private;
|
|
1763
1795
|
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
1764
1796
|
const unit = "forgezero-deployment-mesh.service";
|
|
1797
|
+
const forwarding = "/etc/sysctl.d/99-forgezero-mesh.conf";
|
|
1798
|
+
host.write(forwarding, `net.ipv4.ip_forward = 1
|
|
1799
|
+
net.ipv6.conf.all.forwarding = 1
|
|
1800
|
+
net.ipv6.conf.all.accept_ra = 2
|
|
1801
|
+
`, 420);
|
|
1802
|
+
await checked2(host, ["/usr/sbin/sysctl", "-p", forwarding], "Mesh subnet forwarding");
|
|
1765
1803
|
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
1766
1804
|
host.write(`/etc/systemd/system/${unit}`, `[Unit]
|
|
1767
1805
|
Description=ForgeZero deployment Mesh/WARP connector
|
|
@@ -1796,6 +1834,95 @@ WantedBy=multi-user.target
|
|
|
1796
1834
|
if (!evidence.private)
|
|
1797
1835
|
throw new Error("WARP connector readiness failed");
|
|
1798
1836
|
}
|
|
1837
|
+
if (topology) {
|
|
1838
|
+
const syncAddresses = topology.role === "relay" ? [...topology.localPeerAddresses, ...topology.remoteRelayAddresses] : [topology.localRelayAddress];
|
|
1839
|
+
const topologyEpoch = `topology-${topology.generation.slice("sha256:".length, "sha256:".length + 48)}`;
|
|
1840
|
+
host.write("/etc/forgezero/deployment-topology.env", [
|
|
1841
|
+
`FZ_TOPOLOGY_NODE_IDENTITY=${topology.nodeIdentity}`,
|
|
1842
|
+
`FZ_TOPOLOGY_PEER_ADDRESSES=${[...new Set(syncAddresses)].join(",")}`,
|
|
1843
|
+
`FZ_TOPOLOGY_MEMBERS=${topology.memberIdentities.join(",")}`,
|
|
1844
|
+
`FZ_TOPOLOGY_EPOCH=${topologyEpoch}`,
|
|
1845
|
+
`FZ_TOPOLOGY_SITE=${topology.site}`,
|
|
1846
|
+
`FZ_TOPOLOGY_ROLE=${topology.role}`,
|
|
1847
|
+
`FZ_TOPOLOGY_TRANSPORT=${topology.transport}`,
|
|
1848
|
+
""
|
|
1849
|
+
].join(`
|
|
1850
|
+
`), 384);
|
|
1851
|
+
const routeUnit = "forgezero-deployment-topology.service";
|
|
1852
|
+
const routeUnitPath = `/etc/systemd/system/${routeUnit}`;
|
|
1853
|
+
const routePairs = topology.role === "member" ? topology.remoteSiteCidrs.map((network) => ({ network, via: topology.localRelayAddress })) : topology.transport === "private-lan" ? topology.remoteSiteCidrs.map((network, index) => ({ network, via: topology.remoteRelayAddresses[index] })) : [];
|
|
1854
|
+
for (const { via } of routePairs) {
|
|
1855
|
+
await checked2(host, ["/usr/sbin/ip", "route", "get", via], `private route gateway ${via}`);
|
|
1856
|
+
}
|
|
1857
|
+
await host.exec(["/usr/bin/systemctl", "disable", "--now", routeUnit]);
|
|
1858
|
+
const forwarding = topology.role !== "member" && topology.remoteSiteCidrs.length > 0 ? `ExecStart=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
|
|
1859
|
+
` : "";
|
|
1860
|
+
const starts = routePairs.map(({ network, via }) => `ExecStart=/usr/sbin/ip route replace ${network} via ${via}`).join(`
|
|
1861
|
+
`);
|
|
1862
|
+
const stops = routePairs.map(({ network, via }) => `ExecStop=-/usr/sbin/ip route del ${network} via ${via}`).join(`
|
|
1863
|
+
`);
|
|
1864
|
+
const noOp = !forwarding && !starts ? `ExecStart=/usr/bin/true
|
|
1865
|
+
` : "";
|
|
1866
|
+
host.write(routeUnitPath, `[Unit]
|
|
1867
|
+
Description=ForgeZero fenced deployment site routing
|
|
1868
|
+
After=network-online.target${topology.transport === "cloudflare-warp" ? " forgezero-deployment-mesh.service" : ""}
|
|
1869
|
+
Wants=network-online.target
|
|
1870
|
+
|
|
1871
|
+
[Service]
|
|
1872
|
+
Type=oneshot
|
|
1873
|
+
RemainAfterExit=yes
|
|
1874
|
+
${forwarding}${noOp}${starts}${starts ? `
|
|
1875
|
+
` : ""}${stops}${stops ? `
|
|
1876
|
+
` : ""}
|
|
1877
|
+
[Install]
|
|
1878
|
+
WantedBy=multi-user.target
|
|
1879
|
+
`, 420);
|
|
1880
|
+
for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
|
|
1881
|
+
for (const port of topology.routedTcpPorts) {
|
|
1882
|
+
await checked2(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (topology.role !== "member")
|
|
1886
|
+
for (const remoteCidr of topology.remoteSiteCidrs) {
|
|
1887
|
+
for (const port of topology.routedTcpPorts) {
|
|
1888
|
+
await checked2(host, [
|
|
1889
|
+
"/usr/sbin/ufw",
|
|
1890
|
+
"route",
|
|
1891
|
+
"allow",
|
|
1892
|
+
"proto",
|
|
1893
|
+
"tcp",
|
|
1894
|
+
"from",
|
|
1895
|
+
topology.siteCidr,
|
|
1896
|
+
"to",
|
|
1897
|
+
remoteCidr,
|
|
1898
|
+
"port",
|
|
1899
|
+
String(port)
|
|
1900
|
+
], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
|
|
1904
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
|
|
1905
|
+
const probes = topology.role === "member" ? [topology.localRelayAddress] : topology.remoteRelayAddresses;
|
|
1906
|
+
for (const address of [...new Set(probes)]) {
|
|
1907
|
+
await checked2(host, ["/usr/sbin/ip", "route", "get", address], `private route ${address}`);
|
|
1908
|
+
let ready = false;
|
|
1909
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
1910
|
+
if (await host.probe(address, topology.healthPort, 2000)) {
|
|
1911
|
+
ready = true;
|
|
1912
|
+
break;
|
|
1913
|
+
}
|
|
1914
|
+
await host.sleep(1000);
|
|
1915
|
+
}
|
|
1916
|
+
if (!ready)
|
|
1917
|
+
throw new Error(`private topology relay ${address}:${topology.healthPort} is unreachable`);
|
|
1918
|
+
}
|
|
1919
|
+
evidence.topology = {
|
|
1920
|
+
site: topology.site,
|
|
1921
|
+
siteCidr: topology.siteCidr,
|
|
1922
|
+
role: topology.role,
|
|
1923
|
+
probed: [...new Set(probes)]
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1799
1926
|
return evidence;
|
|
1800
1927
|
}
|
|
1801
1928
|
|
|
@@ -2520,7 +2647,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2520
2647
|
}
|
|
2521
2648
|
|
|
2522
2649
|
// src/version.ts
|
|
2523
|
-
var VERSION3 = "0.1.
|
|
2650
|
+
var VERSION3 = "0.1.80";
|
|
2524
2651
|
|
|
2525
2652
|
// src/egress-policy.ts
|
|
2526
2653
|
import { realpathSync as realpathSync3 } from "node:fs";
|
package/dist/software-helper.js
CHANGED
|
@@ -971,6 +971,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
971
971
|
// src/deployment-connectivity.ts
|
|
972
972
|
import { createHash as createHash2 } from "node:crypto";
|
|
973
973
|
import { mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
974
|
+
import { createConnection } from "node:net";
|
|
974
975
|
import { dirname as dirname2 } from "node:path";
|
|
975
976
|
|
|
976
977
|
// src/process-input.ts
|
|
@@ -1023,6 +1024,21 @@ var defaultHost = {
|
|
|
1023
1024
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
1024
1025
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
1025
1026
|
},
|
|
1027
|
+
probe: (address, port, timeoutMs) => new Promise((resolve) => {
|
|
1028
|
+
const socket = createConnection({ host: address, port });
|
|
1029
|
+
let settled = false;
|
|
1030
|
+
const finish = (value) => {
|
|
1031
|
+
if (settled)
|
|
1032
|
+
return;
|
|
1033
|
+
settled = true;
|
|
1034
|
+
socket.destroy();
|
|
1035
|
+
resolve(value);
|
|
1036
|
+
};
|
|
1037
|
+
socket.setTimeout(timeoutMs);
|
|
1038
|
+
socket.once("connect", () => finish(true));
|
|
1039
|
+
socket.once("timeout", () => finish(false));
|
|
1040
|
+
socket.once("error", () => finish(false));
|
|
1041
|
+
}),
|
|
1026
1042
|
sleep: (ms) => Bun.sleep(ms)
|
|
1027
1043
|
};
|
|
1028
1044
|
function validate(request) {
|
|
@@ -1037,7 +1053,7 @@ function validate(request) {
|
|
|
1037
1053
|
} else if (request.capabilities.public)
|
|
1038
1054
|
throw new Error("unexpected public deployment capability");
|
|
1039
1055
|
if (privateIntent?.mode === "cloudflare-warp") {
|
|
1040
|
-
if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
|
|
1056
|
+
if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !UUID.test(request.capabilities.private.siteRouteId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
|
|
1041
1057
|
throw new Error("private deployment connectivity is malformed");
|
|
1042
1058
|
}
|
|
1043
1059
|
} else if (request.capabilities.private)
|
|
@@ -1047,6 +1063,22 @@ async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
|
1047
1063
|
validate(request);
|
|
1048
1064
|
const id = idFor(request.key);
|
|
1049
1065
|
const evidence = { key: request.key };
|
|
1066
|
+
const topology = request.topology;
|
|
1067
|
+
if (topology) {
|
|
1068
|
+
const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
|
|
1069
|
+
const identities = [
|
|
1070
|
+
topology.nodeIdentity,
|
|
1071
|
+
...topology.localPeerIdentities,
|
|
1072
|
+
...topology.remoteRelayIdentities,
|
|
1073
|
+
...topology.memberIdentities
|
|
1074
|
+
];
|
|
1075
|
+
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
|
|
1076
|
+
throw new Error("deployment topology is malformed");
|
|
1077
|
+
}
|
|
1078
|
+
if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
|
|
1079
|
+
throw new Error("deployment topology transport differs from private connectivity intent");
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1050
1082
|
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
1051
1083
|
const capability = request.capabilities.public;
|
|
1052
1084
|
const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
|
|
@@ -1084,6 +1116,12 @@ WantedBy=multi-user.target
|
|
|
1084
1116
|
const capability = request.capabilities.private;
|
|
1085
1117
|
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
1086
1118
|
const unit = "forgezero-deployment-mesh.service";
|
|
1119
|
+
const forwarding = "/etc/sysctl.d/99-forgezero-mesh.conf";
|
|
1120
|
+
host.write(forwarding, `net.ipv4.ip_forward = 1
|
|
1121
|
+
net.ipv6.conf.all.forwarding = 1
|
|
1122
|
+
net.ipv6.conf.all.accept_ra = 2
|
|
1123
|
+
`, 420);
|
|
1124
|
+
await checked(host, ["/usr/sbin/sysctl", "-p", forwarding], "Mesh subnet forwarding");
|
|
1087
1125
|
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
1088
1126
|
host.write(`/etc/systemd/system/${unit}`, `[Unit]
|
|
1089
1127
|
Description=ForgeZero deployment Mesh/WARP connector
|
|
@@ -1118,6 +1156,95 @@ WantedBy=multi-user.target
|
|
|
1118
1156
|
if (!evidence.private)
|
|
1119
1157
|
throw new Error("WARP connector readiness failed");
|
|
1120
1158
|
}
|
|
1159
|
+
if (topology) {
|
|
1160
|
+
const syncAddresses = topology.role === "relay" ? [...topology.localPeerAddresses, ...topology.remoteRelayAddresses] : [topology.localRelayAddress];
|
|
1161
|
+
const topologyEpoch = `topology-${topology.generation.slice("sha256:".length, "sha256:".length + 48)}`;
|
|
1162
|
+
host.write("/etc/forgezero/deployment-topology.env", [
|
|
1163
|
+
`FZ_TOPOLOGY_NODE_IDENTITY=${topology.nodeIdentity}`,
|
|
1164
|
+
`FZ_TOPOLOGY_PEER_ADDRESSES=${[...new Set(syncAddresses)].join(",")}`,
|
|
1165
|
+
`FZ_TOPOLOGY_MEMBERS=${topology.memberIdentities.join(",")}`,
|
|
1166
|
+
`FZ_TOPOLOGY_EPOCH=${topologyEpoch}`,
|
|
1167
|
+
`FZ_TOPOLOGY_SITE=${topology.site}`,
|
|
1168
|
+
`FZ_TOPOLOGY_ROLE=${topology.role}`,
|
|
1169
|
+
`FZ_TOPOLOGY_TRANSPORT=${topology.transport}`,
|
|
1170
|
+
""
|
|
1171
|
+
].join(`
|
|
1172
|
+
`), 384);
|
|
1173
|
+
const routeUnit = "forgezero-deployment-topology.service";
|
|
1174
|
+
const routeUnitPath = `/etc/systemd/system/${routeUnit}`;
|
|
1175
|
+
const routePairs = topology.role === "member" ? topology.remoteSiteCidrs.map((network) => ({ network, via: topology.localRelayAddress })) : topology.transport === "private-lan" ? topology.remoteSiteCidrs.map((network, index) => ({ network, via: topology.remoteRelayAddresses[index] })) : [];
|
|
1176
|
+
for (const { via } of routePairs) {
|
|
1177
|
+
await checked(host, ["/usr/sbin/ip", "route", "get", via], `private route gateway ${via}`);
|
|
1178
|
+
}
|
|
1179
|
+
await host.exec(["/usr/bin/systemctl", "disable", "--now", routeUnit]);
|
|
1180
|
+
const forwarding = topology.role !== "member" && topology.remoteSiteCidrs.length > 0 ? `ExecStart=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
|
|
1181
|
+
` : "";
|
|
1182
|
+
const starts = routePairs.map(({ network, via }) => `ExecStart=/usr/sbin/ip route replace ${network} via ${via}`).join(`
|
|
1183
|
+
`);
|
|
1184
|
+
const stops = routePairs.map(({ network, via }) => `ExecStop=-/usr/sbin/ip route del ${network} via ${via}`).join(`
|
|
1185
|
+
`);
|
|
1186
|
+
const noOp = !forwarding && !starts ? `ExecStart=/usr/bin/true
|
|
1187
|
+
` : "";
|
|
1188
|
+
host.write(routeUnitPath, `[Unit]
|
|
1189
|
+
Description=ForgeZero fenced deployment site routing
|
|
1190
|
+
After=network-online.target${topology.transport === "cloudflare-warp" ? " forgezero-deployment-mesh.service" : ""}
|
|
1191
|
+
Wants=network-online.target
|
|
1192
|
+
|
|
1193
|
+
[Service]
|
|
1194
|
+
Type=oneshot
|
|
1195
|
+
RemainAfterExit=yes
|
|
1196
|
+
${forwarding}${noOp}${starts}${starts ? `
|
|
1197
|
+
` : ""}${stops}${stops ? `
|
|
1198
|
+
` : ""}
|
|
1199
|
+
[Install]
|
|
1200
|
+
WantedBy=multi-user.target
|
|
1201
|
+
`, 420);
|
|
1202
|
+
for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
|
|
1203
|
+
for (const port of topology.routedTcpPorts) {
|
|
1204
|
+
await checked(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (topology.role !== "member")
|
|
1208
|
+
for (const remoteCidr of topology.remoteSiteCidrs) {
|
|
1209
|
+
for (const port of topology.routedTcpPorts) {
|
|
1210
|
+
await checked(host, [
|
|
1211
|
+
"/usr/sbin/ufw",
|
|
1212
|
+
"route",
|
|
1213
|
+
"allow",
|
|
1214
|
+
"proto",
|
|
1215
|
+
"tcp",
|
|
1216
|
+
"from",
|
|
1217
|
+
topology.siteCidr,
|
|
1218
|
+
"to",
|
|
1219
|
+
remoteCidr,
|
|
1220
|
+
"port",
|
|
1221
|
+
String(port)
|
|
1222
|
+
], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
|
|
1226
|
+
await checked(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
|
|
1227
|
+
const probes = topology.role === "member" ? [topology.localRelayAddress] : topology.remoteRelayAddresses;
|
|
1228
|
+
for (const address of [...new Set(probes)]) {
|
|
1229
|
+
await checked(host, ["/usr/sbin/ip", "route", "get", address], `private route ${address}`);
|
|
1230
|
+
let ready = false;
|
|
1231
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
1232
|
+
if (await host.probe(address, topology.healthPort, 2000)) {
|
|
1233
|
+
ready = true;
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
await host.sleep(1000);
|
|
1237
|
+
}
|
|
1238
|
+
if (!ready)
|
|
1239
|
+
throw new Error(`private topology relay ${address}:${topology.healthPort} is unreachable`);
|
|
1240
|
+
}
|
|
1241
|
+
evidence.topology = {
|
|
1242
|
+
site: topology.site,
|
|
1243
|
+
siteCidr: topology.siteCidr,
|
|
1244
|
+
role: topology.role,
|
|
1245
|
+
probed: [...new Set(probes)]
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1121
1248
|
return evidence;
|
|
1122
1249
|
}
|
|
1123
1250
|
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.80";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgezero/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.80",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check": "tsc --noEmit",
|
|
@@ -88,6 +88,10 @@
|
|
|
88
88
|
"types": "./dist/deployment-targets.d.ts",
|
|
89
89
|
"default": "./dist/deployment-targets.js"
|
|
90
90
|
},
|
|
91
|
+
"./deployment-topology": {
|
|
92
|
+
"types": "./dist/deployment-topology.d.ts",
|
|
93
|
+
"default": "./dist/deployment-topology.js"
|
|
94
|
+
},
|
|
91
95
|
"./deployment-connectivity": {
|
|
92
96
|
"types": "./dist/deployment-connectivity.d.ts",
|
|
93
97
|
"default": "./dist/deployment-connectivity.js"
|