@takodotid/circuit 0.11.0 → 0.13.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.
@@ -47,7 +47,13 @@ function neighborPolicies(device) {
47
47
  const bgp = device.routing?.bgp;
48
48
  return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
49
49
  const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
50
- return { name, address: neighbor.address, import: neighbor.import ?? group?.import, export: neighbor.export ?? group?.export };
50
+ return {
51
+ name,
52
+ address: neighbor.address,
53
+ role: neighbor.role ?? group?.role,
54
+ import: neighbor.import ?? group?.import,
55
+ export: neighbor.export ?? group?.export
56
+ };
51
57
  });
52
58
  }
53
59
  function offered(device, policy) {
@@ -58,6 +64,8 @@ var exportsEndInReject = (devices) => {
58
64
  const findings = [];
59
65
  for (const device of devices) {
60
66
  for (const neighbor of neighborPolicies(device)) {
67
+ if (neighbor.role === "customer")
68
+ continue;
61
69
  const last = neighbor.export ? device.policies?.[neighbor.export]?.at(-1) : undefined;
62
70
  if (neighbor.export && (last?.action !== "reject" || last.match)) {
63
71
  findings.push({
@@ -153,14 +161,15 @@ function communityScheme(scheme) {
153
161
  const ours = { standard: [`${asn}:*`], large: [`${asn}:*:*`] };
154
162
  const information = [scheme.learned_from?.function, scheme.learned_at?.function, scheme.learned_from_as].filter((fn) => fn !== undefined);
155
163
  const tag = (neighborClass, neighborAs, options = {}) => {
156
- const { site, keepActions = false } = options;
164
+ const { site } = options;
165
+ const isCustomer = scheme.customers?.includes(neighborClass) ?? false;
157
166
  if (scheme.learned_at && site === undefined)
158
167
  throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
159
168
  return {
160
169
  description: `Learned from AS${neighborAs}`,
161
170
  set: {
162
171
  remove_communities: ours.standard,
163
- remove_large_communities: keepActions ? information.map((fn) => community(fn, "*")) : ours.large,
172
+ remove_large_communities: isCustomer ? information.map((fn) => community(fn, "*")) : ours.large,
164
173
  add_large_communities: [
165
174
  ...scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : [],
166
175
  ...scheme.learned_at ? [community(scheme.learned_at.function, site)] : [],
@@ -253,12 +262,68 @@ function communityScheme(scheme) {
253
262
  ];
254
263
  return { community, tag, actions, blackhole, catalogue };
255
264
  }
265
+ function internetExchange(exchange, members) {
266
+ const percent = exchange.storm_percent ?? 1;
267
+ const ports = (device) => {
268
+ const result = {};
269
+ for (const member of members) {
270
+ const port = member.ports[device];
271
+ if (!port)
272
+ continue;
273
+ result[port] = {
274
+ description: `${member.name}, AS${member.asn}`,
275
+ access_vlan: exchange.vlan,
276
+ stp: false,
277
+ lldp: false,
278
+ storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } }
279
+ };
280
+ }
281
+ return result;
282
+ };
283
+ const check = (devices) => {
284
+ const findings = [];
285
+ const error = (message, device) => findings.push({ level: "error", ...device ? { device } : {}, message });
286
+ const taken = new Map((exchange.reserved ?? []).map((address) => [address, "reserved"]));
287
+ members.forEach((member, index) => {
288
+ for (const other of members.slice(index + 1)) {
289
+ if (other.name === member.name)
290
+ error(`member ${member.name} is declared twice`);
291
+ if (other.asn === member.asn)
292
+ error(`members ${member.name} and ${other.name} share AS${member.asn}`);
293
+ }
294
+ for (const [address, lan] of [
295
+ [member.ipv4, exchange.lan.ipv4],
296
+ [member.ipv6, exchange.lan.ipv6]
297
+ ]) {
298
+ if (!address)
299
+ continue;
300
+ if (!lan || !contains(lan, address))
301
+ error(`${address} of ${member.name} is outside the peering LAN`);
302
+ const holder = taken.get(address);
303
+ if (holder)
304
+ error(`${address} of ${member.name} is already ${holder === "reserved" ? "reserved" : `${holder}'s`}`);
305
+ taken.set(address, member.name);
306
+ }
307
+ for (const [deviceName, portName] of Object.entries(member.ports)) {
308
+ const device = devices.find((candidate) => candidate.name === deviceName);
309
+ const port = device?.ports?.[portName];
310
+ const carried = port ? vlansOf(port) : [];
311
+ if (carried.length !== 1 || carried[0] !== exchange.vlan) {
312
+ error(`port ${portName} of ${member.name} does not carry the peering VLAN alone`, deviceName);
313
+ }
314
+ }
315
+ });
316
+ return findings;
317
+ };
318
+ return { ports, check };
319
+ }
256
320
  export {
257
321
  vlansOf,
258
322
  tunnelsOutsideOffered,
259
323
  trustBoundary,
260
324
  offered,
261
325
  neighborPolicies,
326
+ internetExchange,
262
327
  exportsEndInReject,
263
328
  communityScheme,
264
329
  bgpSanity,
@@ -1,5 +1,5 @@
1
1
  import type { Check, Community } from "../core/define";
2
- import type { AclRule, Device, FilterRule, PolicyRule, Prefix } from "../schema";
2
+ import type { AclRule, Device, FilterRule, IP, PolicyRule, Port, Prefix } from "../schema";
3
3
  /** IPv4 sources that never arrive from the internet: loopback, link-local and "this network", after RFC 6890. */
4
4
  export declare const MARTIANS: readonly ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8"];
5
5
  /** IPv4 private space, RFC 1918. */
@@ -23,16 +23,19 @@ export type SanityLengths = {
23
23
  };
24
24
  /** Route policy rules rejecting what no neighbor should send: prefixes too short or too long for the global table, and RPKI invalid. Place it first in an import, or as a policy other imports call. */
25
25
  export declare function bgpSanity(lengths?: SanityLengths): PolicyRule<never, never>[];
26
- /** Each BGP neighbor's own policies, or its group's. */
26
+ /** Each BGP neighbor's own policies and role, or its group's. */
27
27
  export declare function neighborPolicies(device: Device): {
28
28
  name: string;
29
29
  address: string;
30
+ role: import("..").BgpRole | undefined;
30
31
  import: string | undefined;
31
32
  export: string | undefined;
32
33
  }[];
33
34
  /** The prefixes an export policy accepts by name. */
34
35
  export declare function offered(device: Device, policy: string | undefined): string[];
35
- /** Every BGP export ends in a reject that matches everything, so nothing leaves by accident. */
36
+ /**
37
+ * Every BGP export ends in a reject that matches everything, so nothing leaves by accident. An export to a neighbor whose `role` is `customer` is left out: sending a customer everything you know is what it pays for, and cannot leak a route to the rest of the internet.
38
+ */
36
39
  export declare const exportsEndInReject: Check;
37
40
  /** The VLANs a port carries, untagged or tagged. */
38
41
  export declare const vlansOf: (port: {
@@ -61,6 +64,8 @@ export type CommunityScheme<Class extends string = string> = {
61
64
  function: number;
62
65
  classes: Readonly<Record<Class, number>>;
63
66
  };
67
+ /** Which of the classes above are your customers. Only a customer may ask you for something, such as not announcing its route somewhere; what anyone else asks is removed when the route comes in. */
68
+ customers?: readonly NoInfer<Class>[];
64
69
  /** Set on import: the site a route was learned at, `asn:function:site`. `sites` names the sites, for the catalogue. */
65
70
  learned_at?: {
66
71
  function: number;
@@ -82,7 +87,7 @@ export type CommunityScheme<Class extends string = string> = {
82
87
  /**
83
88
  * Policy rules and a catalogue from one community scheme, so the communities your routers set, the ones they act on, and the ones you publish never disagree.
84
89
  *
85
- * - `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.
90
+ * - `tag(neighborClass, neighborAs, { site })`: a rule for an import policy. It removes your communities a neighbor set, then adds where the route was learned. A customer, a class in `customers`, keeps its requests. `site` is needed when the scheme has `learned_at`.
86
91
  * - `actions(neighborAs, { blackhole })`: 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. A blackholed route goes no further, unless `blackhole` names the neighbor's own blackhole community: then it is passed on with it, so the neighbor drops the traffic too.
87
92
  * - `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.
88
93
  * - `catalogue`: every community, for `communities` in `defineNetwork`.
@@ -91,7 +96,6 @@ export declare function communityScheme<const Class extends string>(scheme: Comm
91
96
  community: (fn: number, parameter: number | string) => string;
92
97
  tag: (neighborClass: Class, neighborAs: number, options?: {
93
98
  site?: number;
94
- keepActions?: boolean;
95
99
  }) => PolicyRule<never, never>;
96
100
  actions: (neighborAs: number, options?: {
97
101
  blackhole?: string;
@@ -99,3 +103,38 @@ export declare function communityScheme<const Class extends string>(scheme: Comm
99
103
  blackhole: <S extends string>(prefixSet: S) => PolicyRule<never, S>;
100
104
  catalogue: Community[];
101
105
  };
106
+ /** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
107
+ export type ExchangeMember = {
108
+ /** Lowercase, and never reused. */
109
+ name: string;
110
+ asn: number;
111
+ /** The member's port on each fabric switch, by switch name. */
112
+ ports: Readonly<Record<string, string>>;
113
+ /** Its addresses on the peering LAN. The route servers peer with these. */
114
+ ipv4?: IP;
115
+ ipv6?: IP;
116
+ };
117
+ /** An internet exchange's peering LAN, as its fabric carries it. */
118
+ export type InternetExchange<V extends string> = {
119
+ /** The peering VLAN, by the name the fabric switches give it. */
120
+ vlan: V;
121
+ /** The peering LAN's subnets. */
122
+ lan: {
123
+ ipv4?: Prefix;
124
+ ipv6?: Prefix;
125
+ };
126
+ /** Addresses on the peering LAN no member may take, such as the route servers'. */
127
+ reserved?: readonly IP[];
128
+ /** How much broadcast, multicast and unknown unicast a member port lets through, in percent of its speed. 1 when absent. */
129
+ storm_percent?: number;
130
+ };
131
+ /**
132
+ * Member ports and a check for an internet exchange's fabric.
133
+ *
134
+ * - `ports(device)`: every member's port on one switch. A member port is in the peering VLAN and nothing else, takes no part in spanning tree or LLDP, and lets only a trickle of broadcast, multicast and unknown unicast through.
135
+ * - `check`: members never share a name, an AS or an address; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
136
+ */
137
+ export declare function internetExchange<const V extends string>(exchange: InternetExchange<V>, members: readonly ExchangeMember[]): {
138
+ ports: (device: string) => Record<string, Port<V, string, string, string>>;
139
+ check: Check;
140
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { contains } from "../core/addr";
4
4
  import type { Check, Community, Finding } from "../core/define";
5
- import type { AclRule, Device, FilterRule, PolicyRule, Prefix, TcpFlag } from "../schema";
5
+ import type { AclRule, Device, FilterRule, IP, PolicyRule, Port, Prefix, TcpFlag } from "../schema";
6
6
 
7
7
  /** IPv4 sources that never arrive from the internet: loopback, link-local and "this network", after RFC 6890. */
8
8
  export const MARTIANS = ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8"] as const satisfies readonly Prefix[];
@@ -66,13 +66,20 @@ export function bgpSanity(lengths: SanityLengths = {}): PolicyRule<never, never>
66
66
  ];
67
67
  }
68
68
 
69
- /** Each BGP neighbor's own policies, or its group's. */
69
+ /** Each BGP neighbor's own policies and role, or its group's. */
70
70
  export function neighborPolicies(device: Device) {
71
71
  const bgp = device.routing?.bgp;
72
72
 
73
73
  return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
74
74
  const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
75
- return { name, address: neighbor.address, import: neighbor.import ?? group?.import, export: neighbor.export ?? group?.export };
75
+
76
+ return {
77
+ name,
78
+ address: neighbor.address,
79
+ role: neighbor.role ?? group?.role,
80
+ import: neighbor.import ?? group?.import,
81
+ export: neighbor.export ?? group?.export,
82
+ };
76
83
  });
77
84
  }
78
85
 
@@ -82,12 +89,16 @@ export function offered(device: Device, policy: string | undefined): string[] {
82
89
  return rules.filter((rule) => rule.action === "accept" && rule.match?.prefix).map((rule) => rule.match!.prefix!);
83
90
  }
84
91
 
85
- /** Every BGP export ends in a reject that matches everything, so nothing leaves by accident. */
92
+ /**
93
+ * Every BGP export ends in a reject that matches everything, so nothing leaves by accident. An export to a neighbor whose `role` is `customer` is left out: sending a customer everything you know is what it pays for, and cannot leak a route to the rest of the internet.
94
+ */
86
95
  export const exportsEndInReject: Check = (devices) => {
87
96
  const findings: Finding[] = [];
88
97
 
89
98
  for (const device of devices) {
90
99
  for (const neighbor of neighborPolicies(device)) {
100
+ if (neighbor.role === "customer") continue;
101
+
91
102
  const last = neighbor.export ? device.policies?.[neighbor.export]?.at(-1) : undefined;
92
103
 
93
104
  if (neighbor.export && (last?.action !== "reject" || last.match)) {
@@ -215,6 +226,8 @@ export type CommunityScheme<Class extends string = string> = {
215
226
  asn: number;
216
227
  /** Set on import: the kind of neighbor a route came from, `asn:function:class`. */
217
228
  learned_from?: { function: number; classes: Readonly<Record<Class, number>> };
229
+ /** Which of the classes above are your customers. Only a customer may ask you for something, such as not announcing its route somewhere; what anyone else asks is removed when the route comes in. */
230
+ customers?: readonly NoInfer<Class>[];
218
231
  /** Set on import: the site a route was learned at, `asn:function:site`. `sites` names the sites, for the catalogue. */
219
232
  learned_at?: { function: number; sites?: Readonly<Record<number, string>> };
220
233
  /** Set on import: the AS a route was learned from, `asn:function:asn`. */
@@ -238,7 +251,7 @@ const article = (word: string) => (/^[aeiou]/i.test(word) ? `an ${word}` : `a ${
238
251
  /**
239
252
  * Policy rules and a catalogue from one community scheme, so the communities your routers set, the ones they act on, and the ones you publish never disagree.
240
253
  *
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.
254
+ * - `tag(neighborClass, neighborAs, { site })`: a rule for an import policy. It removes your communities a neighbor set, then adds where the route was learned. A customer, a class in `customers`, keeps its requests. `site` is needed when the scheme has `learned_at`.
242
255
  * - `actions(neighborAs, { blackhole })`: 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. A blackholed route goes no further, unless `blackhole` names the neighbor's own blackhole community: then it is passed on with it, so the neighbor drops the traffic too.
243
256
  * - `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.
244
257
  * - `catalogue`: every community, for `communities` in `defineNetwork`.
@@ -252,12 +265,9 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
252
265
  (fn): fn is number => fn !== undefined
253
266
  );
254
267
 
255
- const tag = (
256
- neighborClass: Class,
257
- neighborAs: number,
258
- options: { site?: number; keepActions?: boolean } = {}
259
- ): PolicyRule<never, never> => {
260
- const { site, keepActions = false } = options;
268
+ const tag = (neighborClass: Class, neighborAs: number, options: { site?: number } = {}): PolicyRule<never, never> => {
269
+ const { site } = options;
270
+ const isCustomer = scheme.customers?.includes(neighborClass) ?? false;
261
271
  if (scheme.learned_at && site === undefined)
262
272
  throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
263
273
 
@@ -265,7 +275,7 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
265
275
  description: `Learned from AS${neighborAs}`,
266
276
  set: {
267
277
  remove_communities: ours.standard,
268
- remove_large_communities: keepActions ? information.map((fn) => community(fn, "*")) : ours.large,
278
+ remove_large_communities: isCustomer ? information.map((fn) => community(fn, "*")) : ours.large,
269
279
  add_large_communities: [
270
280
  ...(scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : []),
271
281
  ...(scheme.learned_at ? [community(scheme.learned_at.function, site!)] : []),
@@ -376,3 +386,95 @@ export function communityScheme<const Class extends string>(scheme: CommunitySch
376
386
 
377
387
  return { community, tag, actions, blackhole, catalogue };
378
388
  }
389
+
390
+ /** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
391
+ export type ExchangeMember = {
392
+ /** Lowercase, and never reused. */
393
+ name: string;
394
+ asn: number;
395
+ /** The member's port on each fabric switch, by switch name. */
396
+ ports: Readonly<Record<string, string>>;
397
+ /** Its addresses on the peering LAN. The route servers peer with these. */
398
+ ipv4?: IP;
399
+ ipv6?: IP;
400
+ };
401
+
402
+ /** An internet exchange's peering LAN, as its fabric carries it. */
403
+ export type InternetExchange<V extends string> = {
404
+ /** The peering VLAN, by the name the fabric switches give it. */
405
+ vlan: V;
406
+ /** The peering LAN's subnets. */
407
+ lan: { ipv4?: Prefix; ipv6?: Prefix };
408
+ /** Addresses on the peering LAN no member may take, such as the route servers'. */
409
+ reserved?: readonly IP[];
410
+ /** How much broadcast, multicast and unknown unicast a member port lets through, in percent of its speed. 1 when absent. */
411
+ storm_percent?: number;
412
+ };
413
+
414
+ /**
415
+ * Member ports and a check for an internet exchange's fabric.
416
+ *
417
+ * - `ports(device)`: every member's port on one switch. A member port is in the peering VLAN and nothing else, takes no part in spanning tree or LLDP, and lets only a trickle of broadcast, multicast and unknown unicast through.
418
+ * - `check`: members never share a name, an AS or an address; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
419
+ */
420
+ export function internetExchange<const V extends string>(exchange: InternetExchange<V>, members: readonly ExchangeMember[]) {
421
+ const percent = exchange.storm_percent ?? 1;
422
+
423
+ const ports = (device: string) => {
424
+ const result: Record<string, Port<V, string, string, string>> = {};
425
+
426
+ for (const member of members) {
427
+ const port = member.ports[device];
428
+ if (!port) continue;
429
+
430
+ result[port] = {
431
+ description: `${member.name}, AS${member.asn}`,
432
+ access_vlan: exchange.vlan,
433
+ stp: false,
434
+ lldp: false,
435
+ storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
436
+ };
437
+ }
438
+
439
+ return result;
440
+ };
441
+
442
+ const check: Check = (devices) => {
443
+ const findings: Finding[] = [];
444
+ const error = (message: string, device?: string) => findings.push({ level: "error", ...(device ? { device } : {}), message });
445
+ const taken = new Map<string, string>((exchange.reserved ?? []).map((address) => [address, "reserved"]));
446
+
447
+ members.forEach((member, index) => {
448
+ for (const other of members.slice(index + 1)) {
449
+ if (other.name === member.name) error(`member ${member.name} is declared twice`);
450
+ if (other.asn === member.asn) error(`members ${member.name} and ${other.name} share AS${member.asn}`);
451
+ }
452
+
453
+ for (const [address, lan] of [
454
+ [member.ipv4, exchange.lan.ipv4],
455
+ [member.ipv6, exchange.lan.ipv6],
456
+ ] as const) {
457
+ if (!address) continue;
458
+ if (!lan || !contains(lan, address)) error(`${address} of ${member.name} is outside the peering LAN`);
459
+
460
+ const holder = taken.get(address);
461
+ if (holder) error(`${address} of ${member.name} is already ${holder === "reserved" ? "reserved" : `${holder}'s`}`);
462
+ taken.set(address, member.name);
463
+ }
464
+
465
+ for (const [deviceName, portName] of Object.entries(member.ports)) {
466
+ const device = devices.find((candidate) => candidate.name === deviceName);
467
+ const port = device?.ports?.[portName as keyof typeof device.ports];
468
+ const carried = port ? vlansOf(port) : [];
469
+
470
+ if (carried.length !== 1 || carried[0] !== exchange.vlan) {
471
+ error(`port ${portName} of ${member.name} does not carry the peering VLAN alone`, deviceName);
472
+ }
473
+ }
474
+ });
475
+
476
+ return findings;
477
+ };
478
+
479
+ return { ports, check };
480
+ }
@@ -1,4 +1,6 @@
1
1
  // The edge router: BGP with an IP transit and an internet exchange, the gateway of every tenant, and the filter between them.
2
+ //
3
+ // Hurricane Electric and SGIX stand for your own transit and exchange. Addresses are from the documentation ranges; use the ones your providers give you.
2
4
 
3
5
  import type { Prefix } from "@takodotid/circuit";
4
6
  import { defineDevice, merge, secret } from "@takodotid/circuit";
@@ -29,16 +31,16 @@ export default defineDevice({
29
31
  vlans: merge(
30
32
  {
31
33
  mgmt: { id: 99, description: "Management of the switches" },
32
- transit: { id: 300, description: "IP transit from Example Transit, AS64501" },
33
- ix: { id: 200, description: "Peering LAN of Example IX" },
34
+ transit: { id: 300, description: "IP transit from Hurricane Electric, AS6939" },
35
+ ix: { id: 200, description: "Peering LAN of SGIX" },
34
36
  },
35
37
  internetVlans(TENANTS)
36
38
  ),
37
39
 
38
40
  ports: {
39
41
  "1g-1": { description: "Management", addresses: ["10.0.0.1/24"] },
40
- "25g-1": { description: "Example Transit", access_vlan: "transit", acl: "edge" },
41
- "25g-2": { description: "Example IX", access_vlan: "ix", acl: "edge" },
42
+ "25g-1": { description: "Hurricane Electric", access_vlan: "transit", acl: "edge" },
43
+ "25g-2": { description: "SGIX", access_vlan: "ix", acl: "edge" },
42
44
  "100g-1": {
43
45
  description: "tor-01",
44
46
  speed: "40g",
@@ -64,25 +66,25 @@ export default defineDevice({
64
66
  asn: ASN,
65
67
  networks: [...OURS],
66
68
  neighbors: {
67
- "example-transit": {
69
+ "hurricane-electric": {
68
70
  address: "203.0.113.1",
69
- remote_as: 64501,
71
+ remote_as: 6939,
70
72
  role: "provider",
71
73
  import: "TRANSIT-IN",
72
74
  export: "OUT",
73
75
  max_prefixes: 1_200_000,
74
76
  },
75
- "example-ix-rs-v4": {
77
+ "sgix-rs-v4": {
76
78
  address: "192.0.2.1",
77
- remote_as: 64502,
79
+ remote_as: 55518,
78
80
  role: "rs",
79
81
  import: "IX-IN",
80
82
  export: "OUT",
81
83
  max_prefixes: 100_000,
82
84
  },
83
- "example-ix-rs-v6": {
85
+ "sgix-rs-v6": {
84
86
  address: "2001:db8:ffff::1",
85
- remote_as: 64502,
87
+ remote_as: 55518,
86
88
  role: "rs",
87
89
  import: "IX-IN",
88
90
  export: "OUT",
@@ -95,8 +97,8 @@ export default defineDevice({
95
97
  policies: {
96
98
  SANITY: bgpSanity(),
97
99
  // A route from the exchange is preferred over the same route from the transit: higher local preference wins.
98
- "TRANSIT-IN": [{ call: "SANITY" }, communities.tag("transit", 64501), { set: { local_pref: 100 } }, { action: "accept" }],
99
- "IX-IN": [{ call: "SANITY" }, communities.tag("exchange", 64502), { set: { local_pref: 200 } }, { action: "accept" }],
100
+ "TRANSIT-IN": [{ call: "SANITY" }, communities.tag("transit", 6939), { set: { local_pref: 100 } }, { action: "accept" }],
101
+ "IX-IN": [{ call: "SANITY" }, communities.tag("exchange", 55518), { set: { local_pref: 200 } }, { action: "accept" }],
100
102
  OUT: [
101
103
  ...OURS.map((prefix) => ({ description: `Announce ${prefix}`, match: { prefix }, action: "accept" as const })),
102
104
  { description: "Nothing else leaves", action: "reject" },
@@ -1,4 +1,6 @@
1
1
  // The edge router: BGP with an IP transit and an internet exchange, filtered on the way in and on the way out.
2
+ //
3
+ // Hurricane Electric and SGIX stand for your own transit and exchange. Addresses are from the documentation ranges; use the ones your providers give you.
2
4
 
3
5
  import type { Prefix } from "@takodotid/circuit";
4
6
  import { defineDevice, secret } from "@takodotid/circuit";
@@ -25,14 +27,14 @@ export default defineDevice({
25
27
  management: { allow: [MANAGEMENT], ssh: {} },
26
28
 
27
29
  vlans: {
28
- transit: { id: 300, description: "IP transit from Example Transit, AS64501" },
29
- ix: { id: 200, description: "Peering LAN of Example IX" },
30
+ transit: { id: 300, description: "IP transit from Hurricane Electric, AS6939" },
31
+ ix: { id: 200, description: "Peering LAN of SGIX" },
30
32
  },
31
33
 
32
34
  ports: {
33
35
  "1g-1": { description: "Management", addresses: ["10.0.0.1/24"] },
34
- "25g-1": { description: "Example Transit", access_vlan: "transit", acl: "edge" },
35
- "25g-2": { description: "Example IX", access_vlan: "ix", acl: "edge" },
36
+ "25g-1": { description: "Hurricane Electric", access_vlan: "transit", acl: "edge" },
37
+ "25g-2": { description: "SGIX", access_vlan: "ix", acl: "edge" },
36
38
  },
37
39
 
38
40
  interfaces: {
@@ -48,25 +50,25 @@ export default defineDevice({
48
50
  asn: ASN,
49
51
  networks: [...OURS],
50
52
  neighbors: {
51
- "example-transit": {
53
+ "hurricane-electric": {
52
54
  address: "203.0.113.1",
53
- remote_as: 64501,
55
+ remote_as: 6939,
54
56
  role: "provider",
55
57
  import: "TRANSIT-IN",
56
58
  export: "OUT",
57
59
  max_prefixes: 1_200_000,
58
60
  },
59
- "example-ix-rs-v4": {
61
+ "sgix-rs-v4": {
60
62
  address: "192.0.2.1",
61
- remote_as: 64502,
63
+ remote_as: 55518,
62
64
  role: "rs",
63
65
  import: "IX-IN",
64
66
  export: "OUT",
65
67
  max_prefixes: 100_000,
66
68
  },
67
- "example-ix-rs-v6": {
69
+ "sgix-rs-v6": {
68
70
  address: "2001:db8:ffff::1",
69
- remote_as: 64502,
71
+ remote_as: 55518,
70
72
  role: "rs",
71
73
  import: "IX-IN",
72
74
  export: "OUT",
@@ -79,8 +81,8 @@ export default defineDevice({
79
81
  policies: {
80
82
  SANITY: bgpSanity(),
81
83
  // A route from the exchange is preferred over the same route from the transit: higher local preference wins.
82
- "TRANSIT-IN": [{ call: "SANITY" }, communities.tag("transit", 64501), { set: { local_pref: 100 } }, { action: "accept" }],
83
- "IX-IN": [{ call: "SANITY" }, communities.tag("exchange", 64502), { set: { local_pref: 200 } }, { action: "accept" }],
84
+ "TRANSIT-IN": [{ call: "SANITY" }, communities.tag("transit", 6939), { set: { local_pref: 100 } }, { action: "accept" }],
85
+ "IX-IN": [{ call: "SANITY" }, communities.tag("exchange", 55518), { set: { local_pref: 200 } }, { action: "accept" }],
84
86
  OUT: [
85
87
  ...OURS.map((prefix) => ({ description: `Announce ${prefix}`, match: { prefix }, action: "accept" as const })),
86
88
  { description: "Nothing else leaves", action: "reject" },