@forgezero/agent 0.1.102 → 0.1.107

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.
@@ -1026,7 +1026,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
1026
1026
  // src/deployment-connectivity.ts
1027
1027
  import { createHash as createHash2 } from "node:crypto";
1028
1028
  import { mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
1029
- import { createConnection } from "node:net";
1029
+ import { createConnection, isIP } from "node:net";
1030
1030
  import { dirname as dirname2 } from "node:path";
1031
1031
 
1032
1032
  // src/process-input.ts
@@ -1113,21 +1113,99 @@ function validate(request) {
1113
1113
  }
1114
1114
  } else if (request.capabilities.private)
1115
1115
  throw new Error("unexpected private deployment capability");
1116
+ if (request.firewallPolicy) {
1117
+ const policy = request.firewallPolicy;
1118
+ if (!/^sha256:[a-f0-9]{64}$/.test(policy.generation) || !Array.isArray(policy.rules) || policy.rules.length > 256) {
1119
+ throw new Error("deployment firewall policy is malformed");
1120
+ }
1121
+ let expanded = 0;
1122
+ for (const rule of policy.rules) {
1123
+ expanded += rule.sourceAddresses.length;
1124
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(rule.ruleKey) || !["allow", "deny"].includes(rule.action) || !["tcp", "udp"].includes(rule.protocol) || rule.sourceAddresses.length < 1 || rule.sourceAddresses.length > 1024 || new Set(rule.sourceAddresses).size !== rule.sourceAddresses.length || rule.sourceAddresses.some((address) => isIP(address) !== 4) || !Number.isSafeInteger(rule.portFrom) || rule.portFrom < 1 || rule.portFrom > 65535 || !Number.isSafeInteger(rule.portTo) || rule.portTo < rule.portFrom || rule.portTo > 65535 || !Number.isSafeInteger(rule.priority) || rule.priority < 0 || rule.priority > 1e6) {
1125
+ throw new Error("deployment firewall policy is malformed");
1126
+ }
1127
+ }
1128
+ if (expanded > 2048)
1129
+ throw new Error("deployment firewall policy expands beyond its rule limit");
1130
+ }
1131
+ }
1132
+ async function replaceTaggedUfwRules(host, comment) {
1133
+ const status = await checked(host, ["/usr/sbin/ufw", "status", "numbered"], "firewall inventory");
1134
+ const numbers = status.split(`
1135
+ `).flatMap((line) => {
1136
+ if (!line.includes(comment))
1137
+ return [];
1138
+ const match = line.match(/^\s*\[\s*(\d{1,6})\]/);
1139
+ return match ? [Number(match[1])] : [];
1140
+ }).filter((value) => Number.isSafeInteger(value) && value > 0).sort((left, right) => right - left);
1141
+ for (const number of numbers)
1142
+ await checked(host, ["/usr/sbin/ufw", "--force", "delete", String(number)], `stale firewall rule ${number}`);
1116
1143
  }
1117
1144
  async function applyDeploymentConnectivity(request, host = defaultHost) {
1118
1145
  validate(request);
1119
1146
  const id = idFor(request.key);
1120
1147
  const evidence = { key: request.key };
1148
+ if (request.firewallPolicy) {
1149
+ const policy = request.firewallPolicy;
1150
+ const comment = `fz-policy-${id}`;
1151
+ await replaceTaggedUfwRules(host, comment);
1152
+ const ordered = [...policy.rules].sort((left, right) => left.priority - right.priority || (left.action === right.action ? left.ruleKey.localeCompare(right.ruleKey) : left.action === "deny" ? -1 : 1));
1153
+ const commands = [];
1154
+ for (const rule of ordered)
1155
+ for (const source of rule.sourceAddresses)
1156
+ commands.push([
1157
+ "/usr/sbin/ufw",
1158
+ "insert",
1159
+ "1",
1160
+ rule.action,
1161
+ "from",
1162
+ source,
1163
+ "to",
1164
+ "any",
1165
+ "port",
1166
+ rule.portFrom === rule.portTo ? String(rule.portFrom) : `${rule.portFrom}:${rule.portTo}`,
1167
+ "proto",
1168
+ rule.protocol,
1169
+ "comment",
1170
+ comment
1171
+ ]);
1172
+ const ranges = new Map;
1173
+ for (const rule of ordered)
1174
+ ranges.set(`${rule.protocol}:${rule.portFrom}:${rule.portTo}`, rule);
1175
+ for (const range of ranges.values())
1176
+ commands.push([
1177
+ "/usr/sbin/ufw",
1178
+ "insert",
1179
+ "1",
1180
+ "deny",
1181
+ "to",
1182
+ "any",
1183
+ "port",
1184
+ range.portFrom === range.portTo ? String(range.portFrom) : `${range.portFrom}:${range.portTo}`,
1185
+ "proto",
1186
+ range.protocol,
1187
+ "comment",
1188
+ comment
1189
+ ]);
1190
+ for (const command of commands.toReversed())
1191
+ await checked(host, command, "deployment firewall rule");
1192
+ evidence.firewall = { generation: policy.generation, rules: commands.length, active: true };
1193
+ }
1121
1194
  const topology = request.topology;
1122
1195
  if (topology) {
1123
- const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
1196
+ const values = [
1197
+ topology.localRelayAddress,
1198
+ ...topology.localPeerAddresses,
1199
+ ...topology.remoteRelayAddresses,
1200
+ ...topology.remoteMemberAddresses
1201
+ ];
1124
1202
  const identities = [
1125
1203
  topology.nodeIdentity,
1126
1204
  ...topology.localPeerIdentities,
1127
1205
  ...topology.remoteRelayIdentities,
1128
1206
  ...topology.memberIdentities
1129
1207
  ];
1130
- 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") {
1208
+ if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => isIP(value) !== 4) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.remoteMemberAddresses.length > 1024 || new Set(topology.remoteMemberAddresses).size !== topology.remoteMemberAddresses.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") {
1131
1209
  throw new Error("deployment topology is malformed");
1132
1210
  }
1133
1211
  if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
@@ -1254,28 +1332,46 @@ ${forwarding}${noOp}${starts}${starts ? `
1254
1332
  [Install]
1255
1333
  WantedBy=multi-user.target
1256
1334
  `, 420);
1257
- for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
1335
+ const firewallComment = `fz-topology-${id}`;
1336
+ await replaceTaggedUfwRules(host, firewallComment);
1337
+ for (const source of [...new Set([...topology.localPeerAddresses, ...topology.remoteMemberAddresses])]) {
1258
1338
  for (const port of topology.routedTcpPorts) {
1259
- await checked(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
1339
+ await checked(host, [
1340
+ "/usr/sbin/ufw",
1341
+ "allow",
1342
+ "from",
1343
+ source,
1344
+ "to",
1345
+ "any",
1346
+ "port",
1347
+ String(port),
1348
+ "proto",
1349
+ "tcp",
1350
+ "comment",
1351
+ firewallComment
1352
+ ], `private service ${source}:${port}`);
1260
1353
  }
1261
1354
  }
1262
1355
  if (topology.role !== "member")
1263
- for (const remoteCidr of topology.remoteSiteCidrs) {
1264
- for (const port of topology.routedTcpPorts) {
1265
- await checked(host, [
1266
- "/usr/sbin/ufw",
1267
- "route",
1268
- "allow",
1269
- "proto",
1270
- "tcp",
1271
- "from",
1272
- topology.siteCidr,
1273
- "to",
1274
- remoteCidr,
1275
- "port",
1276
- String(port)
1277
- ], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
1278
- }
1356
+ for (const remoteAddress of topology.remoteMemberAddresses) {
1357
+ for (const source of topology.localPeerAddresses)
1358
+ for (const port of topology.routedTcpPorts) {
1359
+ await checked(host, [
1360
+ "/usr/sbin/ufw",
1361
+ "route",
1362
+ "allow",
1363
+ "proto",
1364
+ "tcp",
1365
+ "from",
1366
+ source,
1367
+ "to",
1368
+ remoteAddress,
1369
+ "port",
1370
+ String(port),
1371
+ "comment",
1372
+ firewallComment
1373
+ ], `private routed service ${source}->${remoteAddress}:${port}`);
1374
+ }
1279
1375
  }
1280
1376
  await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
1281
1377
  await checked(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
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.102";
2
+ export declare const VERSION = "0.1.107";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.102",
3
+ "version": "0.1.107",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@forgezero/runtime": "0.1.14",
20
- "@forgezero/vault": "0.1.19",
20
+ "@forgezero/vault": "0.1.20",
21
21
  "@noble/curves": "2.2.0",
22
22
  "@noble/hashes": "2.2.0",
23
23
  "@noble/post-quantum": "0.6.1",