@adviser/ovn-fabric 0.1.2 → 0.1.4
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 +21 -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/define.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
// src/define.ts — defineNetwork(): a Vite-defineConfig / Jest-describe
|
|
2
|
-
// style builder. Lets config/topology.ts read as a declaration rather
|
|
3
|
-
// than a set of raw object literals, and validates as it goes (e.g. a
|
|
4
|
-
// segment can only reference an uplink that was already declared in
|
|
5
|
-
// the same defineNetwork call).
|
|
6
|
-
|
|
7
|
-
import type { UplinkBuilder } from "./factories.ts";
|
|
8
|
-
import {
|
|
9
|
-
FixedUplink,
|
|
10
|
-
type Host,
|
|
11
|
-
localHost,
|
|
12
|
-
type Segment,
|
|
13
|
-
sshHost,
|
|
14
|
-
type Uplink,
|
|
15
|
-
type UplinkSelector,
|
|
16
|
-
} from "./types.ts";
|
|
17
|
-
|
|
18
|
-
export interface NetworkDefinition {
|
|
19
|
-
readonly name: string;
|
|
20
|
-
readonly allUplinks: readonly Uplink[];
|
|
21
|
-
readonly allSegments: readonly Segment[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* net.uplink(name, uplinkVlan({...})) / net.uplink(name, uplinkPhysical({...})) —
|
|
26
|
-
* the second argument is an UplinkBuilder: a function of `slot` that a
|
|
27
|
-
* factory in factories.ts returned. NetworkBuilder assigns the next
|
|
28
|
-
* free slot (0-4095, see addressing.ts transferNet()) and calls it —
|
|
29
|
-
* config/topology.ts never sees or chooses a slot itself, so two
|
|
30
|
-
* uplinks can never collide on the same transfer-link block.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
type SegmentSpec = Omit<Segment, "name">;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The builder context passed into defineNetwork's callback. Each method
|
|
37
|
-
* both registers the declared thing and returns a handle to it, so later
|
|
38
|
-
* calls in the same callback can reference earlier ones directly
|
|
39
|
-
* (`net.segment("home", { uplink: avm, ... })`), the same way Jest's
|
|
40
|
-
* `describe`/`it` or Vite's defineConfig read top-to-bottom.
|
|
41
|
-
*/
|
|
42
|
-
export class NetworkBuilder {
|
|
43
|
-
private readonly uplinksByName = new Map<string, Uplink>();
|
|
44
|
-
private readonly usedUplinkIds = new Set<number>();
|
|
45
|
-
private nextSlot = 0;
|
|
46
|
-
private readonly segmentsByName = new Map<string, Segment>();
|
|
47
|
-
private readonly usedSegmentIds = new Set<number>();
|
|
48
|
-
private readonly hostsByName = new Map<string, Host>();
|
|
49
|
-
|
|
50
|
-
/** Declare a host reachable via SSH. Returns a handle for reuse. */
|
|
51
|
-
sshHost(name: string, address: string, user: string): Host {
|
|
52
|
-
if (this.hostsByName.has(name)) {
|
|
53
|
-
throw new Error(`host "${name}" declared more than once`);
|
|
54
|
-
}
|
|
55
|
-
const host = sshHost(name, address, user);
|
|
56
|
-
this.hostsByName.set(name, host);
|
|
57
|
-
return host;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Declare the generator's own host — no SSH needed. */
|
|
61
|
-
localHost(name: string): Host {
|
|
62
|
-
if (this.hostsByName.has(name)) {
|
|
63
|
-
throw new Error(`host "${name}" declared more than once`);
|
|
64
|
-
}
|
|
65
|
-
const host = localHost(name);
|
|
66
|
-
this.hostsByName.set(name, host);
|
|
67
|
-
return host;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
uplink(name: string, builder: UplinkBuilder): Uplink {
|
|
71
|
-
if (this.uplinksByName.has(name)) {
|
|
72
|
-
throw new Error(`uplink "${name}" declared more than once`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Passed as an allocator, not a single slot: most uplinks call this
|
|
76
|
-
// once (their own front-door transfer link), but one with a
|
|
77
|
-
// `backdoor` (see Backdoor, types.ts) calls it again for the
|
|
78
|
-
// backdoor's own, separate slot — same sequential pool either way,
|
|
79
|
-
// so nothing declared via this builder can ever collide on a slot.
|
|
80
|
-
const allocSlot = (): number => {
|
|
81
|
-
if (this.nextSlot > 4095) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
"no transfer-link slots remaining (4096 max, see addressing.ts " +
|
|
84
|
-
"transferNet()) — this network has more uplinks (and/or " +
|
|
85
|
-
"backdoors) than the 10.99.0.0/16 transfer-link space can " +
|
|
86
|
-
"address",
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
return this.nextSlot++;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const spec = builder(allocSlot, name);
|
|
93
|
-
|
|
94
|
-
const id = spec.addresses[0]?.id();
|
|
95
|
-
if (id !== undefined && this.usedUplinkIds.has(id)) {
|
|
96
|
-
const existing = [...this.uplinksByName.entries()]
|
|
97
|
-
.find(([, u]) => u.addresses[0]?.id() === id)?.[0];
|
|
98
|
-
throw new Error(
|
|
99
|
-
`uplink "${name}" reuses id ${id}, ` +
|
|
100
|
-
`already used by uplink "${existing}"`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Same "must already be declared via this builder" fail-fast as
|
|
105
|
-
// segment()'s uplink check below — a backdoor's `via` has to be a
|
|
106
|
-
// real, already-registered uplink, not an arbitrary object that
|
|
107
|
-
// happens to match the Uplink shape (and, since it must be
|
|
108
|
-
// declared BEFORE this one, its own emitUplinkTransfer/netns setup
|
|
109
|
-
// is guaranteed to already exist by the time this uplink's backdoor
|
|
110
|
-
// is emitted — see generate-ovn.ts, scriptForHost).
|
|
111
|
-
if (
|
|
112
|
-
spec.backdoor !== undefined &&
|
|
113
|
-
!this.uplinksByName.has(spec.backdoor.via.name)
|
|
114
|
-
) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`uplink "${name}" has a backdoor via "${spec.backdoor.via.name}", ` +
|
|
117
|
-
`which was not declared via net.uplink() before this uplink`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const uplink: Uplink = { name, ...spec };
|
|
122
|
-
|
|
123
|
-
this.uplinksByName.set(name, uplink);
|
|
124
|
-
if (id !== undefined) this.usedUplinkIds.add(id);
|
|
125
|
-
return uplink;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
segment(name: string, spec: SegmentSpec): Segment {
|
|
129
|
-
if (this.segmentsByName.has(name)) {
|
|
130
|
-
throw new Error(`segment "${name}" declared more than once`);
|
|
131
|
-
}
|
|
132
|
-
const id = spec.addresses[0]?.id();
|
|
133
|
-
if (id !== undefined && this.usedSegmentIds.has(id)) {
|
|
134
|
-
const existing = [...this.segmentsByName.entries()]
|
|
135
|
-
.find(([, s]) => s.addresses[0]?.id() === id)?.[0];
|
|
136
|
-
throw new Error(
|
|
137
|
-
`segment "${name}" reuses id ${id}, ` +
|
|
138
|
-
`already used by segment "${existing}"`,
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// undefined means "no uplink yet, deliberately isolated" (see
|
|
143
|
-
// Segment.uplink, types.ts) — not every segment resolves to
|
|
144
|
-
// something, so this must be checked before the "resolve" in
|
|
145
|
-
// probe below, which throws on undefined.
|
|
146
|
-
const selector: UplinkSelector | undefined = spec.uplink === undefined
|
|
147
|
-
? undefined
|
|
148
|
-
: ("resolve" in spec.uplink ? spec.uplink : new FixedUplink(spec.uplink));
|
|
149
|
-
|
|
150
|
-
// fail fast: if an uplink WAS given, it must have been declared via
|
|
151
|
-
// this same builder, not an arbitrary object that happens to match
|
|
152
|
-
// the Uplink shape.
|
|
153
|
-
if (selector !== undefined) {
|
|
154
|
-
const resolved = selector.resolve();
|
|
155
|
-
if (!this.uplinksByName.has(resolved.name)) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`segment "${name}" references an uplink ("${resolved.name}") ` +
|
|
158
|
-
`that was not declared via net.uplink() in this defineNetwork call`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const segment: Segment = { name, ...spec, uplink: selector };
|
|
164
|
-
|
|
165
|
-
this.segmentsByName.set(name, segment);
|
|
166
|
-
if (id !== undefined) this.usedSegmentIds.add(id);
|
|
167
|
-
return segment;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** @internal used by defineNetwork to extract the final declarations */
|
|
171
|
-
build(name: string): NetworkDefinition {
|
|
172
|
-
return {
|
|
173
|
-
name,
|
|
174
|
-
allUplinks: [...this.uplinksByName.values()],
|
|
175
|
-
allSegments: [...this.segmentsByName.values()],
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function defineNetwork(
|
|
181
|
-
name: string,
|
|
182
|
-
build: (net: NetworkBuilder) => void,
|
|
183
|
-
): NetworkDefinition {
|
|
184
|
-
const builder = new NetworkBuilder();
|
|
185
|
-
build(builder);
|
|
186
|
-
return builder.build(name);
|
|
187
|
-
}
|
package/src/factories.ts
DELETED
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
// src/factories.ts — sparse-input factories for Uplink/Segment.
|
|
2
|
-
//
|
|
3
|
-
// net.uplink(name, uplinkVlan({ ... })) reads as: "this uplink is realized as
|
|
4
|
-
// a VLAN; here are the few facts that vary." Every factory here takes a
|
|
5
|
-
// sparse, optional-heavy input and returns an UplinkBuilder — a function
|
|
6
|
-
// of `slot` (the transfer-link /28 block index, see addressing.ts
|
|
7
|
-
// transferNet()) producing a FULLY RESOLVED object with no optional
|
|
8
|
-
// fields left unset. `slot` is deliberately NOT something the caller
|
|
9
|
-
// supplies in config/topology.ts: NetworkBuilder.uplink() (define.ts)
|
|
10
|
-
// assigns slots sequentially in declaration order and calls this
|
|
11
|
-
// function itself, so a config never has to think about slot allocation
|
|
12
|
-
// or its 0-4095 range at all — and two uplinks can never collide on the
|
|
13
|
-
// same slot, since each is assigned exactly once by NetworkBuilder.
|
|
14
|
-
//
|
|
15
|
-
// Addresses are computed from `id` via addressing.ts's fold rules
|
|
16
|
-
// unless the caller asks for "dhcp"/"slaac" discovery instead, in which
|
|
17
|
-
// case the corresponding stack's address is left to be learned at
|
|
18
|
-
// runtime (see ADR 0001 — the netns owns that, not this generator).
|
|
19
|
-
//
|
|
20
|
-
// This is the layer config/topology.ts actually calls. define.ts's
|
|
21
|
-
// NetworkBuilder.uplink()/segment() accept the UplinkBuilder/Segment
|
|
22
|
-
// these factories produce — they do not know about "vlan" vs "physical"
|
|
23
|
-
// vs sparse input at all.
|
|
24
|
-
|
|
25
|
-
import { segmentNet, transferNet } from "./addressing.ts";
|
|
26
|
-
import type {
|
|
27
|
-
Addresses,
|
|
28
|
-
Backdoor,
|
|
29
|
-
DhcpClient,
|
|
30
|
-
Discovery,
|
|
31
|
-
Host,
|
|
32
|
-
InterfaceKind,
|
|
33
|
-
Nat,
|
|
34
|
-
StaticIpv4,
|
|
35
|
-
Uplink,
|
|
36
|
-
WireguardInterfaceConfig,
|
|
37
|
-
} from "./types.ts";
|
|
38
|
-
|
|
39
|
-
/** A factory result still waiting on its assigned transfer-link
|
|
40
|
-
* slot(s). Takes an ALLOCATOR, not a bare slot number: most uplinks
|
|
41
|
-
* consume exactly one slot (their own front-door transfer link), but
|
|
42
|
-
* an uplink with a `backdoor` (see types.ts) needs a SECOND, entirely
|
|
43
|
-
* separate slot for it — sharing the front-door's /28 was tried and is
|
|
44
|
-
* broken (see Backdoor's doc comment). Calling allocSlot() again for
|
|
45
|
-
* the backdoor, from the same sequential pool NetworkBuilder already
|
|
46
|
-
* guarantees uniqueness over, is what keeps the two from ever
|
|
47
|
-
* colliding.
|
|
48
|
-
*
|
|
49
|
-
* Also receives `name` — the SAME string given to net.uplink(name,
|
|
50
|
-
* ...) — so a factory that needs a human-facing identifier of its own
|
|
51
|
-
* (e.g. uplinkWireguard's real interface/conf-file name) can reuse it
|
|
52
|
-
* instead of asking for a second, redundant one. Most factories ignore
|
|
53
|
-
* it entirely (JS/TS both allow a callback to declare fewer params
|
|
54
|
-
* than its type permits), which is why this was added as a second
|
|
55
|
-
* parameter rather than changed in place — every existing builder
|
|
56
|
-
* above keeps working unmodified. */
|
|
57
|
-
export type UplinkBuilder = (
|
|
58
|
-
allocSlot: () => number,
|
|
59
|
-
name: string,
|
|
60
|
-
) => Omit<Uplink, "name">;
|
|
61
|
-
import {
|
|
62
|
-
FixedUplink,
|
|
63
|
-
segmentId,
|
|
64
|
-
type Segment,
|
|
65
|
-
uplinkId,
|
|
66
|
-
type UplinkSelector,
|
|
67
|
-
} from "./types.ts";
|
|
68
|
-
|
|
69
|
-
// ── shared per-stack address-or-discovery input ─────────────────────
|
|
70
|
-
// Each stack is either omitted (defaults to "dhcp"/"slaac" — the common
|
|
71
|
-
// case for a real WAN uplink, which has no fixed address to compute),
|
|
72
|
-
// or explicitly "dhcp"/"slaac" (same as omitted, written out for
|
|
73
|
-
// clarity), or a literal already-known IPAddress-parseable string for
|
|
74
|
-
// cases like a cable/DSL uplink's historically-stable lease.
|
|
75
|
-
|
|
76
|
-
interface AddressSpec {
|
|
77
|
-
readonly ipv4?: "dhcp" | string;
|
|
78
|
-
readonly ipv6?: "slaac" | string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function resolveDiscovery(
|
|
82
|
-
spec: AddressSpec | undefined,
|
|
83
|
-
client?: DhcpClient,
|
|
84
|
-
static4?: StaticIpv4,
|
|
85
|
-
): Discovery {
|
|
86
|
-
// client === "static" implies ipv4 "static" outright — the caller
|
|
87
|
-
// doesn't also have to remember to set addresses.ipv4: "static"
|
|
88
|
-
// separately for it to take effect (see emitIpv4Discovery,
|
|
89
|
-
// generate-netns.ts).
|
|
90
|
-
const ipv4 = client === "static"
|
|
91
|
-
? "static"
|
|
92
|
-
: spec?.ipv4 === undefined || spec.ipv4 === "dhcp"
|
|
93
|
-
? "dhcp"
|
|
94
|
-
: "static";
|
|
95
|
-
const ipv6 = spec?.ipv6 === undefined || spec.ipv6 === "slaac"
|
|
96
|
-
? "slaac"
|
|
97
|
-
: "static";
|
|
98
|
-
return { ipv4, ipv6, client, static4 };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── vlan() ───────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
interface VlanUplinkInput {
|
|
104
|
-
readonly id: string | number;
|
|
105
|
-
readonly vlanParent: string;
|
|
106
|
-
/** Override the VLAN tag if it should differ from `id`. Defaults to id. */
|
|
107
|
-
readonly vlan?: number;
|
|
108
|
-
/** Override the kernel subinterface name — see InterfaceKind
|
|
109
|
-
* (types.ts). Only needed if `${vlanParent}.${vlanId}` is already in
|
|
110
|
-
* use for something else. */
|
|
111
|
-
readonly ifaceName?: string;
|
|
112
|
-
readonly nat?: Nat;
|
|
113
|
-
readonly host: Host;
|
|
114
|
-
readonly addresses?: AddressSpec;
|
|
115
|
-
/** Which program acquires the IPv4 lease when addresses.ipv4 is
|
|
116
|
-
* "dhcp" (or omitted, same default). Defaults to "dhclient" — see
|
|
117
|
-
* DhcpClient (types.ts). Set to "static" (with `static4` below) for
|
|
118
|
-
* a real interface whose address is fixed/known rather than leased. */
|
|
119
|
-
readonly client?: DhcpClient;
|
|
120
|
-
/** Only consulted when client === "static" — see StaticIpv4 (types.ts). */
|
|
121
|
-
readonly static4?: StaticIpv4;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function uplinkVlan(input: VlanUplinkInput): UplinkBuilder {
|
|
125
|
-
const id = uplinkId(
|
|
126
|
-
typeof input.id === "string" ? Number.parseInt(input.id, 10) : input.id,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
return (allocSlot: () => number) => {
|
|
130
|
-
const slot = allocSlot();
|
|
131
|
-
const ifc: InterfaceKind = {
|
|
132
|
-
kind: "vlan",
|
|
133
|
-
vlanParent: input.vlanParent,
|
|
134
|
-
vlanId: input.vlan ?? id,
|
|
135
|
-
ifaceName: input.ifaceName,
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const addresses: Addresses = [
|
|
139
|
-
transferNet(id, slot, 1), // OVN side
|
|
140
|
-
transferNet(id, slot, 2), // netns side
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
slot,
|
|
145
|
-
addresses,
|
|
146
|
-
if: ifc,
|
|
147
|
-
nat: input.nat,
|
|
148
|
-
discovery: resolveDiscovery(input.addresses, input.client, input.static4),
|
|
149
|
-
host: input.host,
|
|
150
|
-
};
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ── physical() ───────────────────────────────────────────────────────
|
|
155
|
-
// The raw case: a real, untagged interface (e.g. { name: "ens19" }).
|
|
156
|
-
// No VLAN math, no transfer-link addressing — used for an uplink
|
|
157
|
-
// (or segment) that's wired directly, no 802.1Q tagging involved.
|
|
158
|
-
|
|
159
|
-
interface PhysicalUplinkInput {
|
|
160
|
-
readonly id: string | number;
|
|
161
|
-
readonly name: string;
|
|
162
|
-
readonly nat?: Nat;
|
|
163
|
-
readonly host: Host;
|
|
164
|
-
readonly addresses?: AddressSpec;
|
|
165
|
-
/** Which program acquires the IPv4 lease when addresses.ipv4 is
|
|
166
|
-
* "dhcp" (or omitted, same default). Defaults to "dhclient" — see
|
|
167
|
-
* DhcpClient (types.ts). Set to "static" (with `static4` below) for
|
|
168
|
-
* a real interface whose address is fixed/known rather than leased. */
|
|
169
|
-
readonly client?: DhcpClient;
|
|
170
|
-
/** Only consulted when client === "static" — see StaticIpv4 (types.ts). */
|
|
171
|
-
readonly static4?: StaticIpv4;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function uplinkPhysical(input: PhysicalUplinkInput): UplinkBuilder {
|
|
175
|
-
const id = uplinkId(
|
|
176
|
-
typeof input.id === "string" ? Number.parseInt(input.id, 10) : input.id,
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return (allocSlot: () => number) => {
|
|
180
|
-
const slot = allocSlot();
|
|
181
|
-
const ifc: InterfaceKind = { kind: "physical", name: input.name };
|
|
182
|
-
|
|
183
|
-
const addresses: Addresses = [
|
|
184
|
-
transferNet(id, slot, 1),
|
|
185
|
-
transferNet(id, slot, 2),
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
slot,
|
|
190
|
-
addresses,
|
|
191
|
-
if: ifc,
|
|
192
|
-
nat: input.nat,
|
|
193
|
-
discovery: resolveDiscovery(input.addresses, input.client, input.static4),
|
|
194
|
-
host: input.host,
|
|
195
|
-
};
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ── dummy() ──────────────────────────────────────────────────────────
|
|
200
|
-
// A placeholder uplink with no real backing interface — see
|
|
201
|
-
// InterfaceKind's "dummy" variant (types.ts). Lets the OVN
|
|
202
|
-
// router/transfer-link/backbone-join chain be built and pinged
|
|
203
|
-
// end-to-end before the real mechanism (e.g. a WireGuard tunnel)
|
|
204
|
-
// exists. No `name`/`vlanParent` to supply — the generator derives and
|
|
205
|
-
// creates the actual kernel interface itself, at a slot-based name.
|
|
206
|
-
|
|
207
|
-
interface DummyUplinkInput {
|
|
208
|
-
readonly id: string | number;
|
|
209
|
-
readonly nat?: Nat;
|
|
210
|
-
readonly host: Host;
|
|
211
|
-
readonly addresses?: AddressSpec;
|
|
212
|
-
readonly client?: DhcpClient;
|
|
213
|
-
/** Borrow real egress from another, already-working uplink — see
|
|
214
|
-
* Backdoor (types.ts). Needed for any VPN-like uplink (WireGuard,
|
|
215
|
-
* ZeroTier, ...): the dummy/tunnel interface itself carries no real
|
|
216
|
-
* traffic path, so something else has to get this uplink's own netns
|
|
217
|
-
* onto the real internet. `via` must already be declared (e.g.
|
|
218
|
-
* isp-primary), earlier in the same defineNetwork call. */
|
|
219
|
-
readonly backdoor?: { readonly via: Uplink };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function uplinkDummy(input: DummyUplinkInput): UplinkBuilder {
|
|
223
|
-
const id = uplinkId(
|
|
224
|
-
typeof input.id === "string" ? Number.parseInt(input.id, 10) : input.id,
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
return (allocSlot: () => number) => {
|
|
228
|
-
const slot = allocSlot();
|
|
229
|
-
const ifc: InterfaceKind = { kind: "dummy" };
|
|
230
|
-
|
|
231
|
-
const addresses: Addresses = [
|
|
232
|
-
transferNet(id, slot, 1),
|
|
233
|
-
transferNet(id, slot, 2),
|
|
234
|
-
];
|
|
235
|
-
|
|
236
|
-
// Backdoor gets its OWN slot — allocSlot() called a second time,
|
|
237
|
-
// from the same sequential pool as every uplink's front-door
|
|
238
|
-
// transfer link, so its /28 (IPv4) can never overlap with this
|
|
239
|
-
// uplink's own `addresses` above (see Backdoor's doc comment,
|
|
240
|
-
// types.ts, for why that overlap is a real bug and not just
|
|
241
|
-
// untidy). host=3/4 here, not 1/2: transferNet's IPv6 fold keys
|
|
242
|
-
// only on (id, host), NOT slot, so reusing host 1/2 under the SAME
|
|
243
|
-
// id would fold to the IDENTICAL IPv6 addresses as this uplink's
|
|
244
|
-
// own front-door transfer link above, even though slot keeps the
|
|
245
|
-
// IPv4 side distinct. 3/4 keeps both stacks distinct.
|
|
246
|
-
const backdoor: Backdoor | undefined = input.backdoor === undefined
|
|
247
|
-
? undefined
|
|
248
|
-
: (() => {
|
|
249
|
-
const bdSlot = allocSlot();
|
|
250
|
-
return {
|
|
251
|
-
via: input.backdoor!.via,
|
|
252
|
-
slot: bdSlot,
|
|
253
|
-
addresses: [
|
|
254
|
-
transferNet(id, bdSlot, 3),
|
|
255
|
-
transferNet(id, bdSlot, 4),
|
|
256
|
-
],
|
|
257
|
-
};
|
|
258
|
-
})();
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
slot,
|
|
262
|
-
addresses,
|
|
263
|
-
if: ifc,
|
|
264
|
-
nat: input.nat,
|
|
265
|
-
// Defaults to "static"/"static" (no discovery) unlike
|
|
266
|
-
// uplinkVlan/uplinkPhysical's "dhcp" default — a dummy interface
|
|
267
|
-
// has no real ISP behind it, so dhclient/dhcpcd would just hang
|
|
268
|
-
// forever waiting for a lease that never comes. Still overridable
|
|
269
|
-
// via `addresses`, in case a future test wants to exercise the
|
|
270
|
-
// discovery path against a dummy on purpose.
|
|
271
|
-
discovery: resolveDiscovery(
|
|
272
|
-
input.addresses ?? { ipv4: "static", ipv6: "static" },
|
|
273
|
-
input.client,
|
|
274
|
-
),
|
|
275
|
-
backdoor,
|
|
276
|
-
host: input.host,
|
|
277
|
-
};
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// ── wireguard() ──────────────────────────────────────────────────────
|
|
282
|
-
// A real WireGuard tunnel — see InterfaceKind's "wireguard" variant
|
|
283
|
-
// (types.ts) for why this shells out to wg-quick against an
|
|
284
|
-
// already-on-disk conf file rather than embedding key material here.
|
|
285
|
-
// Almost always paired with `backdoor` (the tunnel's own
|
|
286
|
-
// handshake/keepalive traffic needs a mundane path to the real
|
|
287
|
-
// internet, same as uplinkDummy() — see Backdoor, types.ts), but not
|
|
288
|
-
// required: an uplink whose netns can already reach the internet some
|
|
289
|
-
// other way (e.g. it inherited a route) wouldn't need one.
|
|
290
|
-
|
|
291
|
-
interface WireguardUplinkInput {
|
|
292
|
-
readonly id: string | number;
|
|
293
|
-
/** Override the real kernel interface name AND the .conf's basename
|
|
294
|
-
* on disk (e.g. "mull-fra" -> /etc/wireguard/mull-fra.conf). Defaults
|
|
295
|
-
* to this uplink's OWN name (the string given to net.uplink(name,
|
|
296
|
-
* ...)) — redundant to repeat that in most cases, since it's already
|
|
297
|
-
* unique per uplink. Only needed when that default doesn't fit: it
|
|
298
|
-
* exceeds IFNAMSIZ (15 usable characters), or a real interface under
|
|
299
|
-
* a different name already exists and renaming it would mean
|
|
300
|
-
* bouncing a live tunnel unnecessarily. */
|
|
301
|
-
readonly ifaceName?: string;
|
|
302
|
-
/** The full wg-quick conf, declared directly here — see
|
|
303
|
-
* WireguardInterfaceConfig and InterfaceKind's "wireguard" variant
|
|
304
|
-
* (types.ts) for why the PrivateKey lives in plain sight in this
|
|
305
|
-
* file rather than behind an env var, for this uplink specifically. */
|
|
306
|
-
readonly config: WireguardInterfaceConfig;
|
|
307
|
-
readonly nat?: Nat;
|
|
308
|
-
readonly host: Host;
|
|
309
|
-
/** Bootstrap egress for the tunnel's own setup/keepalive traffic —
|
|
310
|
-
* see Backdoor (types.ts) and emitBackdoorNat (generate-netns.ts),
|
|
311
|
-
* which scopes the resulting NAT rule to the backdoor's OWN transit
|
|
312
|
-
* address (not any segment subnet) whenever the owning uplink has a
|
|
313
|
-
* real interface of its own, as this one does. */
|
|
314
|
-
readonly backdoor?: { readonly via: Uplink };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export function uplinkWireguard(input: WireguardUplinkInput): UplinkBuilder {
|
|
318
|
-
const id = uplinkId(
|
|
319
|
-
typeof input.id === "string" ? Number.parseInt(input.id, 10) : input.id,
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
return (allocSlot: () => number, name: string) => {
|
|
323
|
-
const slot = allocSlot();
|
|
324
|
-
const ifc: InterfaceKind = {
|
|
325
|
-
kind: "wireguard",
|
|
326
|
-
ifaceName: input.ifaceName ?? name,
|
|
327
|
-
config: input.config,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const addresses: Addresses = [
|
|
331
|
-
transferNet(id, slot, 1),
|
|
332
|
-
transferNet(id, slot, 2),
|
|
333
|
-
];
|
|
334
|
-
|
|
335
|
-
// Same separate-slot reasoning as uplinkDummy()'s backdoor above —
|
|
336
|
-
// see Backdoor's doc comment (types.ts) for why sharing a /28 with
|
|
337
|
-
// this uplink's own front-door transfer link is broken, not just
|
|
338
|
-
// untidy.
|
|
339
|
-
const backdoor: Backdoor | undefined = input.backdoor === undefined
|
|
340
|
-
? undefined
|
|
341
|
-
: (() => {
|
|
342
|
-
const bdSlot = allocSlot();
|
|
343
|
-
return {
|
|
344
|
-
via: input.backdoor!.via,
|
|
345
|
-
slot: bdSlot,
|
|
346
|
-
addresses: [
|
|
347
|
-
transferNet(id, bdSlot, 3),
|
|
348
|
-
transferNet(id, bdSlot, 4),
|
|
349
|
-
],
|
|
350
|
-
};
|
|
351
|
-
})();
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
slot,
|
|
355
|
-
addresses,
|
|
356
|
-
if: ifc,
|
|
357
|
-
nat: input.nat,
|
|
358
|
-
// wg-quick manages the tunnel's own address (and, via its
|
|
359
|
-
// fwmark/policy-routing dance, its own default route) entirely —
|
|
360
|
-
// nothing for dhclient/dhcpcd/static to do on this interface.
|
|
361
|
-
discovery: { ipv4: "static", ipv6: "static" },
|
|
362
|
-
backdoor,
|
|
363
|
-
host: input.host,
|
|
364
|
-
};
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// ── segment factories ─────────────────────────────────────────────
|
|
369
|
-
// Mirror the uplink factories above, but produce a Segment-shaped
|
|
370
|
-
// result (adds `uplink`, uses segmentNet() instead of transferNet()
|
|
371
|
-
// since a segment has a client-facing gateway address, not a
|
|
372
|
-
// transfer-link pair).
|
|
373
|
-
|
|
374
|
-
/** input.uplink -> UplinkSelector | undefined. Omitting uplink entirely
|
|
375
|
-
* means "no egress yet" (see Segment.uplink, types.ts) — a deliberate,
|
|
376
|
-
* representable state, not an oversight. */
|
|
377
|
-
function resolveUplinkSelector(
|
|
378
|
-
uplink: Uplink | UplinkSelector | undefined,
|
|
379
|
-
): UplinkSelector | undefined {
|
|
380
|
-
if (uplink === undefined) return undefined;
|
|
381
|
-
return "resolve" in uplink ? uplink : new FixedUplink(uplink);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
interface SegmentPhysicalInput {
|
|
385
|
-
readonly id: string | number;
|
|
386
|
-
readonly name: string;
|
|
387
|
-
readonly uplink?: Uplink | UplinkSelector;
|
|
388
|
-
readonly nat?: Nat;
|
|
389
|
-
/** Advertise RA/SLAAC for this segment's IPv6 prefix. Defaults to true. */
|
|
390
|
-
readonly slaac?: boolean;
|
|
391
|
-
readonly host: Host;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export function segmentPhysical(
|
|
395
|
-
input: SegmentPhysicalInput,
|
|
396
|
-
): Omit<Segment, "name"> {
|
|
397
|
-
const id = segmentId(
|
|
398
|
-
typeof input.id === "string" ? Number.parseInt(input.id, 10) : input.id,
|
|
399
|
-
);
|
|
400
|
-
return {
|
|
401
|
-
addresses: [segmentNet(id, 2)],
|
|
402
|
-
if: { kind: "physical", name: input.name },
|
|
403
|
-
uplink: resolveUplinkSelector(input.uplink),
|
|
404
|
-
nat: input.nat,
|
|
405
|
-
slaac: input.slaac ?? true,
|
|
406
|
-
host: input.host,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
interface SegmentVlanInput {
|
|
411
|
-
readonly id: string | number;
|
|
412
|
-
readonly vlanParent: string;
|
|
413
|
-
/** Override the VLAN tag if it should differ from `id`. Defaults to id. */
|
|
414
|
-
readonly vlan?: number;
|
|
415
|
-
/** Override the kernel subinterface name — see InterfaceKind
|
|
416
|
-
* (types.ts). Only needed if `${vlanParent}.${vlanId}` is already in
|
|
417
|
-
* use for something else (e.g. VLAN 129 already carries this host's
|
|
418
|
-
* own management IP on `ens18.129` outside OVS). */
|
|
419
|
-
readonly ifaceName?: string;
|
|
420
|
-
readonly uplink?: Uplink | UplinkSelector;
|
|
421
|
-
readonly nat?: Nat;
|
|
422
|
-
/** Advertise RA/SLAAC for this segment's IPv6 prefix. Defaults to true. */
|
|
423
|
-
readonly slaac?: boolean;
|
|
424
|
-
readonly host: Host;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export function segmentVlan(input: SegmentVlanInput): Omit<Segment, "name"> {
|
|
428
|
-
const id = segmentId(
|
|
429
|
-
typeof input.id === "string" ? Number.parseInt(input.id, 10) : input.id,
|
|
430
|
-
);
|
|
431
|
-
return {
|
|
432
|
-
addresses: [segmentNet(id, 2)],
|
|
433
|
-
if: {
|
|
434
|
-
kind: "vlan",
|
|
435
|
-
vlanParent: input.vlanParent,
|
|
436
|
-
vlanId: input.vlan ?? id,
|
|
437
|
-
ifaceName: input.ifaceName,
|
|
438
|
-
},
|
|
439
|
-
uplink: resolveUplinkSelector(input.uplink),
|
|
440
|
-
nat: input.nat,
|
|
441
|
-
slaac: input.slaac ?? true,
|
|
442
|
-
host: input.host,
|
|
443
|
-
};
|
|
444
|
-
}
|