@adviser/ovn-fabric 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 meno
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # ovn-fabric
2
+
3
+ A declarative OVN/OVS network topology generator. You describe a network —
4
+ hosts, segments, uplinks, VPN tunnels — as plain TypeScript, and ovn-fabric
5
+ emits **one self-installing shell script per host**. Copy that script to the
6
+ host and run it: it creates the OVS bridges/interfaces, builds the full OVN
7
+ logical topology (routers, switches, NAT), and installs itself as a
8
+ boot-time systemd unit — idempotently, so re-running it (or rebooting) is
9
+ always safe.
10
+
11
+ No netplan, no hand-written `ovn-nbctl`/`ovs-vsctl` invocations, no
12
+ configuration drift between "what I meant to set up" and "what's actually
13
+ running." The config is the single source of truth; the generated script is
14
+ a disposable, regeneratable artifact.
15
+
16
+ ## Install
17
+
18
+ **Via npx** (no local Deno install required — the official
19
+ [`deno`](https://www.npmjs.com/package/deno) npm package is pulled in
20
+ automatically as a dependency the first time you run this):
21
+
22
+ ```sh
23
+ npx ovn-fabric generate-ovn path/to/topology.ts > install.sh
24
+ ```
25
+
26
+ **Via Deno**, if you already have it:
27
+
28
+ ```sh
29
+ deno run -A jsr:@adviser/ovn-fabric generate-ovn path/to/topology.ts > install.sh
30
+ ```
31
+
32
+ **As a permanent global command**:
33
+
34
+ ```sh
35
+ deno install -g -A -n ovn-fabric jsr:@adviser/ovn-fabric
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ Copy [`examples/minimal-topology.ts`](examples/minimal-topology.ts) as a
41
+ starting point:
42
+
43
+ ```ts
44
+ import { defineNetwork } from "jsr:@adviser/ovn-fabric/define";
45
+ import { segmentPhysical, uplinkPhysical } from "jsr:@adviser/ovn-fabric/factories";
46
+ import { ManualUplink } from "jsr:@adviser/ovn-fabric/types";
47
+
48
+ export const network = defineNetwork("minimal", (net) => {
49
+ const host = net.localHost("this-host");
50
+
51
+ const wan = net.uplink("wan", uplinkPhysical({
52
+ id: "1",
53
+ name: "eth0",
54
+ nat: { ipv4: [{ kind: "masq" }], ipv6: [{ kind: "masq" }] },
55
+ host,
56
+ }));
57
+
58
+ net.segment("lan", segmentPhysical({
59
+ id: "10",
60
+ name: "eth1",
61
+ uplink: new ManualUplink(wan),
62
+ slaac: false,
63
+ host,
64
+ }));
65
+ });
66
+ ```
67
+
68
+ Then:
69
+
70
+ ```sh
71
+ npx ovn-fabric generate topology.ts # sanity-check what it declares
72
+ npx ovn-fabric generate-ovn topology.ts # emit the install script(s)
73
+ ```
74
+
75
+ `generate-ovn` prints one script per distinct `Host` your config declares.
76
+ For a multi-host config, scripts are separated by a `# ===== host: X =====`
77
+ marker line — never printed before the first script, since each script is
78
+ meant to be saved and run (or `systemctl`-exec'd) as-is, and a leading
79
+ non-shebang line would break that.
80
+
81
+ ## Model
82
+
83
+ - **Host** — where a segment's or uplink's config actually gets applied
84
+ (`net.sshHost(name, address, user)` or `net.localHost(name)`). Every
85
+ segment/uplink declares its own host explicitly, so a multi-chassis
86
+ topology is a config change, not a redesign.
87
+ - **Uplink** — a real (or eventually-real) path to the outside world:
88
+ `uplinkVlan`, `uplinkPhysical`, `uplinkDummy` (placeholder, no backing
89
+ interface yet), `uplinkWireguard` (a real `wg-quick`-managed tunnel).
90
+ - **Segment** — a client-facing network (`segmentPhysical`, `segmentVlan`),
91
+ joined to the shared backbone and routed out through whichever uplink it
92
+ points at.
93
+ - **Backdoor** — a second, dedicated transfer-link-shaped connection an
94
+ uplink can borrow from an *already-real* uplink's own egress. Used for
95
+ two distinct purposes: as a stand-in real interface while an uplink is
96
+ still a placeholder (`uplinkDummy`), or as pure bootstrap egress
97
+ alongside a real interface (e.g. a WireGuard tunnel's own handshake/
98
+ keepalive traffic needs a mundane path to the internet before the tunnel
99
+ itself is up).
100
+ - **NAT** — per-uplink and per-segment; currently `{ kind: "masq" }` for
101
+ IPv4/IPv6 independently.
102
+
103
+ See the doc comments in `src/types.ts` for the full model — every field has
104
+ an explanation of what it's for and why it's shaped the way it is.
105
+
106
+ ## Design notes worth knowing before you rely on this
107
+
108
+ - Boot safety: package checks (`dpkg -s`) on the systemd/boot path are
109
+ `timeout`-capped and warn-only — a missing tool never blocks router
110
+ startup. Real `apt-get install` only ever runs in the manual/first-run
111
+ branch, never from `systemctl`.
112
+ - Idempotency throughout: `--may-exist`/`--if-exists` on every
113
+ `ovs-vsctl`/`ovn-nbctl` call, `ip link show ... || ...` guards, `cmp -s`
114
+ before overwriting any file (including WireGuard confs — the tunnel only
115
+ bounces if the content actually changed).
116
+ - WireGuard uplinks use `wg-quick`, not hand-rolled `wg setconf` — this
117
+ relies on `wg-quick`'s own fwmark + policy-routing so the tunnel's own
118
+ traffic keeps using whatever path already exists (see Backdoor above)
119
+ while everything else gets diverted into the tunnel.
120
+
121
+ ## Publishing (maintainers)
122
+
123
+ Tag a commit `vX.Y.Z` (matching the version in both `package.json` and
124
+ `deno.json`) and push it — CI publishes to npm and JSR automatically via
125
+ OIDC trusted publishing (see `.github/workflows/ci.yaml`; no token secrets
126
+ involved, but it does require a one-time registry-side setup — see the
127
+ comment at the top of that file).
128
+
129
+ ## License
130
+
131
+ Apache-2.0 — see [LICENSE](LICENSE).
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ // bin/ovn-fabric.js — the npx/npm entry point.
3
+ //
4
+ // ovn-fabric itself is a Deno/TypeScript CLI (see src/cli.ts) — this
5
+ // shim is the only piece of the npm distribution that runs under Node.
6
+ // It does nothing but locate the `deno` executable (installed
7
+ // automatically as this package's own npm dependency — see
8
+ // https://www.npmjs.com/package/deno, the official, Deno-team-
9
+ // maintained npm distribution of the Deno runtime itself) and hand off
10
+ // to it with the bundled src/cli.ts as the entrypoint. `deno` handles
11
+ // picking the right prebuilt binary for the current OS/arch on its own
12
+ // (via its optionalDependencies + postinstall) — nothing here needs to
13
+ // know or care what platform it's running on.
14
+ "use strict";
15
+
16
+ const path = require("path");
17
+ const { spawnSync } = require("child_process");
18
+
19
+ // deno/bin.cjs is itself a small Node launcher (see the `deno` npm
20
+ // package) that locates/spawns the real deno binary and forwards
21
+ // argv — resolved via require.resolve so this works regardless of how
22
+ // npm/pnpm/yarn hoisted node_modules.
23
+ const denoLauncher = require.resolve("deno/bin.cjs");
24
+ const cliEntry = path.join(__dirname, "..", "src", "cli.ts");
25
+
26
+ const result = spawnSync(
27
+ process.execPath,
28
+ [
29
+ denoLauncher,
30
+ "run",
31
+ "--allow-read",
32
+ "--allow-env",
33
+ cliEntry,
34
+ ...process.argv.slice(2),
35
+ ],
36
+ { stdio: "inherit" },
37
+ );
38
+
39
+ if (result.error) {
40
+ console.error(result.error.message);
41
+ process.exit(1);
42
+ }
43
+ process.exit(result.status ?? 1);
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@adviser/ovn-fabric",
3
+ "version": "0.1.0",
4
+ "description": "Declarative OVN/OVS topology generator — one config, one self-installing shell script per host.",
5
+ "license": "Apache-2.0",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "ovn-fabric": "bin/ovn-fabric.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mabels/ovn-fabric.git"
21
+ },
22
+ "homepage": "https://github.com/mabels/ovn-fabric#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/mabels/ovn-fabric/issues"
25
+ },
26
+ "keywords": [
27
+ "ovn",
28
+ "ovs",
29
+ "sdn",
30
+ "networking",
31
+ "topology",
32
+ "deno",
33
+ "cli"
34
+ ],
35
+ "dependencies": {
36
+ "deno": "^2.9.1"
37
+ }
38
+ }
@@ -0,0 +1,222 @@
1
+ // src/addressing.ts — the three fold rules, exactly once each, exposed
2
+ // as NetId factories.
3
+ //
4
+ // Fold operation = string construction. Decimal identifiers are placed
5
+ // directly into the address text, so the result stays human-readable
6
+ // and visually corresponds to its IPv4 counterpart (fd00:10:80::128:1
7
+ // reads as "segment 128", the same way 10.80.128.1 does). IPAddress.parse()
8
+ // is called ONLY AFTER the string is fully built — it validates the
9
+ // result and returns a real IPAddress for everything downstream
10
+ // (containment checks, to_string(), comparisons). There is no integer
11
+ // arithmetic on Crunchy here: that produces hex-folded results (segment
12
+ // 128 -> "80") which breaks the human-readability property these rules
13
+ // exist for.
14
+
15
+ import { IPAddress } from "npm:ipaddress@0.2.6";
16
+ import {
17
+ type NetId,
18
+ segmentId,
19
+ type SegmentId,
20
+ uplinkId,
21
+ type UplinkId,
22
+ } from "./types.ts";
23
+
24
+ function makeNetId(
25
+ ipv4Str: string,
26
+ ipv6Str: string,
27
+ rawId: number,
28
+ vlan: number | undefined,
29
+ ): NetId {
30
+ const ipv4 = IPAddress.parse(ipv4Str);
31
+ const ipv6 = IPAddress.parse(ipv6Str);
32
+ return {
33
+ ipv4,
34
+ ipv6,
35
+ id: () => rawId,
36
+ vlan: () => vlan,
37
+ };
38
+ }
39
+
40
+ // ── backbone: one shared /16, every identifier gets its own /28 block ──
41
+ //
42
+ // IPv4: 10.80.0.0/16, 4096 /28 blocks (same partition scheme as
43
+ // transferNet's 10.99.0.0/16 — see that function's comment for the
44
+ // general approach). block_base = identifier << 4. Within a block,
45
+ // SEGMENTS use the low half (host offsets 1-7) and UPLINKS use the
46
+ // high half (block_base | 0x8, host offsets 1-7 within that half) —
47
+ // so a segment and an uplink that happen to share the same numeric
48
+ // slot can never collide, both halves fit in the same /28 without
49
+ // overlapping.
50
+ //
51
+ // IPv6 has no equivalent scarcity (128 bits is enormous) — it keeps
52
+ // directly folding the real identifier into the network portion, with
53
+ // a literal 0/8 marker group distinguishing segment vs uplink, so the
54
+ // human-readability property (you can read an id straight out of the
55
+ // address) is preserved on the v6 side even though v4 has to use the
56
+ // less-readable shifted-block scheme to fit in 32 bits.
57
+ //
58
+ // No physical VLAN on either — the backbone is OVN-internal transit,
59
+ // not wired to any single segment's or uplink's physical VLAN. vlan()
60
+ // is always undefined.
61
+
62
+ function backboneBlockAddress(blockBase: number, host: number): string {
63
+ if (host < 1 || host > 7) {
64
+ throw new RangeError(`backbone host out of range (1-7): ${host}`);
65
+ }
66
+ const third = (blockBase >> 8) & 0xff;
67
+ const fourth = (blockBase & 0xff) + host;
68
+ return `10.80.${third}.${fourth}`;
69
+ }
70
+
71
+ export function segmentBackboneNet(
72
+ segment: SegmentId | number,
73
+ host: number,
74
+ ): NetId {
75
+ const id = typeof segment === "number" ? segmentId(segment) : segment;
76
+ const blockBase = id << 4;
77
+ return makeNetId(
78
+ `${backboneBlockAddress(blockBase, host)}/16`,
79
+ `fd00:10:80::0:${id}:${host}/64`,
80
+ id,
81
+ undefined,
82
+ );
83
+ }
84
+
85
+ export function uplinkBackboneNet(
86
+ uplink: UplinkId | number,
87
+ slot: number,
88
+ host: number,
89
+ ): NetId {
90
+ // `slot` (not the real uplink id) drives the IPv4 block-shift, same
91
+ // as transferNet — uplink ids like 1280 are far too large to use
92
+ // directly in <<4 arithmetic (1280<<4 = 20480, producing a
93
+ // meaningless-looking but technically-valid address in the wrong
94
+ // place: 10.80.80.x). `slot` MUST be the same small sequential index
95
+ // (0,1,2,...) NetworkBuilder already assigns for the transfer link
96
+ // (see define.ts), so an uplink's backbone leg and transfer leg use
97
+ // consistent, small, collision-free numbering. The real uplink id is
98
+ // still used for the IPv6 fold, which has no such size constraint.
99
+ const id = typeof uplink === "number" ? uplinkId(uplink) : uplink;
100
+ if (slot < 0 || slot > 4095) {
101
+ throw new RangeError(`uplinkBackboneNet: slot out of range (0-4095): ${slot}`);
102
+ }
103
+ const blockBase = (slot << 4) | 0x8;
104
+ return makeNetId(
105
+ `${backboneBlockAddress(blockBase, host)}/16`,
106
+ `fd00:10:80::8:${id}:${host}/64`,
107
+ id,
108
+ undefined,
109
+ );
110
+ }
111
+
112
+ // ── client segments: one network PER SEGMENT, identifier folds into the
113
+ // NETWORK portion ──
114
+ // fd00:192:168:<segment>::<host>/64 <-> 192.168.<segment>.<host>/24
115
+ //
116
+ // vlan() returns the segment id itself when the segment's physical VLAN
117
+ // tag matches its numeric id (true for every segment built so far —
118
+ // segment 128 is VLAN 128, etc.). Pass an explicit vlan to override
119
+ // (e.g. segment 128 is untagged on the wire, so its real VLAN concept
120
+ // doesn't apply the same way — see untaggedOnWire on Segment).
121
+
122
+ export function segmentNet(
123
+ segment: SegmentId | number,
124
+ host: number,
125
+ vlan?: number,
126
+ ): NetId {
127
+ const id = typeof segment === "number" ? segmentId(segment) : segment;
128
+ return makeNetId(
129
+ `192.168.${id}.${host}/24`,
130
+ `fd00:192:168:${id}::${host}/64`,
131
+ id,
132
+ vlan ?? id,
133
+ );
134
+ }
135
+
136
+ // ── transfer links: one network PER UPLINK, identifier folds into the
137
+ // NETWORK portion ──
138
+ // fd00:10:99:<uplink>::<host>/124 <-> 10.99.<third>.<fourth>/28
139
+ //
140
+ // IPv4: 10.99.0.0/16 holds exactly 4096 /28 blocks (16 addresses each).
141
+ // `slot` (0-4095) selects which block — slot N is the Nth /28 in
142
+ // sequence: slot 0 = 10.99.0.0/28, slot 1 = 10.99.0.16/28, ...,
143
+ // slot 128 = 10.99.8.0/28, etc. `slot` is NOT the uplink's own id
144
+ // (uplink ids like 1280/1281/1282 exceed 4095 and aren't sequential
145
+ // from 0) — it's a small sequential index the caller assigns, one per
146
+ // uplink, distinct from the uplink's real identity. See define.ts,
147
+ // where NetworkBuilder assigns slots automatically in declaration
148
+ // order so config/topology.ts never has to think about this at all.
149
+ //
150
+ // IPv6 still folds the uplink's real id (not slot) into the network
151
+ // portion, since IPv6 has no equivalent address-space scarcity forcing
152
+ // a slot scheme — fd00:10:99:1280::/124 is perfectly fine on its own.
153
+ //
154
+ // No physical VLAN on the OVN side of a transfer link — vlan() is
155
+ // undefined here; the REAL uplink (see uplinkNet below) is what carries
156
+ // the physical VLAN tag.
157
+
158
+ export function transferNet(
159
+ uplink: UplinkId | number,
160
+ slot: number,
161
+ host: number,
162
+ ): NetId {
163
+ const id = typeof uplink === "number" ? uplinkId(uplink) : uplink;
164
+ if (slot < 0 || slot > 4095) {
165
+ throw new RangeError(`transfer slot out of range (0-4095): ${slot}`);
166
+ }
167
+ if (host < 1 || host > 14) {
168
+ throw new RangeError(`transfer host out of range (1-14): ${host}`);
169
+ }
170
+ const blockBase = slot * 16; // 0-65520, fits in 16 bits
171
+ const third = (blockBase >> 8) & 0xff;
172
+ const fourth = (blockBase & 0xff) + host;
173
+ return makeNetId(
174
+ `10.99.${third}.${fourth}/28`,
175
+ `fd00:10:99:${id}::${host}/124`,
176
+ id,
177
+ undefined,
178
+ );
179
+ }
180
+
181
+ // ── uplink's real-world physical identity ──────────────────────────
182
+ // The uplink's own VLAN (e.g. 1280 for an uplink named isp-primary) — this is the ONE
183
+ // place vlan() reflects a real physical tag tied 1:1 to the uplink
184
+ // numeric id, since every uplink built so far uses vlanId === id.
185
+ // No IPv4/IPv6 addressing of its own (the uplink's real address is
186
+ // whatever the ISP hands out dynamically) — ipv4/ipv6 here are the
187
+ // transfer link's OVN-side address, reused, so every NetId still
188
+ // satisfies the same shape.
189
+
190
+ export function uplinkNet(
191
+ uplink: UplinkId | number,
192
+ slot: number,
193
+ ): NetId {
194
+ const id = typeof uplink === "number" ? uplinkId(uplink) : uplink;
195
+ const transfer = transferNet(id, slot, 1);
196
+ return makeNetId(
197
+ transfer.ipv4.to_string(),
198
+ transfer.ipv6.to_string(),
199
+ id,
200
+ id,
201
+ );
202
+ }
203
+
204
+ // ── MAC address derivation ────────────────────────────────────────
205
+ // Folds an IPv4 address's four octets directly into a MAC, prefixed
206
+ // with 00:00 — the convention already used by hand throughout this
207
+ // project (e.g. 192.168.128.2 -> 00:00:c0:a8:80:02, verified against a
208
+ // live deployment). Locally-administered OUI space
209
+ // (00:00:xx is not a real vendor block) is fine for this use — these
210
+ // MACs only need to be unique within OVN's logical topology, never
211
+ // routed on a real physical LAN segment.
212
+
213
+ export function macFromV4(ipv4: IPAddress): string {
214
+ const octets = ipv4.to_s().split("/")[0].split(".").map((s) =>
215
+ Number.parseInt(s, 10)
216
+ );
217
+ if (octets.length !== 4 || octets.some((o) => Number.isNaN(o))) {
218
+ throw new Error(`macFromV4: not a valid IPv4 address: ${ipv4.to_s()}`);
219
+ }
220
+ const hex = octets.map((o) => o.toString(16).padStart(2, "0"));
221
+ return `00:00:${hex.join(":")}`;
222
+ }