@takodotid/circuit 0.7.0 → 0.9.0

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": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
5
5
  "keywords": [
6
6
  "network",
@@ -31,3 +31,5 @@ Behaviour that looks like something else. Each one has been hit on a real device
31
31
  - `:find` on an array returns a number or `nil`. Globals persist between exec commands of one login.
32
32
  - A fixed port speed is named by medium, `speed=10G-baseSR-LR` for an optic or `speed=1G-baseT-full` for copper, and only holds with `auto-negotiation=no`. With auto-negotiation back on, the export no longer prints the speed, so the plan resets only auto-negotiation. `speed=""` is refused as ambiguous and `unset speed` does not exist.
33
33
  - `bgp-path-len` counts every AS in the path, prepends included, and takes the same strict comparisons as `dst-len`.
34
+ - `:parse` checks the command, not the text inside `rule=`. A routing filter rule is only checked when it is added.
35
+ - `/routing/filter/filter-wizard` adds the rule it builds; it is not a dry run. `set blackhole yes` was proved this way on 7.23, in an unused chain removed right after.
@@ -874,12 +874,13 @@ export const RPKI_GROUP = "rpki";
874
874
  /** The chain a session without a policy uses, so that it learns or announces nothing. */
875
875
  export const REJECT_ALL = "REJECT-ALL";
876
876
 
877
+ // RouterOS takes our own role. The config states the neighbor's, so each becomes its opposite: a neighbor that is our provider makes us its customer.
877
878
  const ROLES = {
878
- provider: "ebgp-provider",
879
- customer: "ebgp-customer",
879
+ provider: "ebgp-customer",
880
+ customer: "ebgp-provider",
880
881
  peer: "ebgp-peer",
881
- rs: "ebgp-rs",
882
- "rs-client": "ebgp-rs-client",
882
+ rs: "ebgp-rs-client",
883
+ "rs-client": "ebgp-rs",
883
884
  } as const;
884
885
 
885
886
  type AnyRule = PolicyRule<string, string>;
@@ -1005,6 +1006,7 @@ function actions(rule: AnyRule, family: Family): string[] {
1005
1006
  // An address only means something to routes of its own family.
1006
1007
  if (set.next_hop && familyOf(set.next_hop) === family) steps.push(`set gw ${set.next_hop}`);
1007
1008
  if (set.preferred_source && familyOf(set.preferred_source) === family) steps.push(`set pref-src ${set.preferred_source}`);
1009
+ if (set.blackhole) steps.push("set blackhole yes");
1008
1010
  if (rule.action) steps.push(rule.action);
1009
1011
  return steps;
1010
1012
  }
@@ -1097,7 +1099,7 @@ function bgpConnections(out: Output, ctx: Context): void {
1097
1099
  const addressFamily = familyOf(neighbor.address);
1098
1100
  const internal = settings.remote_as === bgp.asn;
1099
1101
  const originates = bgp.networks?.some((prefix) => familyOf(host(prefix)) === family);
1100
- const role = settings.local_role ? ROLES[settings.local_role] : internal ? "ibgp" : "ebgp";
1102
+ const role = settings.role ? ROLES[settings.role] : internal ? "ibgp" : "ebgp";
1101
1103
 
1102
1104
  out.add(
1103
1105
  "/routing bgp connection",
@@ -56,8 +56,8 @@ function peersWithRouteServer(device: Device, iface: AnyInterface): boolean {
56
56
 
57
57
  return Object.values(bgp?.neighbors ?? {}).some((neighbor) => {
58
58
  const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
59
- const role = neighbor.local_role ?? group?.local_role;
60
- return role === "rs-client" && subnets.some((subnet) => contains(subnet, neighbor.address));
59
+ const role = neighbor.role ?? group?.role;
60
+ return role === "rs" && subnets.some((subnet) => contains(subnet, neighbor.address));
61
61
  });
62
62
  }
63
63
 
@@ -223,6 +223,8 @@ export type CommunityScheme<Class extends string = string> = {
223
223
  do_not_announce?: number;
224
224
  /** Set by a customer: prepend toward an AS once, twice or three times, `asn:function:asn`. */
225
225
  prepend?: { once?: number; twice?: number; three_times?: number };
226
+ /** Set by a customer: drop traffic to the route in your network, before it reaches them, `asn:function:0`. The route is never announced further. */
227
+ blackhole?: number;
226
228
  };
227
229
 
228
230
  const PREPENDS = [
@@ -238,6 +240,7 @@ const article = (word: string) => (/^[aeiou]/i.test(word) ? `an ${word}` : `a ${
238
240
  *
239
241
  * - `tag(neighborClass, neighborAs, { site, keepActions })`: a rule for an import policy. It removes your communities a neighbor set, then adds where the route was learned. `site` is needed when the scheme has `learned_at`. With `keepActions`, for a customer, its action communities stay.
240
242
  * - `actions(neighborAs)`: rules for an export policy toward one neighbor. They act on a customer's action communities, then remove all of yours. Place them before the rules that accept.
243
+ * - `blackhole(prefixSet)`: a rule for a customer's import policy, before anything that rejects long prefixes. It takes a route the customer asked to blackhole, but only inside `prefixSet`, its own space, and drops traffic to it here.
241
244
  * - `catalogue`: every community, for `communities` in `defineNetwork`.
242
245
  */
243
246
  export function communityScheme<const Class extends string>(scheme: CommunityScheme<Class>) {
@@ -272,7 +275,27 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
272
275
  };
273
276
  };
274
277
 
278
+ const blackhole = <S extends string>(prefixSet: S): PolicyRule<never, S> => {
279
+ if (scheme.blackhole === undefined) throw new Error("communityScheme: blackhole needs a function number in the scheme");
280
+
281
+ return {
282
+ description: "Blackhole, asked by the customer",
283
+ match: { large_community: community(scheme.blackhole, 0), prefix_set: prefixSet },
284
+ set: { blackhole: true },
285
+ action: "accept",
286
+ };
287
+ };
288
+
275
289
  const actions = (neighborAs: number): PolicyRule<never, never>[] => [
290
+ ...(scheme.blackhole !== undefined
291
+ ? [
292
+ {
293
+ description: "Blackholed here, never announced",
294
+ match: { large_community: community(scheme.blackhole, 0) },
295
+ action: "reject" as const,
296
+ },
297
+ ]
298
+ : []),
276
299
  ...(scheme.do_not_announce !== undefined
277
300
  ? [
278
301
  {
@@ -332,7 +355,10 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
332
355
  const fn = scheme.prepend?.[name];
333
356
  return fn === undefined ? [] : [{ community: community(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
334
357
  }),
358
+ ...(scheme.blackhole !== undefined
359
+ ? [{ community: community(scheme.blackhole, 0), description: `Blackhole: dropped in AS${asn}, not announced further` }]
360
+ : []),
335
361
  ];
336
362
 
337
- return { community, tag, actions, catalogue };
363
+ return { community, tag, actions, blackhole, catalogue };
338
364
  }
@@ -64,6 +64,8 @@ export type Sets = {
64
64
  next_hop?: IP;
65
65
  /** Source address for traffic the device itself sends to the route. Applies to routes of the same family as the address. */
66
66
  preferred_source?: IP;
67
+ /** Drop traffic to the route on this device, instead of forwarding it. */
68
+ blackhole?: true;
67
69
  };
68
70
 
69
71
  /**
@@ -18,7 +18,9 @@ export type StaticRoute<I extends string, F extends string> = {
18
18
  description?: string;
19
19
  };
20
20
 
21
- /** Our side of the relationship, as RFC 9234 defines it. */
21
+ /**
22
+ * What a BGP neighbor is to us, with the roles RFC 9234 defines: `provider`, a transit we buy from; `customer`, one we sell transit to; `peer`, one we exchange our own routes with; `rs`, an exchange's route server; `rs-client`, a network using our route server.
23
+ */
22
24
  export type BgpRole = "provider" | "customer" | "peer" | "rs" | "rs-client";
23
25
 
24
26
  /** Settings a BGP neighbor can state itself or take from its group. */
@@ -33,8 +35,8 @@ export type NeighborSettings<P extends string> = {
33
35
  import?: P;
34
36
  /** Policy for routes announced. Nothing is announced when absent. */
35
37
  export?: P;
36
- /** Our role toward this neighbor, which lets both sides reject a leak. */
37
- local_role?: BgpRole;
38
+ /** What the neighbor is to us, such as `provider` for a transit. Both sides then reject a route leak, after RFC 9234. */
39
+ role?: BgpRole;
38
40
  /** The neighbor is more than one hop away. */
39
41
  multihop?: boolean;
40
42
  /** TCP MD5 password shared with the neighbor. */
@@ -67,7 +67,7 @@ export default defineDevice({
67
67
  "example-transit": {
68
68
  address: "203.0.113.1",
69
69
  remote_as: 64501,
70
- local_role: "customer",
70
+ role: "provider",
71
71
  import: "TRANSIT-IN",
72
72
  export: "OUT",
73
73
  max_prefixes: 1_200_000,
@@ -75,7 +75,7 @@ export default defineDevice({
75
75
  "example-ix-rs-v4": {
76
76
  address: "192.0.2.1",
77
77
  remote_as: 64502,
78
- local_role: "rs-client",
78
+ role: "rs",
79
79
  import: "IX-IN",
80
80
  export: "OUT",
81
81
  max_prefixes: 100_000,
@@ -83,7 +83,7 @@ export default defineDevice({
83
83
  "example-ix-rs-v6": {
84
84
  address: "2001:db8:ffff::1",
85
85
  remote_as: 64502,
86
- local_role: "rs-client",
86
+ role: "rs",
87
87
  import: "IX-IN",
88
88
  export: "OUT",
89
89
  max_prefixes: 50_000,
@@ -51,7 +51,7 @@ export default defineDevice({
51
51
  "example-transit": {
52
52
  address: "203.0.113.1",
53
53
  remote_as: 64501,
54
- local_role: "customer",
54
+ role: "provider",
55
55
  import: "TRANSIT-IN",
56
56
  export: "OUT",
57
57
  max_prefixes: 1_200_000,
@@ -59,7 +59,7 @@ export default defineDevice({
59
59
  "example-ix-rs-v4": {
60
60
  address: "192.0.2.1",
61
61
  remote_as: 64502,
62
- local_role: "rs-client",
62
+ role: "rs",
63
63
  import: "IX-IN",
64
64
  export: "OUT",
65
65
  max_prefixes: 100_000,
@@ -67,7 +67,7 @@ export default defineDevice({
67
67
  "example-ix-rs-v6": {
68
68
  address: "2001:db8:ffff::1",
69
69
  remote_as: 64502,
70
- local_role: "rs-client",
70
+ role: "rs",
71
71
  import: "IX-IN",
72
72
  export: "OUT",
73
73
  max_prefixes: 50_000,