@adviser/ovn-fabric 0.1.2 → 0.1.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 +19 -11
- package/bin/ovn-fabric.js +30 -12
- package/package.json +1 -2
- package/src/addressing.ts +0 -222
- package/src/cli.ts +0 -163
- package/src/define.ts +0 -187
- package/src/factories.ts +0 -444
- package/src/generate-netns.ts +0 -622
- package/src/generate-ovn.ts +0 -625
- package/src/types.ts +0 -408
package/src/types.ts
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
// types.ts — distinct identity types and the switchable-uplink mechanism.
|
|
2
|
-
// No topology data lives here. This file defines the SHAPE; model.ts
|
|
3
|
-
// declares the FACTS.
|
|
4
|
-
|
|
5
|
-
import { IPAddress } from "npm:ipaddress@0.2.6";
|
|
6
|
-
|
|
7
|
-
// ── distinct identity types ──────────────────────────────────────
|
|
8
|
-
// Branded types: structurally still numbers at runtime, but the type
|
|
9
|
-
// checker will not let a SegmentId be passed where an UplinkId is
|
|
10
|
-
// expected, or vice versa. Every bug from tonight's session was
|
|
11
|
-
// "correct math, applied to the wrong identifier" — a checker that
|
|
12
|
-
// treats number as number cannot catch that; two distinct branded
|
|
13
|
-
// types can.
|
|
14
|
-
|
|
15
|
-
type Brand<T, B extends string> = T & { readonly __brand: B };
|
|
16
|
-
|
|
17
|
-
export type SegmentId = Brand<number, "SegmentId">;
|
|
18
|
-
export type UplinkId = Brand<number, "UplinkId">;
|
|
19
|
-
|
|
20
|
-
export function segmentId(n: number): SegmentId {
|
|
21
|
-
if (n < 0 || n > 255) {
|
|
22
|
-
throw new RangeError(`SegmentId out of range (0-255): ${n}`);
|
|
23
|
-
}
|
|
24
|
-
return n as SegmentId;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function uplinkId(n: number): UplinkId {
|
|
28
|
-
if (n < 0 || n > 65535) {
|
|
29
|
-
throw new RangeError(`UplinkId out of range: ${n}`);
|
|
30
|
-
}
|
|
31
|
-
return n as UplinkId;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ── host / chassis ─────────────────────────────────────────────────
|
|
35
|
-
// Where a given segment's or uplink's OVN/OVS configuration is actually
|
|
36
|
-
// applied. A given deployment may run everything on one chassis, but the
|
|
37
|
-
// model does not assume that — each Segment/Uplink declares which Host
|
|
38
|
-
// it runs on, so a future topology with multiple chassis is a config
|
|
39
|
-
// change, not a redesign.
|
|
40
|
-
|
|
41
|
-
export type AccessMethod =
|
|
42
|
-
| { method: "ssh"; user: string }
|
|
43
|
-
| { method: "local" }; // the generator's own host — no SSH needed
|
|
44
|
-
|
|
45
|
-
export interface Host {
|
|
46
|
-
readonly name: string;
|
|
47
|
-
readonly address: string; // hostname or IP the generator connects to
|
|
48
|
-
readonly access: AccessMethod;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function sshHost(name: string, address: string, user: string): Host {
|
|
52
|
-
return { name, address, access: { method: "ssh", user } };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function localHost(name: string): Host {
|
|
56
|
-
return { name, address: "127.0.0.1", access: { method: "local" } };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── NetId: the identity every segment/uplink/transfer-link carries ──
|
|
60
|
-
// Backed by the `ipaddress` library (not std — see ADR discussion: std
|
|
61
|
-
// libraries lack reliable prefix-notation parsing and the 128-bit
|
|
62
|
-
// arithmetic IPv6 fold rules need). id() returns the raw numeric
|
|
63
|
-
// identifier this NetId was derived from (a segment or uplink number);
|
|
64
|
-
// vlan() returns the physical VLAN tag if one applies, or undefined —
|
|
65
|
-
// note this is a DERIVED convenience, distinct from whether the thing
|
|
66
|
-
// holding this NetId actually has an `if: { kind: "vlan", ... }` — a
|
|
67
|
-
// NetId can report a vlan() number purely because of its fold rule
|
|
68
|
-
// while the real physical attachment (see InterfaceKind below) is
|
|
69
|
-
// something else entirely (a WireGuard interface, a plain port). Don't
|
|
70
|
-
// use vlan() to decide physical wiring; use the owning Uplink/Segment's
|
|
71
|
-
// `if` field for that.
|
|
72
|
-
//
|
|
73
|
-
// NetId instances are produced by factory functions in addressing.ts
|
|
74
|
-
// (segmentNet(), uplinkNet(), transferNet()), not constructed directly
|
|
75
|
-
// here — this interface only defines the shape every factory must
|
|
76
|
-
// satisfy. The fold operation itself is string construction (see
|
|
77
|
-
// addressing.ts header comment) — IPAddress.parse() is called only
|
|
78
|
-
// after the address string is fully built.
|
|
79
|
-
|
|
80
|
-
export interface NetId {
|
|
81
|
-
readonly ipv4: IPAddress;
|
|
82
|
-
readonly ipv6: IPAddress;
|
|
83
|
-
id(): number;
|
|
84
|
-
vlan(): number | undefined;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* An address PAIR — the thing config/topology.ts actually declares per
|
|
89
|
-
* Uplink/Segment via `addresses: [...]`. Most things have exactly one
|
|
90
|
-
* NetId; the array form exists for cases like a transfer link, which
|
|
91
|
-
* conceptually carries both its OVN-side and netns-side identity.
|
|
92
|
-
*/
|
|
93
|
-
export type Addresses = readonly NetId[];
|
|
94
|
-
|
|
95
|
-
// ── physical realization ─────────────────────────────────────────
|
|
96
|
-
// HOW a Segment/Uplink actually attaches to a real wire. Deliberately
|
|
97
|
-
// separate from addressing: a NetId's vlan() can return a number purely
|
|
98
|
-
// from its fold rule while the real interface here is something else
|
|
99
|
-
// entirely (WireGuard, a bridge port with no VLAN at all). This is the
|
|
100
|
-
// split that was missing before tonight's correction — conflating
|
|
101
|
-
// "has an address" with "is a VLAN" broke as soon as WireGuard needed
|
|
102
|
-
// modelling, since a WireGuard tunnel has addresses but is not a VLAN.
|
|
103
|
-
|
|
104
|
-
/** The [Peer] stanza of a wg-quick conf — see InterfaceKind's
|
|
105
|
-
* "wireguard" variant below. */
|
|
106
|
-
export interface WireguardPeer {
|
|
107
|
-
readonly publicKey: string;
|
|
108
|
-
/** "host:port" */
|
|
109
|
-
readonly endpoint: string;
|
|
110
|
-
/** wg-quick's own comma-joined syntax, e.g. "0.0.0.0/0,::0/0" —
|
|
111
|
-
* stored as one string (not parsed here) since this generator never
|
|
112
|
-
* needs to reason about individual prefixes, only reproduce the
|
|
113
|
-
* conf file verbatim. */
|
|
114
|
-
readonly allowedIps: string;
|
|
115
|
-
readonly persistentKeepalive?: number;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** The [Interface] stanza plus its one [Peer] — everything
|
|
119
|
-
* emitWireguardInterface (generate-netns.ts) needs to reconstruct a
|
|
120
|
-
* wg-quick conf byte-for-byte. See InterfaceKind's "wireguard" variant
|
|
121
|
-
* for why the PrivateKey lives here, in a git-tracked file, rather
|
|
122
|
-
* than behind an env var/secret manager as this project's credentials
|
|
123
|
-
* normally would. */
|
|
124
|
-
export interface WireguardInterfaceConfig {
|
|
125
|
-
readonly privateKey: string;
|
|
126
|
-
/** wg-quick's own comma-joined syntax, e.g.
|
|
127
|
-
* "10.64.56.207/32,fc00:bbbb:bbbb:bb01::1:38ce/128" */
|
|
128
|
-
readonly address: string;
|
|
129
|
-
readonly listenPort?: number;
|
|
130
|
-
readonly dns?: string;
|
|
131
|
-
readonly peer: WireguardPeer;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export type InterfaceKind =
|
|
135
|
-
| {
|
|
136
|
-
kind: "vlan";
|
|
137
|
-
vlanParent: string;
|
|
138
|
-
vlanId: number;
|
|
139
|
-
/** Override the kernel subinterface name — defaults to
|
|
140
|
-
* `${vlanParent}.${vlanId}` when omitted. Needed whenever that
|
|
141
|
-
* default name is already in use for something else on the same
|
|
142
|
-
* VLAN tag (e.g. VLAN 129 already carries this host's own
|
|
143
|
-
* management IP on `ens18.129` via netplan/networkd — a second,
|
|
144
|
-
* differently-named vlan subinterface for the same tag can safely
|
|
145
|
-
* coexist and feed an OVS bridge without touching the existing
|
|
146
|
-
* one; Linux delivers matching-tagged frames to every registered
|
|
147
|
-
* vlan netdevice for a given (parent, vlanId) pair, not just one). */
|
|
148
|
-
ifaceName?: string;
|
|
149
|
-
}
|
|
150
|
-
| { kind: "physical"; name: string } // a real, untagged NIC/port
|
|
151
|
-
| { kind: "bridge-port"; bridge: string; port: string }
|
|
152
|
-
/** A real WireGuard tunnel, managed via `wg-quick` rather than
|
|
153
|
-
* hand-rolled `wg setconf`/`ip link` calls — wg-quick's own fwmark +
|
|
154
|
-
* policy-routing dance is exactly what's needed here (the tunnel's
|
|
155
|
-
* own handshake/keepalive UDP packets must keep leaving via whatever
|
|
156
|
-
* route already existed — the Backdoor below, typically — while
|
|
157
|
-
* every OTHER packet gets diverted into the tunnel), and re-deriving
|
|
158
|
-
* that by hand would just duplicate a battle-tested implementation.
|
|
159
|
-
* Confirmed live, 2026-07-06: this is genuinely how it behaves when
|
|
160
|
-
* a default route already exists in the netns before `wg-quick up`
|
|
161
|
-
* runs.
|
|
162
|
-
*
|
|
163
|
-
* `config` (see WireguardInterfaceConfig below) is declared directly
|
|
164
|
-
* in config/topology.ts and written verbatim to
|
|
165
|
-
* /etc/wireguard/<ifaceName>.conf on the target host by the
|
|
166
|
-
* generator (see emitWireguardInterface, generate-netns.ts) — INCLUDING
|
|
167
|
-
* the PrivateKey. This is a DELIBERATE, EXPLICIT exception to this
|
|
168
|
-
* project's usual "never hard-code credentials, use env vars/secret
|
|
169
|
-
* managers" policy, made 2026-07-06 after being asked to confirm:
|
|
170
|
-
* the tradeoff (a real credential living in a git-tracked source
|
|
171
|
-
* file) was accepted on purpose, not overlooked. Treat topology.ts
|
|
172
|
-
* — and any generated script built from it — with the same care as
|
|
173
|
-
* any other credential-bearing file (this repo's git-safety rules
|
|
174
|
-
* around scanning for secrets before any commit/push still apply in
|
|
175
|
-
* full).
|
|
176
|
-
*
|
|
177
|
-
* `ifaceName` is the real kernel interface name AND the .conf's
|
|
178
|
-
* basename on disk — defaults to this uplink's OWN name (see
|
|
179
|
-
* uplinkWireguard, factories.ts, which threads UplinkBuilder's
|
|
180
|
-
* `name` parameter through), overridable when that default doesn't
|
|
181
|
-
* fit: it exceeds IFNAMSIZ (15 usable characters), or would rename
|
|
182
|
-
* an already-running real interface unnecessarily. */
|
|
183
|
-
| {
|
|
184
|
-
kind: "wireguard";
|
|
185
|
-
ifaceName: string;
|
|
186
|
-
config: WireguardInterfaceConfig;
|
|
187
|
-
}
|
|
188
|
-
/** A placeholder Linux dummy interface — no real backing device, no
|
|
189
|
-
* real-world connectivity. Stands in for an uplink whose real
|
|
190
|
-
* mechanism (e.g. a WireGuard tunnel) isn't built yet, so the rest of
|
|
191
|
-
* the chain (OVN router, transfer link, backbone join, back-routes)
|
|
192
|
-
* can be wired up and tested end-to-end first. No `name` field, on
|
|
193
|
-
* purpose: unlike "vlan"/"physical", a dummy interface has no
|
|
194
|
-
* pre-existing real-world name to preserve — the generator derives
|
|
195
|
-
* and creates it itself, at a slot-based name (see generate-netns.ts,
|
|
196
|
-
* dummyIface()), the same IFNAMSIZ-safe convention already used for
|
|
197
|
-
* every other uplink-owned kernel interface (veth-ovn-N, veth-krn-N,
|
|
198
|
-
* br-up-N). See WireGuard design discussion, 2026-07-06. */
|
|
199
|
-
| { kind: "dummy" };
|
|
200
|
-
|
|
201
|
-
// ── NAT ────────────────────────────────────────────────────────────
|
|
202
|
-
// Per-stack, since a segment/uplink might need v4 masquerade but not
|
|
203
|
-
// v6 (the common case once real delegated IPv6 prefixes exist — see
|
|
204
|
-
// ADR 0001 consequence notes on DHCPv6-PD vs NAT66).
|
|
205
|
-
|
|
206
|
-
export type NatRule = { readonly kind: "masq" };
|
|
207
|
-
|
|
208
|
-
export interface Nat {
|
|
209
|
-
readonly ipv4?: readonly NatRule[];
|
|
210
|
-
readonly ipv6?: readonly NatRule[];
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ── discovery ──────────────────────────────────────────────────────
|
|
214
|
-
// HOW this Uplink/Segment's real-world address is learned, per stack.
|
|
215
|
-
// "static" means the NetId's address IS the real address, nothing to
|
|
216
|
-
// discover. This is what determines which mechanism runs inside an
|
|
217
|
-
// uplink's netns (see ADR 0001 — dhclient supervision, SLAAC accept_ra
|
|
218
|
-
// handling, etc.) — addressing.ts and define.ts do not need to know
|
|
219
|
-
// about discovery; it's read by the (not yet built) generation layer.
|
|
220
|
-
|
|
221
|
-
/** Which real userspace program acquires this uplink's IPv4 lease when
|
|
222
|
-
* discovery.ipv4 is "dhcp". Defaults to "dhclient" (the only client
|
|
223
|
-
* used so far, and the one confirmed live). "dhcpcd" is the same
|
|
224
|
-
* "dhcp" discovery KIND, a different PROGRAM doing it — same
|
|
225
|
-
* idempotent "already running? no-op : start" shape, different
|
|
226
|
-
* command line (see generate-netns.ts, emitIpv4Discovery). "static"
|
|
227
|
-
* is different again: not a program at all, just "configure this
|
|
228
|
-
* fixed address and gateway directly" — added 2026-07-06 for a real
|
|
229
|
-
* uplink whose real-world address is known and stable (e.g. a
|
|
230
|
-
* reserved LAN IP on the ISP router) rather than DHCP-leased. Kept as
|
|
231
|
-
* its own field rather than folded into the ipv4 union so a future
|
|
232
|
-
* WireGuard uplink — not "dhcp" at all, its own InterfaceKind branch
|
|
233
|
-
* entirely (see WireGuard design discussion, 2026-07-06) — never has
|
|
234
|
-
* to touch this dance. */
|
|
235
|
-
export type DhcpClient = "dhclient" | "dhcpcd" | "static";
|
|
236
|
-
|
|
237
|
-
/** The fixed address+prefix and default gateway to configure directly
|
|
238
|
-
* on a real interface when Discovery.client is "static" — see
|
|
239
|
-
* emitStaticIpv4 (generate-netns.ts). Only consulted then; every other
|
|
240
|
-
* client ignores it. */
|
|
241
|
-
export interface StaticIpv4 {
|
|
242
|
-
/** e.g. "192.0.2.93/24" */
|
|
243
|
-
readonly address: string;
|
|
244
|
-
/** e.g. "192.0.2.1" */
|
|
245
|
-
readonly gateway: string;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export interface Discovery {
|
|
249
|
-
readonly ipv4?: "static" | "dhcp";
|
|
250
|
-
readonly ipv6?: "static" | "slaac";
|
|
251
|
-
/** Defaults to "dhclient" when ipv4 is "dhcp" and no client is
|
|
252
|
-
* given; otherwise (ipv4 "static" with no explicit client — e.g. a
|
|
253
|
-
* backdoor's merged dummy interface, see Backdoor below) nothing
|
|
254
|
-
* runs here at all. */
|
|
255
|
-
readonly client?: DhcpClient;
|
|
256
|
-
/** Only consulted when client === "static". See StaticIpv4. */
|
|
257
|
-
readonly static4?: StaticIpv4;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ── backdoor: borrowed egress for a VPN-like uplink ─────────────────
|
|
261
|
-
// Any uplink with no real interface of its own (dummy today; WireGuard,
|
|
262
|
-
// ZeroTier, Tailscale, ... tomorrow — anything tunnel-shaped) still
|
|
263
|
-
// needs a mundane, unencrypted path to the real internet: something has
|
|
264
|
-
// to carry the tunnel's own setup/keepalive traffic, separate from
|
|
265
|
-
// whatever the tunnel itself eventually carries. A backdoor is exactly
|
|
266
|
-
// that: a second, dedicated transfer-link-shaped connection from this
|
|
267
|
-
// uplink's OWN netns into an ALREADY-real uplink's router (`via`),
|
|
268
|
-
// borrowing its egress instead of duplicating one.
|
|
269
|
-
//
|
|
270
|
-
// Deliberately generic — this is not a WireGuard-specific concept, it's
|
|
271
|
-
// what ANY VPN-shaped uplink needs (originally built by hand for one
|
|
272
|
-
// specific VPN uplink borrowing a plain uplink's egress, then
|
|
273
|
-
// generalized here).
|
|
274
|
-
//
|
|
275
|
-
// `addresses`/`slot` here are the backdoor's OWN dedicated /28 — NOT
|
|
276
|
-
// the owning uplink's own `addresses` (that's its front-door transfer
|
|
277
|
-
// link to ITS OWN router). Drawing them from a genuinely separate slot
|
|
278
|
-
// is required, not optional: sharing the front-door's /28 (both links'
|
|
279
|
-
// addresses inside the SAME subnet, on two different netns interfaces)
|
|
280
|
-
// was tried and is broken — Linux ends up with two equally-specific
|
|
281
|
-
// connected routes for the one prefix, on two different devices, and
|
|
282
|
-
// which one actually wins is unreliable, not a real design. Confirmed
|
|
283
|
-
// live, this session — a ping "worked" against the shared-subnet
|
|
284
|
-
// version, but for the wrong reason, not because the intended path
|
|
285
|
-
// (through `via`'s router) was actually the one carrying it.
|
|
286
|
-
export interface Backdoor {
|
|
287
|
-
/** The real, already-working uplink this borrows egress from (e.g.
|
|
288
|
-
* isp-primary). Must already be declared — see NetworkBuilder.uplink(). */
|
|
289
|
-
readonly via: Uplink;
|
|
290
|
-
/** This backdoor's own transfer-link addresses (OVN-side, netns-side)
|
|
291
|
-
* — drawn from the same global slot sequence as every other transfer
|
|
292
|
-
* link (see NetworkBuilder), so it can never collide with one. */
|
|
293
|
-
readonly addresses: Addresses;
|
|
294
|
-
/** The slot this backdoor consumed — used to derive its own
|
|
295
|
-
* IFNAMSIZ-safe kernel interface/bridge names, same convention as
|
|
296
|
-
* uplinkTransferBridge()/vethOvn()/vethNetns() (generate-netns.ts). */
|
|
297
|
-
readonly slot: number;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ── Uplink ───────────────────────────────────────────────────────────
|
|
301
|
-
|
|
302
|
-
export interface Uplink {
|
|
303
|
-
/** Unique per network — also used directly as the prefix for every
|
|
304
|
-
* generated OVN object name (sw-<name>, router-<name>, lrp-<name>,
|
|
305
|
-
* ...). Uniqueness is enforced by NetworkBuilder (see define.ts). */
|
|
306
|
-
readonly name: string;
|
|
307
|
-
/** The small sequential index NetworkBuilder assigned this uplink
|
|
308
|
-
* (0-4095), used for BOTH the transfer-link IPv4 block (transferNet)
|
|
309
|
-
* and the backbone-leg IPv4 block (uplinkBackboneNet) — kept on the
|
|
310
|
-
* resolved object so tier-2 generation can recover it without
|
|
311
|
-
* re-deriving it from an already-computed address. */
|
|
312
|
-
readonly slot: number;
|
|
313
|
-
readonly addresses: Addresses;
|
|
314
|
-
readonly if: InterfaceKind;
|
|
315
|
-
readonly nat?: Nat;
|
|
316
|
-
readonly discovery?: Discovery;
|
|
317
|
-
/** Borrowed egress for a VPN-like uplink with no real interface of
|
|
318
|
-
* its own — see Backdoor above. Undefined for every uplink that has
|
|
319
|
-
* real connectivity itself (a VLAN uplink, a physical NIC, a working
|
|
320
|
-
* VPN tunnel, ...). */
|
|
321
|
-
readonly backdoor?: Backdoor;
|
|
322
|
-
readonly host: Host;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ── switchable uplink selection ───────────────────────────────────
|
|
326
|
-
// A segment does not hold a fixed Uplink reference. It holds an
|
|
327
|
-
// UplinkSelector — something that can be asked "which uplink right
|
|
328
|
-
// now" — so the generator can support failover/manual-switch later
|
|
329
|
-
// without changing the Segment type or any derivation logic that
|
|
330
|
-
// consumes it. Three selector strategies are provided; all of them
|
|
331
|
-
// satisfy the same interface, so emit-time code only ever calls
|
|
332
|
-
// `.resolve()` and never needs to know which strategy is in play.
|
|
333
|
-
|
|
334
|
-
export interface UplinkSelector {
|
|
335
|
-
resolve(): Uplink;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** Always the same uplink. The common case, and tonight's actual need. */
|
|
339
|
-
export class FixedUplink implements UplinkSelector {
|
|
340
|
-
constructor(private readonly uplink: Uplink) {}
|
|
341
|
-
resolve(): Uplink {
|
|
342
|
-
return this.uplink;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Picks the first uplink in priority order whose `isAvailable` callback
|
|
348
|
-
* returns true. `isAvailable` is injected, not hardcoded — at generation
|
|
349
|
-
* time it might always return true (no live-state check, "as designed"
|
|
350
|
-
* output); at a future runtime-aware stage it could call a data-source
|
|
351
|
-
* plugin (see ADR 0001 §5) to check a real lease/handshake state.
|
|
352
|
-
*/
|
|
353
|
-
export class PriorityUplink implements UplinkSelector {
|
|
354
|
-
constructor(
|
|
355
|
-
private readonly candidates: readonly Uplink[],
|
|
356
|
-
private readonly isAvailable: (u: Uplink) => boolean = () => true,
|
|
357
|
-
) {
|
|
358
|
-
if (candidates.length === 0) {
|
|
359
|
-
throw new Error("PriorityUplink requires at least one candidate");
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
resolve(): Uplink {
|
|
363
|
-
const found = this.candidates.find((u) => this.isAvailable(u));
|
|
364
|
-
return found ?? this.candidates[0];
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Explicit manual override — for an operator-driven "switch to X now". */
|
|
369
|
-
export class ManualUplink implements UplinkSelector {
|
|
370
|
-
private current: Uplink;
|
|
371
|
-
constructor(initial: Uplink) {
|
|
372
|
-
this.current = initial;
|
|
373
|
-
}
|
|
374
|
-
resolve(): Uplink {
|
|
375
|
-
return this.current;
|
|
376
|
-
}
|
|
377
|
-
switchTo(uplink: Uplink): void {
|
|
378
|
-
this.current = uplink;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// ── Segment ──────────────────────────────────────────────────────────
|
|
383
|
-
|
|
384
|
-
export interface Segment {
|
|
385
|
-
/** Unique per network — also used directly as the prefix for every
|
|
386
|
-
* generated OVN object name (sw-<name>, router-<name>, lrp-<name>,
|
|
387
|
-
* ...). Uniqueness is enforced by NetworkBuilder (see define.ts). */
|
|
388
|
-
readonly name: string;
|
|
389
|
-
readonly addresses: Addresses;
|
|
390
|
-
readonly if: InterfaceKind;
|
|
391
|
-
/** Undefined means "no egress yet" — deliberately, not a bug: a
|
|
392
|
-
* segment meant to eventually exit via an uplink that doesn't exist
|
|
393
|
-
* yet (e.g. a VPN WireGuard tunnel not built out) should have NO
|
|
394
|
-
* backbone join, NO route, and NO NAT generated for it at all, not
|
|
395
|
-
* be silently routed out whichever uplink happens to be declared —
|
|
396
|
-
* confirmed live: two VPN-bound segments were provisionally pointed
|
|
397
|
-
* at the general default uplink and got MASQUERADEd out alongside
|
|
398
|
-
* another segment, defeating the whole point of routing them
|
|
399
|
-
* through a separate VPN egress later. See emitSegmentBackboneJoin
|
|
400
|
-
* (generate-ovn.ts), which returns no lines at all when this is
|
|
401
|
-
* undefined. */
|
|
402
|
-
readonly uplink?: UplinkSelector;
|
|
403
|
-
readonly nat?: Nat;
|
|
404
|
-
/** Whether OVN advertises RA/SLAAC for this segment's IPv6 prefix so
|
|
405
|
-
* clients self-configure a global address (see generate-ovn.ts). */
|
|
406
|
-
readonly slaac: boolean;
|
|
407
|
-
readonly host: Host;
|
|
408
|
-
}
|