@takodotid/circuit 1.0.0-rc.1 → 1.0.0-rc.3

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 CHANGED
@@ -88,9 +88,9 @@ In the editor, `"10g-49"` is an error, `10g-49 is not a port of CE6855-48S6Q-HI`
88
88
 
89
89
  ## Start
90
90
 
91
- 1. Install Circuit:
92
- - Linux and macOS: `curl -fsSL https://circuit.tako.id/install | sh`
93
- - Windows, in PowerShell: `irm https://circuit.tako.id/install.ps1 | iex`
91
+ 1. Install Circuit. Circuit v1 is a release candidate, so its installers come from its GitHub release until circuit.tako.id serves v1:
92
+ - Linux and macOS: `curl -fsSL https://github.com/takodotid/circuit/releases/download/v1.0.0-rc.2/install | sh`
93
+ - Windows, in PowerShell: `irm https://github.com/takodotid/circuit/releases/download/v1.0.0-rc.2/install.ps1 | iex`
94
94
  2. Start a project: `circuit new network`. Open its folder in VS Code. Add a site in `circuit.ts`, then write each device in `sites/<site>/<device>.ts`.
95
95
  3. Run `circuit`. It opens a menu, or runs a command straight away, such as `circuit validate`.
96
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.3",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. One binary, nothing to install.",
5
5
  "homepage": "https://circuit.tako.id",
6
6
  "bugs": {
@@ -77,7 +77,7 @@ export class DeviceChecks {
77
77
  DeviceChecks.interfaces(device, ports, report, ip);
78
78
  DeviceChecks.routing(device, report, cidr, ip);
79
79
  DeviceChecks.policies(device, report, cidr);
80
- DeviceChecks.firewall(device, cidr);
80
+ DeviceChecks.firewall(device, report, cidr);
81
81
  DeviceChecks.services(device, report, cidr);
82
82
  DeviceChecks.certificates(device, report);
83
83
  DeviceChecks.vdoms(device, report, cidr, ip);
@@ -217,17 +217,53 @@ export class DeviceChecks {
217
217
  const where = `interfaces.${kind}.${name}`;
218
218
  if (ports.includes(name)) report(`${where} has the name of a port`);
219
219
 
220
- if (kind === "gre" || kind === "vxlan") {
220
+ if (kind === "gre" || kind === "vxlan" || kind === "eoip") {
221
221
  const tunnel = settings as Extract<Interface, { remote_address: string }>;
222
222
  ip(where, tunnel.local_address);
223
223
  ip(where, tunnel.remote_address);
224
224
  }
225
225
 
226
+ if (kind === "eoip") {
227
+ const id = (settings as Extract<Interface, { tunnel_id: number }>).tunnel_id;
228
+ if (!(Number.isInteger(id) && id >= 0 && id <= 65535))
229
+ report(`${where}.tunnel_id ${id} is not a whole number from 0 to 65535`);
230
+
231
+ const same = Object.entries(device.interfaces?.eoip ?? {}).find(
232
+ ([other, tunnel]) => other !== name && tunnel?.tunnel_id === id
233
+ );
234
+ if (same && same[0] > name) report(`${where}.tunnel_id ${id} is also interfaces.eoip.${same[0]}'s`);
235
+ }
236
+
237
+ if (settings.shaping && !(settings.shaping.mbps > 0)) report(`${where}.shaping.mbps ${settings.shaping.mbps} is not above 0`);
238
+
239
+ if (kind === "vlan_interface") {
240
+ const subnets = (settings.addresses ?? []).map((entry) => DeviceChecks.addressOf(entry));
241
+
242
+ for (const address of (settings as Extract<Interface, { vlan: string }>).proxy_arp ?? []) {
243
+ ip(`${where}.proxy_arp`, address);
244
+ if (!subnets.some((subnet) => Address.isCidr(subnet) && Address.contains(subnet, address)))
245
+ report(`${where}.proxy_arp ${address} is in none of its subnets, so no host there asks for it`);
246
+ }
247
+ }
248
+
226
249
  if (kind === "lag") {
250
+ const lag = settings as Extract<Interface, { mode?: string }> & {
251
+ link_check?: { arp_targets: readonly string[]; interval_ms: number };
252
+ };
227
253
  DeviceChecks.frames(where, settings as Switched, report);
228
254
 
229
- const members = Derive.ports(device).filter(([, port]) => port.in_lag === name);
230
- if (!members.length) report(`${where} has no member ports`);
255
+ const ports = Derive.ports(device).filter(([, port]) => port.in_lag === name);
256
+ const tunnels = Object.entries(device.interfaces?.eoip ?? {}).filter(([, tunnel]) => tunnel?.in_lag === name);
257
+ if (!ports.length && !tunnels.length) report(`${where} has no members`);
258
+ if (tunnels.length && lag.mode !== "round_robin") report(`${where} has EoIP members, which only a round_robin LAG takes`);
259
+
260
+ const check = lag.link_check;
261
+ if (check) {
262
+ if (!check.arp_targets.length) report(`${where}.link_check.arp_targets is empty`);
263
+ for (const target of check.arp_targets) ip(`${where}.link_check.arp_targets`, target);
264
+ if (!(Number.isInteger(check.interval_ms) && check.interval_ms > 0))
265
+ report(`${where}.link_check.interval_ms ${check.interval_ms} is not a whole number above 0`);
266
+ }
231
267
  }
232
268
 
233
269
  if (kind === "bridge") {
@@ -355,7 +391,7 @@ export class DeviceChecks {
355
391
  for (const policy of Object.keys(policies)) visit(policy, []);
356
392
  }
357
393
 
358
- private static firewall(device: Device, cidr: Checker): void {
394
+ private static firewall(device: Device, report: Report, cidr: Checker): void {
359
395
  const firewall = device.firewall;
360
396
 
361
397
  for (const [name, members] of Object.entries(firewall?.address_sets ?? {})) {
@@ -371,8 +407,15 @@ export class DeviceChecks {
371
407
  })),
372
408
  { where: "firewall.nat.source", rules: firewall?.nat?.source ?? [] },
373
409
  { where: "firewall.nat.destination", rules: firewall?.nat?.destination ?? [] },
410
+ { where: "firewall.tcp_mss", rules: firewall?.tcp_mss ?? [] },
374
411
  ];
375
412
 
413
+ (firewall?.tcp_mss ?? []).forEach((rule, index) => {
414
+ // 536 is the MSS every IPv4 host takes (RFC 879); 65495 is the largest IP packet less 40 bytes of headers.
415
+ if (rule.mss !== "path_mtu" && !(Number.isInteger(rule.mss) && rule.mss >= 536 && rule.mss <= 65495))
416
+ report(`firewall.tcp_mss[${index}].mss ${rule.mss} is not "path_mtu" or a whole number from 536 to 65495`);
417
+ });
418
+
376
419
  for (const { where, rules } of chains) {
377
420
  for (const rule of rules) {
378
421
  // The model holds one prefix a side; a list is taken as the language writes it.
package/src/cli/app.ts CHANGED
@@ -42,11 +42,15 @@ export class App {
42
42
  App.removeFromWindowsPath(folder);
43
43
  spawnSync("reg", ["delete", App.UNINSTALL_KEY, "/f"], { stdio: "ignore" });
44
44
 
45
- // Windows does not let a running program delete itself, so a separate process deletes it once this one has ended.
46
- spawn("cmd", ["/c", `ping -n 3 127.0.0.1 >nul & del /f /q "${binary}" & del /f /q "${binary}.old" & rmdir "${folder}"`], {
45
+ // Windows does not let a running program delete itself, so a separate process deletes it once this one has ended: it tries
46
+ // again until the file goes, for about ten seconds. Only cmd's own commands: a program it started, such as ping to wait,
47
+ // would open a console window. The line goes to cmd as written; quoted the usual way, cmd can't read the paths.
48
+ const remove = `del /f /q "${binary}" "${binary}.old" 2>nul & if not exist "${binary}" (rmdir "${folder}" 2>nul & exit)`;
49
+ spawn("cmd", ["/d", "/c", `for /l %i in (1,1,10000) do @(${remove})`], {
47
50
  detached: true,
48
51
  stdio: "ignore",
49
52
  windowsHide: true,
53
+ windowsVerbatimArguments: true,
50
54
  }).unref();
51
55
  } else {
52
56
  rmSync(binary, { force: true });