buninu-linux 0.2.1 → 0.3.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.
@@ -0,0 +1,629 @@
1
+ #!/bin/bun
2
+ // ip — show and configure links, addresses, routes and neighbours, with the
3
+ // iproute2 command grammar. `ip --help` renders /usr/share/doc/buninu-linux/ip.md.
4
+ //
5
+ // A Bun script that reads /sys/class/net and /proc/net, and configures the
6
+ // kernel through the classic SIOC* ioctls on an AF_INET socket bound through
7
+ // /lib/dlopen.js — the same calls init.js uses to bring up lo and eth0. That
8
+ // keeps it small at the cost of one IPv4 address per interface; IPv6 is
9
+ // shown but not configured.
10
+
11
+ import { readdirSync, readFileSync } from "node:fs";
12
+ import { cString, ptr, withInetSocket, Errno, SysError, showDocument } from "../lib/dlopen.js";
13
+
14
+ const fail = (message, status = 1) => {
15
+ console.error(`ip: ${message}`);
16
+ process.exit(status);
17
+ };
18
+
19
+ // -------------------------------------------------------------- constants --
20
+
21
+ const SIOC = {
22
+ ADDRT: 0x890b, DELRT: 0x890c,
23
+ GIFFLAGS: 0x8913, SIFFLAGS: 0x8914, GIFADDR: 0x8915, SIFADDR: 0x8916,
24
+ GIFBRDADDR: 0x8919, SIFBRDADDR: 0x891a, GIFNETMASK: 0x891b, SIFNETMASK: 0x891c,
25
+ SIFMTU: 0x8922, SIFNAME: 0x8923, SIFHWADDR: 0x8924,
26
+ };
27
+
28
+ // <linux/if.h>. The sysfs `flags` file is the raw dev->flags, so RUNNING and
29
+ // LOWER_UP are derived from the `carrier` file instead (see interfaces()).
30
+ const IFF = {
31
+ UP: 0x1, BROADCAST: 0x2, DEBUG: 0x4, LOOPBACK: 0x8, POINTOPOINT: 0x10, NOTRAILERS: 0x20,
32
+ RUNNING: 0x40, NOARP: 0x80, PROMISC: 0x100, ALLMULTI: 0x200, MASTER: 0x400, SLAVE: 0x800,
33
+ MULTICAST: 0x1000, PORTSEL: 0x2000, AUTOMEDIA: 0x4000, DYNAMIC: 0x8000,
34
+ LOWER_UP: 0x10000, DORMANT: 0x20000, ECHO: 0x40000,
35
+ };
36
+
37
+ const RTF = { UP: 0x1, GATEWAY: 0x2, HOST: 0x4 };
38
+
39
+ const linkTypes = {
40
+ 0: "generic", 1: "ether", 6: "ieee802", 24: "ieee1394", 32: "infiniband", 512: "ppp",
41
+ 768: "ipip", 769: "ip6tnl", 772: "loopback", 776: "sit", 778: "gre", 783: "irda",
42
+ 801: "ieee802.11", 823: "ip6gre", 65534: "none", 65535: "void",
43
+ };
44
+
45
+ // ------------------------------------------------------------ addresses ---
46
+
47
+ const parseIpv4 = (text) => {
48
+ const parts = text.split(".");
49
+ if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part) || Number(part) > 255)) {
50
+ fail(`Error: inet address is expected rather than "${text}".`);
51
+ }
52
+ return parts.map(Number);
53
+ };
54
+
55
+ const formatIpv4 = (bytes) => bytes.join(".");
56
+
57
+ const prefixToMask = (prefix) => {
58
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
59
+ return [mask >>> 24, (mask >>> 16) & 255, (mask >>> 8) & 255, mask & 255];
60
+ };
61
+
62
+ const maskToPrefix = (bytes) => {
63
+ let value = ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0;
64
+ let prefix = 0;
65
+ while (value & 0x80000000) { prefix++; value = (value << 1) >>> 0; }
66
+ return prefix;
67
+ };
68
+
69
+ // "10.0.2.15/24" -> { address, prefix }; a bare address is a /32 host.
70
+ const parseCidr = (text, { defaultPrefix = 32 } = {}) => {
71
+ const [address, prefix = String(defaultPrefix)] = text.split("/");
72
+ if (!/^\d{1,2}$/.test(prefix) || Number(prefix) > 32) fail(`Error: invalid prefix length "${prefix}".`);
73
+ return { address: parseIpv4(address), prefix: Number(prefix) };
74
+ };
75
+
76
+ const scopeOfIpv4 = (bytes) => bytes[0] === 127 ? "host" : bytes[0] === 169 && bytes[1] === 254 ? "link" : "global";
77
+
78
+ // 32 hex digits from /proc/net/if_inet6 -> RFC 5952 compressed text.
79
+ const formatIpv6 = (hex) => {
80
+ const groups = [];
81
+ for (let i = 0; i < 32; i += 4) groups.push(parseInt(hex.slice(i, i + 4), 16));
82
+ let bestStart = -1, bestLength = 0;
83
+ for (let i = 0; i < 8; i++) {
84
+ if (groups[i] !== 0) continue;
85
+ let j = i;
86
+ while (j < 8 && groups[j] === 0) j++;
87
+ if (j - i > bestLength) { bestStart = i; bestLength = j - i; }
88
+ i = j;
89
+ }
90
+ const text = groups.map((group) => group.toString(16));
91
+ if (bestLength < 2) return text.join(":");
92
+ const head = text.slice(0, bestStart).join(":");
93
+ const tail = text.slice(bestStart + bestLength).join(":");
94
+ return `${head}::${tail}`;
95
+ };
96
+
97
+ // Little-endian hex words from /proc/net/route ("0F02000A") -> dotted quad.
98
+ const hexToIpv4 = (hex) => [6, 4, 2, 0].map((offset) => parseInt(hex.slice(offset, offset + 2), 16));
99
+
100
+ // ------------------------------------------------------------ ioctl glue ---
101
+
102
+ const IFNAMSIZ = 16;
103
+
104
+ const ifreq = (name) => {
105
+ if (name.length >= IFNAMSIZ) fail(`Error: "${name}" is longer than ${IFNAMSIZ - 1} bytes.`);
106
+ const request = new Uint8Array(40);
107
+ request.set(new TextEncoder().encode(name), 0);
108
+ return request;
109
+ };
110
+
111
+ const view = (buffer) => new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
112
+
113
+ const setSockaddrIn = (buffer, offset, address) => {
114
+ view(buffer).setUint16(offset, 2, true); // AF_INET
115
+ buffer.set(address, offset + 4);
116
+ };
117
+
118
+ const getSockaddrIn = (buffer, offset) => [...buffer.subarray(offset + 4, offset + 8)];
119
+
120
+ const translate = (error, name) => {
121
+ if (!(error instanceof SysError)) throw error;
122
+ if (error.errno === Errno.ENODEV) fail(`Cannot find device "${name}"`);
123
+ if (error.errno === Errno.EPERM || error.errno === Errno.EACCES) fail(`${error.operation}: ${error.message.split(": ").pop()} (need to be root)`, 2);
124
+ fail(error.message, 2);
125
+ };
126
+
127
+ // --------------------------------------------------------------- reading ---
128
+
129
+ const sysfs = (name, file) => {
130
+ try {
131
+ return readFileSync(`/sys/class/net/${name}/${file}`, "utf8").trim();
132
+ } catch {
133
+ return null;
134
+ }
135
+ };
136
+
137
+ const interfaces = () => {
138
+ let names;
139
+ try {
140
+ names = readdirSync("/sys/class/net");
141
+ } catch {
142
+ fail("Cannot open /sys/class/net (is sysfs mounted?)");
143
+ }
144
+ return names
145
+ .map((name) => ({
146
+ name,
147
+ index: Number(sysfs(name, "ifindex") ?? 0),
148
+ type: Number(sysfs(name, "type") ?? 0),
149
+ mtu: Number(sysfs(name, "mtu") ?? 0),
150
+ // `carrier` reads 1 with link, 0 without, and EINVAL while the
151
+ // interface is down; iproute2 shows LOWER_UP / NO-CARRIER from that.
152
+ flags: parseInt(sysfs(name, "flags") ?? "0", 16)
153
+ | (sysfs(name, "carrier") === "1" ? IFF.RUNNING | IFF.LOWER_UP : 0),
154
+ operstate: sysfs(name, "operstate") ?? "unknown",
155
+ address: sysfs(name, "address") ?? "",
156
+ broadcast: sysfs(name, "broadcast") ?? "",
157
+ qlen: Number(sysfs(name, "tx_queue_len") ?? 0),
158
+ }))
159
+ .sort((a, b) => a.index - b.index);
160
+ };
161
+
162
+ const interfaceNamed = (name) => {
163
+ const found = interfaces().find((link) => link.name === name);
164
+ if (!found) fail(`Device "${name}" does not exist.`);
165
+ return found;
166
+ };
167
+
168
+ // The primary IPv4 address and its mask/broadcast, or null.
169
+ const ipv4Of = (name) => withInetSocket((ioctl) => {
170
+ const request = ifreq(name);
171
+ const missing = [Errno.EADDRNOTAVAIL, Errno.ENODEV];
172
+ if (ioctl(SIOC.GIFADDR, request, "SIOCGIFADDR", missing) < 0) return null;
173
+ const address = getSockaddrIn(request, 16);
174
+ ioctl(SIOC.GIFNETMASK, request, "SIOCGIFNETMASK", missing);
175
+ const prefix = maskToPrefix(getSockaddrIn(request, 16));
176
+ let broadcast = null;
177
+ if (ioctl(SIOC.GIFBRDADDR, request, "SIOCGIFBRDADDR", missing) >= 0) {
178
+ const candidate = getSockaddrIn(request, 16);
179
+ if (candidate.some(Boolean)) broadcast = candidate;
180
+ }
181
+ return { address, prefix, broadcast };
182
+ });
183
+
184
+ const ipv6Of = (name) => {
185
+ const list = [];
186
+ let table;
187
+ try {
188
+ table = readFileSync("/proc/net/if_inet6", "utf8");
189
+ } catch {
190
+ return list;
191
+ }
192
+ const scopes = { 0x00: "global", 0x10: "host", 0x20: "link", 0x40: "site" };
193
+ for (const line of table.split("\n")) {
194
+ const [hex, , prefix, scope, flags, device] = line.trim().split(/\s+/);
195
+ if (device !== name) continue;
196
+ list.push({
197
+ address: formatIpv6(hex),
198
+ prefix: parseInt(prefix, 16),
199
+ scope: scopes[parseInt(scope, 16)] ?? `scope ${scope}`,
200
+ flags: parseInt(flags, 16),
201
+ });
202
+ }
203
+ return list;
204
+ };
205
+
206
+ const routes = () => {
207
+ let table;
208
+ try {
209
+ table = readFileSync("/proc/net/route", "utf8");
210
+ } catch {
211
+ fail("Cannot open /proc/net/route");
212
+ }
213
+ const list = [];
214
+ for (const line of table.split("\n").slice(1)) {
215
+ if (!line.trim()) continue;
216
+ const [device, destination, gateway, flags, , , metric, mask] = line.trim().split(/\s+/);
217
+ list.push({
218
+ device,
219
+ destination: hexToIpv4(destination),
220
+ gateway: hexToIpv4(gateway),
221
+ flags: parseInt(flags, 16),
222
+ metric: Number(metric),
223
+ prefix: maskToPrefix(hexToIpv4(mask)),
224
+ });
225
+ }
226
+ return list;
227
+ };
228
+
229
+ const neighbours = () => {
230
+ let table;
231
+ try {
232
+ table = readFileSync("/proc/net/arp", "utf8");
233
+ } catch {
234
+ fail("Cannot open /proc/net/arp");
235
+ }
236
+ return table.split("\n").slice(1).filter((line) => line.trim()).map((line) => {
237
+ const [address, , flags, lladdr, , device] = line.trim().split(/\s+/);
238
+ return { address, flags: parseInt(flags, 16), lladdr, device };
239
+ });
240
+ };
241
+
242
+ // -------------------------------------------------------------- printing ---
243
+
244
+ const flagNames = (flags) => {
245
+ const names = [];
246
+ if (flags & IFF.UP && !(flags & IFF.RUNNING)) names.push("NO-CARRIER");
247
+ for (const name of ["LOOPBACK", "BROADCAST", "POINTOPOINT", "MULTICAST", "NOARP", "ALLMULTI", "PROMISC",
248
+ "MASTER", "SLAVE", "DEBUG", "DYNAMIC", "AUTOMEDIA", "PORTSEL", "NOTRAILERS", "UP", "LOWER_UP", "DORMANT", "ECHO"]) {
249
+ if (flags & IFF[name]) names.push(name);
250
+ }
251
+ return `<${names.join(",")}>`;
252
+ };
253
+
254
+ const printLink = (link) => {
255
+ const state = link.operstate.toUpperCase();
256
+ const qlen = link.qlen ? ` qlen ${link.qlen}` : "";
257
+ console.log(`${link.index}: ${link.name}: ${flagNames(link.flags)} mtu ${link.mtu} state ${state}${qlen}`);
258
+ const type = linkTypes[link.type] ?? `[${link.type}]`;
259
+ const broadcast = link.flags & IFF.BROADCAST || link.type === 772 ? ` brd ${link.broadcast}` : "";
260
+ console.log(` link/${type} ${link.address}${broadcast}`);
261
+ };
262
+
263
+ const printAddresses = (link, family) => {
264
+ if (family !== 6) {
265
+ const inet = ipv4Of(link.name);
266
+ if (inet) {
267
+ const broadcast = inet.broadcast ? ` brd ${formatIpv4(inet.broadcast)}` : "";
268
+ console.log(` inet ${formatIpv4(inet.address)}/${inet.prefix}${broadcast} scope ${scopeOfIpv4(inet.address)} ${link.name}`);
269
+ }
270
+ }
271
+ if (family !== 4) {
272
+ for (const inet6 of ipv6Of(link.name)) {
273
+ const extra = [inet6.flags & 0x40 ? "tentative" : "", inet6.flags & 0x20 ? "deprecated" : ""].filter(Boolean).join(" ");
274
+ console.log(` inet6 ${inet6.address}/${inet6.prefix} scope ${inet6.scope}${extra ? ` ${extra}` : ""}`);
275
+ }
276
+ }
277
+ };
278
+
279
+ const briefState = (link) => {
280
+ const state = link.operstate.toUpperCase();
281
+ return state === "UNKNOWN" && !(link.flags & IFF.UP) ? "DOWN" : state;
282
+ };
283
+
284
+ const printBrief = (link, { addresses, family }) => {
285
+ const column = (text, width) => text.padEnd(width);
286
+ let tail;
287
+ if (addresses) {
288
+ const list = [];
289
+ if (family !== 6) {
290
+ const inet = ipv4Of(link.name);
291
+ if (inet) list.push(`${formatIpv4(inet.address)}/${inet.prefix}`);
292
+ }
293
+ if (family !== 4) for (const inet6 of ipv6Of(link.name)) list.push(`${inet6.address}/${inet6.prefix}`);
294
+ tail = list.join(" ");
295
+ } else {
296
+ tail = `${link.address} ${flagNames(link.flags)}`;
297
+ }
298
+ console.log(`${column(link.name, 16)} ${column(briefState(link), 14)} ${tail}`.trimEnd());
299
+ };
300
+
301
+ const printRoutes = () => {
302
+ const primaries = new Map();
303
+ const sourceOf = (device) => {
304
+ if (!primaries.has(device)) primaries.set(device, ipv4Of(device));
305
+ return primaries.get(device);
306
+ };
307
+ for (const route of routes()) {
308
+ if (!(route.flags & RTF.UP)) continue;
309
+ const words = [];
310
+ const destination = route.prefix === 0 && route.destination.every((byte) => byte === 0)
311
+ ? "default"
312
+ : `${formatIpv4(route.destination)}${route.prefix === 32 ? "" : `/${route.prefix}`}`;
313
+ words.push(destination);
314
+ if (route.flags & RTF.GATEWAY) words.push("via", formatIpv4(route.gateway));
315
+ words.push("dev", route.device);
316
+ if (!(route.flags & RTF.GATEWAY)) {
317
+ const inet = sourceOf(route.device);
318
+ words.push("proto kernel scope link");
319
+ if (inet) words.push("src", formatIpv4(inet.address));
320
+ }
321
+ if (route.metric) words.push("metric", String(route.metric));
322
+ console.log(words.join(" "));
323
+ }
324
+ };
325
+
326
+ const printNeighbours = () => {
327
+ for (const entry of neighbours()) {
328
+ const state = entry.flags & 0x2 ? `lladdr ${entry.lladdr} REACHABLE` : "FAILED";
329
+ console.log(`${entry.address} dev ${entry.device} ${state}`);
330
+ }
331
+ };
332
+
333
+ // ------------------------------------------------------------- commands ----
334
+
335
+ // iproute2 accepts any unambiguous prefix: `ip a`, `ip li`, `ip r`, `ip n`.
336
+ // The first match in table order wins, which is how `a` means address.
337
+ const matchWord = (word, table, what) => {
338
+ const found = table.find((candidate) => candidate.startsWith(word));
339
+ if (!found) fail(`Object "${word}" is unknown, try "ip help".`.replace("Object", what));
340
+ return found;
341
+ };
342
+
343
+ const takeDevice = (words, index) => {
344
+ const word = words[index] === "dev" ? words[index + 1] : words[index];
345
+ if (!word) fail('Not enough information: "dev" argument is required.');
346
+ return [word, words[index] === "dev" ? index + 2 : index + 1];
347
+ };
348
+
349
+ const commandLink = (words, options) => {
350
+ const verb = words.length ? matchWord(words[0], ["show", "list", "lst", "ls", "set", "help", "add", "delete", "change"], "Command") : "show";
351
+ if (verb === "help") return showDocument("ip");
352
+ if (verb === "add" || verb === "delete" || verb === "change") fail(`"ip link ${verb}" needs rtnetlink, which this ip does not speak yet.`);
353
+
354
+ if (verb !== "set") {
355
+ let list = interfaces();
356
+ if (words[1]) {
357
+ const [name] = takeDevice(words, 1);
358
+ list = [interfaceNamed(name)];
359
+ }
360
+ for (const link of list) options.brief ? printBrief(link, { addresses: false, family: options.family }) : printLink(link);
361
+ return;
362
+ }
363
+
364
+ let [name, index] = takeDevice(words, 1);
365
+ interfaceNamed(name);
366
+ withInetSocket((ioctl) => {
367
+ const request = ifreq(name);
368
+ const flagsOf = () => { ioctl(SIOC.GIFFLAGS, request, "SIOCGIFFLAGS"); return view(request).getUint16(16, true); };
369
+ const setFlags = (flags) => { view(request).setUint16(16, flags & 0xffff, true); ioctl(SIOC.SIFFLAGS, request, "SIOCSIFFLAGS"); };
370
+ const toggle = (bit, value) => setFlags(value === "on" ? flagsOf() | bit : flagsOf() & ~bit);
371
+ const onOff = (key, value) => {
372
+ if (value !== "on" && value !== "off") fail(`Error: argument "${value ?? ""}" is wrong: ${key} must be "on" or "off"`);
373
+ return value;
374
+ };
375
+ while (index < words.length) {
376
+ const key = words[index++];
377
+ const value = words[index];
378
+ try {
379
+ if (key === "up") setFlags(flagsOf() | IFF.UP);
380
+ else if (key === "down") setFlags(flagsOf() & ~IFF.UP);
381
+ else if (key === "mtu") {
382
+ if (!/^\d+$/.test(value ?? "")) fail(`Error: argument "${value ?? ""}" is wrong: Invalid "mtu" value`);
383
+ view(request).setInt32(16, Number(value), true);
384
+ ioctl(SIOC.SIFMTU, request, "SIOCSIFMTU");
385
+ index++;
386
+ } else if (key === "address" || key === "addr" || key === "lladdr") {
387
+ const bytes = (value ?? "").split(":").map((part) => parseInt(part, 16));
388
+ if (bytes.length !== 6 || bytes.some(Number.isNaN)) fail(`Error: argument "${value ?? ""}" is wrong: "address" value is invalid`);
389
+ const hardware = ifreq(name);
390
+ view(hardware).setUint16(16, 1, true); // ARPHRD_ETHER
391
+ hardware.set(bytes, 18);
392
+ ioctl(SIOC.SIFHWADDR, hardware, "SIOCSIFHWADDR");
393
+ index++;
394
+ } else if (key === "name") {
395
+ if (!value) fail('Error: argument "name" is required');
396
+ const rename = ifreq(name);
397
+ rename.set(new TextEncoder().encode(value.slice(0, IFNAMSIZ - 1)), 16);
398
+ ioctl(SIOC.SIFNAME, rename, "SIOCSIFNAME");
399
+ name = value;
400
+ request.fill(0);
401
+ request.set(new TextEncoder().encode(name), 0);
402
+ index++;
403
+ } else if (key === "promisc") toggle(IFF.PROMISC, onOff(key, value)), index++;
404
+ else if (key === "allmulticast") toggle(IFF.ALLMULTI, onOff(key, value)), index++;
405
+ else if (key === "multicast") toggle(IFF.MULTICAST, onOff(key, value)), index++;
406
+ else if (key === "arp") toggle(IFF.NOARP, onOff(key, value) === "on" ? "off" : "on"), index++;
407
+ else if (key === "dev") {} // `ip link set dev eth0 up` already consumed
408
+ else fail(`Error: either "dev" is duplicate, or "${key}" is a garbage.`);
409
+ } catch (error) {
410
+ translate(error, name);
411
+ }
412
+ }
413
+ });
414
+ };
415
+
416
+ const commandAddress = (words, options) => {
417
+ const verb = words.length ? matchWord(words[0], ["show", "list", "lst", "ls", "add", "change", "replace", "delete", "flush", "help"], "Command") : "show";
418
+ if (verb === "help") return showDocument("ip");
419
+
420
+ if (verb === "show" || verb === "list" || verb === "lst" || verb === "ls") {
421
+ let list = interfaces();
422
+ let index = 1;
423
+ while (index < words.length) {
424
+ if (words[index] === "dev" || index === 1 && !["up", "scope", "to"].includes(words[index])) {
425
+ const [name, next] = takeDevice(words, index);
426
+ list = [interfaceNamed(name)];
427
+ index = next;
428
+ } else if (words[index] === "up") {
429
+ list = list.filter((link) => link.flags & IFF.UP);
430
+ index++;
431
+ } else fail(`Error: argument "${words[index]}" is not supported here.`);
432
+ }
433
+ for (const link of list) {
434
+ if (options.brief) printBrief(link, { addresses: true, family: options.family });
435
+ else {
436
+ printLink(link);
437
+ printAddresses(link, options.family);
438
+ }
439
+ }
440
+ return;
441
+ }
442
+
443
+ // add | change | replace | delete | flush
444
+ let cidr = null, broadcast = null, name = null;
445
+ let index = 1;
446
+ while (index < words.length) {
447
+ const word = words[index];
448
+ if (word === "dev") [name, index] = takeDevice(words, index);
449
+ else if (word === "brd" || word === "broadcast") { broadcast = words[index + 1] ?? fail('Error: argument "brd" needs a value'); index += 2; }
450
+ else if (word === "local" || word === "to") { cidr = words[index + 1] ?? fail('Error: argument "local" needs a value'); index += 2; }
451
+ else if (word === "scope" || word === "label" || word === "valid_lft" || word === "preferred_lft") index += 2; // accepted, ignored
452
+ else if (!cidr && !word.startsWith("-")) { cidr = word; index++; }
453
+ else fail(`Error: either "local" is duplicate, or "${word}" is a garbage.`);
454
+ }
455
+ if (!name) fail('Not enough information: "dev" argument is required.');
456
+ interfaceNamed(name);
457
+ if (verb !== "flush" && !cidr) fail("Not enough information: \"local\" argument is required.");
458
+ if (cidr?.includes(":")) fail("IPv6 addresses cannot be configured with this ip (ioctl-based); IPv6 is shown only.");
459
+
460
+ withInetSocket((ioctl) => {
461
+ const request = ifreq(name);
462
+ try {
463
+ if (verb === "delete" || verb === "flush") {
464
+ // Writing 0.0.0.0 as the interface address removes the primary
465
+ // address and, with it, its secondaries. Deleting checks the
466
+ // argument matches what is configured, the way iproute2 does.
467
+ if (verb === "delete") {
468
+ const current = ipv4Of(name);
469
+ const wanted = parseCidr(cidr, { defaultPrefix: current?.prefix ?? 32 });
470
+ if (!current || formatIpv4(current.address) !== formatIpv4(wanted.address)) fail("RTNETLINK answers: Cannot assign requested address", 2);
471
+ }
472
+ setSockaddrIn(request, 16, [0, 0, 0, 0]);
473
+ ioctl(SIOC.SIFADDR, request, "SIOCSIFADDR");
474
+ return;
475
+ }
476
+ const { address, prefix } = parseCidr(cidr);
477
+ if (verb === "add") {
478
+ const current = ipv4Of(name);
479
+ if (current && formatIpv4(current.address) === formatIpv4(address)) fail("RTNETLINK answers: File exists", 2);
480
+ if (current) fail(`${name} already has ${formatIpv4(current.address)}/${current.prefix}; this ioctl-based ip keeps one IPv4 address per interface (use "replace", or "del" first).`, 2);
481
+ }
482
+ setSockaddrIn(request, 16, address);
483
+ ioctl(SIOC.SIFADDR, request, "SIOCSIFADDR");
484
+ setSockaddrIn(request, 16, prefixToMask(prefix));
485
+ ioctl(SIOC.SIFNETMASK, request, "SIOCSIFNETMASK");
486
+ if (broadcast) {
487
+ const mask = prefixToMask(prefix);
488
+ const value = broadcast === "+" ? address.map((byte, i) => byte | (~mask[i] & 255))
489
+ : broadcast === "-" ? address.map((byte, i) => byte & mask[i])
490
+ : parseIpv4(broadcast);
491
+ setSockaddrIn(request, 16, value);
492
+ ioctl(SIOC.SIFBRDADDR, request, "SIOCSIFBRDADDR");
493
+ }
494
+ } catch (error) {
495
+ translate(error, name);
496
+ }
497
+ });
498
+ };
499
+
500
+ // struct rtentry on x86_64: rt_dst @8, rt_gateway @24, rt_genmask @40 (each a
501
+ // 16-byte sockaddr), rt_flags u16 @56, rt_metric i16 @80, rt_dev char* @88.
502
+ const rtentry = ({ destination, prefix, gateway, device, metric }) => {
503
+ const route = new Uint8Array(120);
504
+ const routeView = view(route);
505
+ setSockaddrIn(route, 8, destination);
506
+ setSockaddrIn(route, 24, gateway ?? [0, 0, 0, 0]);
507
+ setSockaddrIn(route, 40, prefixToMask(prefix));
508
+ let flags = RTF.UP;
509
+ if (gateway) flags |= RTF.GATEWAY;
510
+ if (prefix === 32) flags |= RTF.HOST;
511
+ routeView.setUint16(56, flags, true);
512
+ // The kernel stores rt_metric - 1, so 0 here means "unset".
513
+ routeView.setInt16(80, metric === null ? 0 : metric + 1, true);
514
+ const deviceName = device ? cString(device) : null;
515
+ if (deviceName) routeView.setBigUint64(88, BigInt(ptr(deviceName)), true);
516
+ return { route, deviceName }; // deviceName kept alive alongside the pointer
517
+ };
518
+
519
+ const commandRoute = (words, options) => {
520
+ const verb = words.length ? matchWord(words[0], ["show", "list", "lst", "ls", "add", "delete", "replace", "change", "help", "get", "flush"], "Command") : "show";
521
+ if (verb === "help") return showDocument("ip");
522
+ if (options.family === 6) fail("IPv6 routes are not supported by this ip (ioctl-based).");
523
+ if (verb === "get" || verb === "flush") fail(`"ip route ${verb}" needs rtnetlink, which this ip does not speak yet.`);
524
+ if (verb === "show" || verb === "list" || verb === "lst" || verb === "ls") return printRoutes();
525
+
526
+ let destination = null, prefix = null, gateway = null, device = null, metric = null;
527
+ let index = 1;
528
+ while (index < words.length) {
529
+ const word = words[index];
530
+ if (word === "via") { gateway = parseIpv4(words[index + 1] ?? fail('Error: argument "via" needs a value')); index += 2; }
531
+ else if (word === "dev") [device, index] = takeDevice(words, index);
532
+ else if (word === "metric" || word === "priority" || word === "preference") {
533
+ if (!/^\d+$/.test(words[index + 1] ?? "")) fail(`Error: argument "${words[index + 1] ?? ""}" is wrong: "metric" value is invalid`);
534
+ metric = Number(words[index + 1]);
535
+ index += 2;
536
+ }
537
+ else if (word === "to") index++;
538
+ else if (word === "default" || word === "0/0" || word === "0.0.0.0/0") { destination = [0, 0, 0, 0]; prefix = 0; index++; }
539
+ else if (word === "proto" || word === "scope" || word === "src" || word === "table" || word === "mtu" || word === "onlink") index += word === "onlink" ? 1 : 2;
540
+ else if (destination === null && !word.startsWith("-")) {
541
+ ({ address: destination, prefix } = parseCidr(word));
542
+ index++;
543
+ }
544
+ else fail(`Error: either "to" is duplicate, or "${word}" is a garbage.`);
545
+ }
546
+ if (destination === null) fail('Not enough information: "to" argument is required.');
547
+ // Deleting matches on destination alone; adding needs somewhere to go.
548
+ if (verb !== "delete" && !gateway && !device) fail('Not enough information: "via" or "dev" argument is required.');
549
+ if (device) interfaceNamed(device);
550
+ if (gateway && !device) {
551
+ // Pick the interface whose subnet contains the gateway, like the kernel would.
552
+ for (const route of routes()) {
553
+ const mask = prefixToMask(route.prefix);
554
+ if (route.flags & RTF.GATEWAY) continue;
555
+ if (gateway.every((byte, i) => (byte & mask[i]) === (route.destination[i] & mask[i]))) { device = route.device; break; }
556
+ }
557
+ }
558
+ const entry = rtentry({ destination, prefix, gateway, device, metric });
559
+ withInetSocket((ioctl) => {
560
+ try {
561
+ if (verb === "replace" || verb === "change") {
562
+ // ESRCH when there is nothing to replace; the add below still runs.
563
+ ioctl(SIOC.DELRT, entry.route, "SIOCDELRT", [Errno.ESRCH, Errno.ENOENT, Errno.EINVAL]);
564
+ }
565
+ if (verb === "delete") {
566
+ const result = ioctl(SIOC.DELRT, entry.route, "SIOCDELRT", [Errno.ESRCH]);
567
+ if (result < 0) fail("RTNETLINK answers: No such process", 2);
568
+ return;
569
+ }
570
+ const result = ioctl(SIOC.ADDRT, entry.route, "SIOCADDRT", [Errno.EEXIST, Errno.ENETUNREACH]);
571
+ if (result === -Errno.EEXIST) fail("RTNETLINK answers: File exists", 2);
572
+ if (result === -Errno.ENETUNREACH) fail("RTNETLINK answers: Network is unreachable", 2);
573
+ } catch (error) {
574
+ translate(error, device ?? "");
575
+ }
576
+ });
577
+ };
578
+
579
+ const commandNeighbour = (words) => {
580
+ const verb = words.length ? matchWord(words[0], ["show", "list", "lst", "ls", "help", "add", "delete", "flush", "replace"], "Command") : "show";
581
+ if (verb === "help") return showDocument("ip");
582
+ if (verb !== "show" && verb !== "list" && verb !== "lst" && verb !== "ls") fail(`"ip neigh ${verb}" needs rtnetlink, which this ip does not speak yet.`);
583
+ return printNeighbours();
584
+ };
585
+
586
+ // ------------------------------------------------------------------ main ---
587
+
588
+ const main = async () => {
589
+ const options = { family: null, brief: false };
590
+ const words = [];
591
+ const argv = process.argv.slice(2);
592
+ for (let i = 0; i < argv.length; i++) {
593
+ const argument = argv[i];
594
+ if (words.length || !argument.startsWith("-")) { words.push(argument); continue; }
595
+ if (argument === "-h" || argument === "--help" || argument === "-help") return showDocument("ip");
596
+ if (argument === "-V" || argument === "-Version" || argument === "--version") return console.log("ip utility, Buninu Linux (ioctl + procfs edition)");
597
+ if (argument === "-4") options.family = 4;
598
+ else if (argument === "-6") options.family = 6;
599
+ else if (argument === "-br" || argument === "-brief") options.brief = true;
600
+ else if (argument === "-f" || argument === "-family") {
601
+ const family = argv[++i];
602
+ options.family = family === "inet" ? 4 : family === "inet6" ? 6 : fail(`Protocol "${family}" is unknown, try "ip help".`);
603
+ }
604
+ else if (["-c", "-color", "-s", "-stats", "-statistics", "-d", "-details", "-p", "-pretty", "-o", "-oneline", "-n", "-netns"].includes(argument)) {
605
+ if (argument === "-n" || argument === "-netns") i++;
606
+ }
607
+ else fail(`Option "${argument}" is unknown, try "ip -help".`);
608
+ }
609
+ if (words.length === 0) {
610
+ console.error('Usage: ip [ -4 | -6 | -br ] OBJECT { COMMAND | help }\n ip --help\nOBJECT := { link | address | route | neighbour }');
611
+ process.exit(255);
612
+ }
613
+ const object = matchWord(words[0], ["address", "link", "route", "neighbour", "neighbor", "neigh", "help"], "Object");
614
+ const rest = words.slice(1);
615
+ switch (object) {
616
+ case "help": return showDocument("ip");
617
+ case "address": return commandAddress(rest, options);
618
+ case "link": return commandLink(rest, options);
619
+ case "route": return commandRoute(rest, options);
620
+ default: return commandNeighbour(rest, options);
621
+ }
622
+ };
623
+
624
+ try {
625
+ await main();
626
+ } catch (error) {
627
+ if (error instanceof SysError) fail(error.message, 2);
628
+ throw error;
629
+ }