@takodotid/circuit 1.0.0-rc.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "1.0.0-rc.2",
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.