@takodotid/circuit 0.40.0 → 0.41.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.
@@ -1,48 +1,67 @@
1
- import type { Exchange as ExchangeLan, Family, FilterRule, Gre, InterfaceAddress, IP, LargeCommunity, Neighbor, NeighborSettings, PolicyRule, Port, Secret, StandardCommunity, Vlan, VlanInterface } from "../schema/index.js";
2
- import { type Announced } from "./bgp.js";
3
- /** One network we exchange routes with: a transit we buy, an exchange's route servers, or a peer. */
1
+ import type { Exchange as ExchangeLan, Family, FilterRule, Gre, InterfaceAddress, IP, Neighbor, NeighborSettings, PolicyRule, Port, Secret, Vlan, VlanInterface } from "../schema/index.js";
2
+ /** A route policy an upstream's sessions use: its name on the device, and its rules, in order. */
3
+ export type UpstreamPolicy = {
4
+ /** The policy's name, as the device carries it. */
5
+ name: string;
6
+ /** Its rules, in order. Nothing is added to them unless `mapImports` or `mapExports` adds it. */
7
+ rules: readonly PolicyRule<string, string>[];
8
+ };
9
+ /** A VLAN by its name and its ID. */
10
+ export type NamedVlan = {
11
+ name: string;
12
+ id: number;
13
+ };
14
+ /** One network we exchange routes with: a transit we buy, an exchange's route servers, or a peer. Every name it puts on a device is written here. */
4
15
  export type Upstream = {
5
16
  /** Its name as people write it, for descriptions: `Transit A`. The key when absent. */
6
17
  name?: string;
7
18
  asn: number;
8
19
  /** What it is to us: `provider` for a transit, `rs` for an exchange's route servers, `peer` for a direct session. */
9
20
  role: "provider" | "rs" | "peer";
10
- /** Where it lands on our side: a VLAN, our addresses on it, and the switch port it arrives on. A peer over a tunnel, or over an exchange's VLAN, has none. */
21
+ /** Where it lands on our side: a VLAN, the router's interface on it, and the switch port it arrives on. A peer over a tunnel, or over an exchange's VLAN, has none. */
11
22
  handoff?: {
12
- /** The VLAN's ID. */
13
- vlan: number;
14
- /** It sends its VLAN tagged: its port gets a native VLAN of its own with this ID, which nothing else is in. Untagged when absent. */
15
- native?: number;
16
- /** Its number in names with `{number}`, such as `core-ipt01-transita`. Written here, so removing another upstream renumbers nothing. */
17
- number?: number;
23
+ /** The VLAN it arrives on. */
24
+ vlan: NamedVlan;
25
+ /** It sends its VLAN tagged: its switch port gets this native VLAN, which nothing else is in. Untagged when absent. */
26
+ native?: NamedVlan;
27
+ /** The router's VLAN interface on it. */
28
+ interface: string;
18
29
  /** Our addresses on it. `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
19
30
  addresses: readonly (InterfaceAddress | {
20
31
  address: InterfaceAddress;
21
32
  peer: IP;
22
33
  })[];
23
- /** The switch port it arrives on. */
34
+ /**
35
+ * The switch port it arrives on. LLDP and spanning tree are off there unless `lldp` or `stp` says otherwise: an upstream's port faces someone else's network, which should neither learn ours nor join our spanning tree.
36
+ */
24
37
  port?: {
25
38
  device: string;
26
39
  port: string;
27
40
  storm_control?: Port<string, string, string, string>["storm_control"];
41
+ /** LLDP on the port. Off when absent. */
42
+ lldp?: boolean;
43
+ /** Spanning tree on the port. Off when absent. */
44
+ stp?: boolean;
28
45
  };
29
46
  /** An exchange's LAN, for `circuit peeringdb`. */
30
47
  exchange?: ExchangeLan;
31
48
  };
32
- /** A GRE tunnel to it. `number` is its number in names with `{number}`. */
49
+ /** A GRE tunnel to it, and the router's interface for it. */
33
50
  tunnel?: {
51
+ interface: string;
34
52
  local: IP;
35
53
  remote: IP;
36
54
  addresses: readonly InterfaceAddress[];
37
55
  mtu?: number;
38
- number?: number;
39
56
  };
40
57
  /** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
41
58
  sessions: Readonly<Record<string, {
42
59
  address: IP;
43
60
  local_address?: IP;
44
61
  }>>;
45
- /** The sessions are more than one hop away, from this address of ours. They share a group. */
62
+ /** A BGP group its sessions share, by this name: the group holds the AS, role, policies and multihop settings, and each session names it. Each session holds them itself when absent. */
63
+ group?: string;
64
+ /** The sessions are more than one hop away, from this address of ours. */
46
65
  multihop?: {
47
66
  local_address: IP;
48
67
  families?: readonly Family[];
@@ -53,185 +72,100 @@ export type Upstream = {
53
72
  ipv4?: number;
54
73
  ipv6?: number;
55
74
  };
56
- /** What we take. Nothing when absent. */
57
- import?: {
58
- /** Its default route, at this preference. */
59
- default?: {
60
- local_pref?: number;
61
- description?: string;
62
- };
63
- /** Its routes, through `Bgp.learn`: the `before` rules, sanity, this preference, the tag. Only the default when absent. */
64
- learn?: {
65
- local_pref?: number;
66
- before?: readonly PolicyRule<never, never>[];
67
- };
68
- };
69
- /** Rules of yours in its export, between the community actions and what we announce, such as a prepend. */
70
- export?: readonly PolicyRule<never, never>[];
71
- /** What we announce to it. Everything else is rejected. */
72
- announce: readonly Announced[];
73
- /** Drop RFC 1918 sources arriving from it. `privateDrops()` lists the VLANs. */
75
+ /** What we take, as its policy. Nothing when absent. */
76
+ import?: UpstreamPolicy;
77
+ /** What we send it, as its policy. */
78
+ export: UpstreamPolicy;
79
+ /** Its VLAN is listed by `privateDrops()`, for an ACL that drops RFC 1918 sources arriving from it. Nothing else is done with it. */
74
80
  drop_private?: true;
75
81
  };
76
- /** Upstreams by key. The key names everything built from it. */
82
+ /** Upstreams by key. */
77
83
  export type UpstreamCatalog = Readonly<Record<string, Upstream>>;
78
- /** A name made from an upstream: `{key}` is its key, `{KEY}` the key in capitals, `{number}` its `handoff.number` or `tunnel.number`. */
79
- type Pattern = string;
80
- /** A pattern for every upstream, or one per role. A role left out takes the default. */
81
- type ByRole = Pattern | {
82
- readonly provider?: Pattern;
83
- readonly rs?: Pattern;
84
- readonly peer?: Pattern;
84
+ /** What every upstream's policies share, written once: plain functions over each policy's rules. */
85
+ export type UpstreamOptions = {
86
+ /** Each import's rules as the device gets them, from the rules the upstream writes. The rules as written when absent. */
87
+ mapImports?: (rules: readonly PolicyRule<string, string>[], key: string, upstream: Upstream) => readonly PolicyRule<string, string>[];
88
+ /** Each export's rules as the device gets them, from the rules the upstream writes. The rules as written when absent. */
89
+ mapExports?: (rules: readonly PolicyRule<string, string>[], key: string, upstream: Upstream) => readonly PolicyRule<string, string>[];
85
90
  };
86
- /** How the names built from each upstream are made. Each one left out takes its default. */
87
- export type UpstreamNames = {
88
- /** The VLAN a handoff is on. `{key}` when absent. */
89
- vlan?: ByRole;
90
- /** The native VLAN of a handoff sent tagged. `{key}-native` when absent. */
91
- native?: Pattern;
92
- /** The router's interface: per role, and `tunnel` for one reached over a tunnel. `{key}` when absent. */
93
- interface?: Pattern | {
94
- readonly provider?: Pattern;
95
- readonly rs?: Pattern;
96
- readonly peer?: Pattern;
97
- readonly tunnel?: Pattern;
98
- };
99
- /** The import policy. `{KEY}-IN` when absent. */
100
- import?: Pattern;
101
- /** The export policy. `{KEY}-OUT` when absent. */
102
- export?: Pattern;
103
- /** The BGP group of an exchange's route servers or of multihop sessions. `{key}` when absent. */
104
- group?: Pattern;
105
- /** The description of a handoff's switch port. Its `name` when absent. No device names it, so it may be any function. */
106
- port?: (key: string, upstream: Upstream) => string;
107
- };
108
- /** How each upstream's policies are written: shared by every upstream of a catalog. */
109
- export type UpstreamPolicies<P extends string> = {
110
- /** A policy holding `Bgp.sanity()` that each import calls. The sanity rules are written in each import when absent. */
111
- sanity?: P;
112
- /** Where a route came from, by upstream: a community or a rule, such as `communityScheme(...).tag(...)`. Nothing is tagged when absent. */
113
- tag?: (key: string, upstream: Upstream) => StandardCommunity | LargeCommunity | PolicyRule<never, never>;
114
- /** Rules first in each export, such as `communityScheme(...).actions(upstream.asn)`. */
115
- actions?: (key: string, upstream: Upstream) => readonly PolicyRule<never, never>[];
116
- /** The source address for routes of its family, one per family. */
117
- sources?: readonly IP[];
118
- };
119
- /** The names a device can refer to, and their patterns when the project gives none. */
120
- declare const DEFAULTS: {
121
- readonly vlan: "{key}";
122
- readonly native: "{key}-native";
123
- readonly interface: "{key}";
124
- readonly import: "{KEY}-IN";
125
- readonly export: "{KEY}-OUT";
126
- readonly group: "{key}";
127
- };
128
- type Defaults = typeof DEFAULTS;
129
- type Field = keyof Defaults;
130
- type Key<C> = keyof C & string;
131
- /** The upstreams that land on a VLAN of ours. */
132
- type Landed<C> = {
133
- [K in Key<C>]: C[K] extends {
134
- handoff: object;
135
- } ? K : never;
136
- }[Key<C>];
137
- /** The upstreams that send their VLAN tagged. */
138
- type Tagged<C> = {
139
- [K in Key<C>]: C[K] extends {
91
+ type Values<T> = T[keyof T];
92
+ /** Each VLAN name the catalog writes. */
93
+ type VlanName<C> = Values<{
94
+ [K in keyof C]: C[K] extends {
140
95
  handoff: {
141
- native: number;
96
+ vlan: {
97
+ name: infer N extends string;
98
+ };
142
99
  };
143
- } ? K : never;
144
- }[Key<C>];
145
- /** The upstreams reached over a tunnel. */
146
- type Tunneled<C> = {
147
- [K in Key<C>]: C[K] extends {
148
- tunnel: object;
149
- } ? K : never;
150
- }[Key<C>];
151
- /** The upstreams whose sessions share a group: an exchange's route servers, or multihop sessions. */
152
- type Grouped<C> = {
153
- [K in Key<C>]: C[K] extends {
154
- role: "rs";
155
- } | {
156
- multihop: object;
157
- } ? K : never;
158
- }[Key<C>];
159
- /** The upstreams we take routes from. */
160
- type Importing<C> = {
161
- [K in Key<C>]: C[K] extends {
162
- import: object;
163
- } ? K : never;
164
- }[Key<C>];
165
- /** The project's pattern for a name, or the default. */
166
- type PatternOf<N, F extends Field> = N extends {
167
- [X in F]: infer P;
168
- } ? P : Defaults[F];
169
- /** The pattern an upstream takes: the only one, its tunnel's, its role's, or the default. */
170
- type Chosen<P, U, F extends Field> = P extends string ? P : U extends {
171
- tunnel: object;
172
- } ? P extends {
173
- tunnel: infer T extends string;
174
- } ? T : Defaults[F] : U extends {
175
- role: infer R extends string;
176
- } ? P extends {
177
- [X in R]: infer T extends string;
178
- } ? T : Defaults[F] : Defaults[F];
179
- /** Every `From` in `S` replaced. */
180
- type Replace<S extends string, From extends string, To extends string> = S extends `${infer A}${From}${infer B}` ? `${A}${To}${Replace<B, From, To>}` : S;
181
- /** An upstream's `handoff.number` or `tunnel.number`. */
182
- type NumberOf<U> = U extends {
183
- handoff: {
184
- number: infer N extends number;
185
- };
186
- } ? N : U extends {
187
- tunnel: {
188
- number: infer N extends number;
189
- };
190
- } ? N : number;
191
- /** The name built for upstream `K`: its pattern with the key, the key in capitals and its number filled in. */
192
- type Named<C, N, F extends Field, K extends Key<C>> = Replace<Replace<Replace<Chosen<PatternOf<N, F>, C[K], F>, "{key}", K>, "{KEY}", Uppercase<K>>, "{number}", `${NumberOf<C[K]>}`>;
193
- /** Each of these upstreams' names. */
194
- type Names<C, N, F extends Field, Ks extends Key<C>> = {
195
- [K in Ks]: Named<C, N, F, K>;
196
- }[Ks];
197
- /** The catalog as written, with any key the shape does not have refused, at every level. */
198
- type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret | ((...args: never[]) => unknown) ? T : S extends unknown ? {
199
- [K in keyof T]: K extends keyof S ? Exact<NonNullable<S[K]>, T[K]> : never;
200
- } : never;
201
- /** The names as written, with a name or a role Upstreams does not make refused. */
202
- type ExactNames<N> = {
203
- [F in keyof N]: F extends keyof UpstreamNames ? N[F] extends string | ((...args: never[]) => unknown) ? N[F] : {
204
- [R in keyof N[F]]: R extends "provider" | "rs" | "peer" | "tunnel" ? N[F][R] : never;
205
- } : never;
206
- };
100
+ } ? N : never;
101
+ }>;
102
+ /** Each native VLAN name. */
103
+ type NativeName<C> = Values<{
104
+ [K in keyof C]: C[K] extends {
105
+ handoff: {
106
+ native: {
107
+ name: infer N extends string;
108
+ };
109
+ };
110
+ } ? N : never;
111
+ }>;
112
+ /** Each router interface on a handoff. */
113
+ type HandoffInterface<C> = Values<{
114
+ [K in keyof C]: C[K] extends {
115
+ handoff: {
116
+ interface: infer N extends string;
117
+ };
118
+ } ? N : never;
119
+ }>;
120
+ /** Each router interface for a tunnel. */
121
+ type TunnelInterface<C> = Values<{
122
+ [K in keyof C]: C[K] extends {
123
+ tunnel: {
124
+ interface: infer N extends string;
125
+ };
126
+ } ? N : never;
127
+ }>;
128
+ /** Each policy name. */
129
+ type PolicyName<C> = Values<{
130
+ [K in keyof C]: (C[K] extends {
131
+ import: {
132
+ name: infer N extends string;
133
+ };
134
+ } ? N : never) | (C[K] extends {
135
+ export: {
136
+ name: infer N extends string;
137
+ };
138
+ } ? N : never);
139
+ }>;
140
+ /** Each BGP group name. */
141
+ type GroupName<C> = Values<{
142
+ [K in keyof C]: C[K] extends {
143
+ group: infer N extends string;
144
+ } ? N : never;
145
+ }>;
207
146
  /** A network's upstreams written once, and each device's part built from them. */
208
147
  export declare class Upstreams {
209
148
  /**
210
- * Each upstream written once: its AS, where it lands, its sessions, limits and policy. Everything a device needs is built from it, under names made from each key, so a device that names one is checked. By default:
211
- *
212
- * - VLAN `<key>`, and `<key>-native` for one that sends its VLAN tagged. The router's interface is `<key>` too, a VLAN interface or a GRE tunnel.
213
- * - Policies `<KEY>-IN` and `<KEY>-OUT`; a BGP group `<key>` for an exchange's route servers or multihop sessions.
149
+ * Each upstream written once: its AS, where it lands, its sessions, limits and policies, and every name it puts on a device. Everything a device needs is built from it:
214
150
  *
215
- * `names` gives patterns of your own, such as `{ interface: { provider: "core-ipt0{number}-{key}" } }`; see `UpstreamNames`.
151
+ * - On the router: `vlans()`, `interfaces()`, `uplinks()`, `groups()`, `neighbors()`, `policies()`, `tunnelRules()`, `addresses()`.
152
+ * - On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`.
153
+ * - For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`.
216
154
  *
217
- * On the router: `vlans()`, `interfaces()`, `uplinks()`, `groups()`, `neighbors()`, `policies({ ... })`, `tunnelRules()`, `addresses()`. On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`. For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`. Lists follow the catalog's order, where order counts on a device.
155
+ * An import or export is the rules the upstream writes, in order, passed through `mapImports` or `mapExports` when given: what every upstream shares, such as a call to a sanity policy or the community actions, goes there. Nothing else is added. Lists follow the catalog's order, where order counts on a device. A key the shape doesn't have is refused by the type, by its name.
218
156
  */
219
- static define<const C extends UpstreamCatalog, const N extends UpstreamNames = {}>(catalog: C & {
220
- [K in keyof C]: Exact<Upstream, C[K]>;
221
- }, options?: {
222
- names?: N & ExactNames<N>;
223
- }): {
224
- vlans: <const D extends string | undefined = undefined>(device?: D) => D extends string ? { [K in Names<C, N, "vlan", Landed<C>>]: Vlan; } & { [K in Names<C, N, "native", Tagged<C>>]: Vlan; } : { [K in Names<C, N, "vlan", Landed<C>>]: Vlan; };
225
- vlansOn: (device: string) => Names<C, N, "vlan", Landed<C>>[];
226
- ports: (device: string) => Record<string, Port<Names<C, N, "native", Tagged<C>> | Names<C, N, "vlan", Landed<C>>, never, never, never>>;
227
- interfaces: () => { [K in Names<C, N, "interface", Landed<C>>]: VlanInterface<Names<C, N, "vlan", Landed<C>>, never>; } & { [K in Names<C, N, "interface", Tunneled<C>>]: Gre<never>; };
228
- uplinks: () => Names<C, N, "interface", Landed<C>>[];
229
- groups: () => Record<Names<C, N, "group", Grouped<C>>, NeighborSettings<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>>>;
230
- neighbors: () => Record<string, Neighbor<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>, Names<C, N, "group", Grouped<C>>>>;
231
- policies: <const P extends string = never>(options?: UpstreamPolicies<P>) => Record<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>, PolicyRule<P, never>[]>;
232
- untrusted: () => (Names<C, N, "native", Tagged<C>> | Names<C, N, "vlan", Landed<C>>)[];
233
- natives: () => Names<C, N, "native", Tagged<C>>[];
234
- privateDrops: () => Names<C, N, "vlan", Landed<C>>[];
157
+ static define<const C extends UpstreamCatalog>(catalog: C & UpstreamCatalog, options?: UpstreamOptions): {
158
+ vlans: (device?: string) => { [K in VlanName<C> | NativeName<C>]: Vlan; };
159
+ vlansOn: (device: string) => VlanName<C>[];
160
+ ports: (device: string) => Record<string, Port<NativeName<C> | VlanName<C>, never, never, never>>;
161
+ interfaces: () => { [K in HandoffInterface<C>]: VlanInterface<VlanName<C>, never>; } & { [K in TunnelInterface<C>]: Gre<never>; };
162
+ uplinks: () => HandoffInterface<C>[];
163
+ groups: () => Record<GroupName<C>, NeighborSettings<PolicyName<C>>>;
164
+ neighbors: () => Record<string, Neighbor<PolicyName<C>, GroupName<C>>>;
165
+ policies: () => Record<PolicyName<C>, PolicyRule<never, never>[]>;
166
+ untrusted: () => (NativeName<C> | VlanName<C>)[];
167
+ natives: () => NativeName<C>[];
168
+ privateDrops: () => VlanName<C>[];
235
169
  tunnelRules: () => FilterRule<never, never>[];
236
170
  addresses: () => IP[];
237
171
  };
@@ -96,7 +96,11 @@ export type Firewall<I extends string, AS extends string> = {
96
96
  };
97
97
  /** Helpers that run. None when absent. */
98
98
  helpers?: readonly Helper[];
99
- /** Traffic to and from these prefixes passes without connection tracking. A flood toward them cannot fill the connection table, and rules see its packets in the state `untracked`. An address NAT uses inside one stays tracked, or its replies would never be translated back. Everything is tracked when absent. */
99
+ /**
100
+ * Traffic to and from these prefixes passes without connection tracking. A flood toward them cannot fill the connection table, and rules see its packets in the state `untracked`. Everything is tracked when absent.
101
+ *
102
+ * Each address NAT uses inside one of these prefixes (a source translation's `to`, a destination translation's `to` or matched `dst`) stays tracked: Circuit writes two accept rules for it, one for traffic to it and one for traffic from it, before the untracked rules. Without them, a reply to that address would skip tracking and never be translated back.
103
+ */
100
104
  untracked?: readonly Prefix[];
101
105
  };
102
106
  /** A stateless rule evaluated in hardware where traffic arrives, before routing. */
@@ -78,15 +78,22 @@ export type Sets = {
78
78
  blackhole?: true;
79
79
  };
80
80
  /**
81
- * One step of a route policy. Evaluated in this order: `call`, then `match`, then `set`, then `action`.
82
- *
83
- * A rule without `action` changes the route and moves on to the next rule.
81
+ * Run another policy at this point. A route it accepts or rejects stops there; any other route goes on to the next rule. A call holds nothing else: to call a policy for some routes only, put the match inside that policy.
84
82
  */
85
- export type PolicyRule<P extends string, S extends string> = {
83
+ export type CallRule<P extends string> = {
86
84
  /** Shown beside the rule on the device. */
87
85
  description?: string;
88
- /** Evaluate another policy first. A route it accepts or rejects stops there. */
89
- call?: P;
86
+ /** The policy to run. */
87
+ call: P;
88
+ match?: never;
89
+ set?: never;
90
+ action?: never;
91
+ };
92
+ /** Change and decide on the routes that match. A rule without `action` changes the route and moves on to the next rule. */
93
+ export type MatchRule<S extends string> = {
94
+ /** Shown beside the rule on the device. */
95
+ description?: string;
96
+ call?: never;
90
97
  /** Which routes the rule applies to. Every route when absent. */
91
98
  match?: Match<S>;
92
99
  /** What to change on a matching route. */
@@ -94,5 +101,7 @@ export type PolicyRule<P extends string, S extends string> = {
94
101
  /** Accept or reject a matching route, ending the policy for it. */
95
102
  action?: "accept" | "reject";
96
103
  };
104
+ /** One step of a route policy: a call to another policy, or a rule that matches routes, changes them and may decide on them. */
105
+ export type PolicyRule<P extends string, S extends string> = CallRule<P> | MatchRule<S>;
97
106
  /** Rules evaluated in order, first decision wins. A route no rule accepts is rejected. */
98
107
  export type RoutePolicy<P extends string, S extends string> = readonly PolicyRule<P, S>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.40.0",
3
+ "version": "0.41.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",
@@ -332,12 +332,20 @@ export class Validator {
332
332
  const sets = device.prefix_sets ?? {};
333
333
 
334
334
  for (const [name, rules] of Object.entries(policies)) {
335
- for (const rule of rules) {
335
+ rules.forEach((rule, index) => {
336
+ // A device runs a call for every route, so a match beside it would read as a condition it isn't.
337
+ const beside = (["match", "set", "action"] as const).filter((key) => rule.call !== undefined && rule[key] !== undefined);
338
+ if (beside.length) {
339
+ report(
340
+ `policy ${name} rule ${index + 1} has call and ${beside.join(", ")}: a call is a rule of its own, run for every route. Put ${beside.join(", ")} in the next rule, or the match inside ${rule.call}`
341
+ );
342
+ }
343
+
336
344
  names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in policies);
337
345
  const set = rule.match?.prefix_set;
338
346
  names.expect(`policy ${name} prefix set`, set, set !== undefined && set in sets);
339
347
  if (rule.match?.prefix) cidr(`policy ${name}`, rule.match.prefix);
340
- }
348
+ });
341
349
  }
342
350
 
343
351
  // A policy that calls itself, directly or not, never finishes.