buninu-linux 0.2.1 → 0.3.1

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.
Files changed (42) hide show
  1. package/README.md +547 -190
  2. package/build-uki.sh +4 -2
  3. package/fetch-alpine.sh +78 -43
  4. package/index.js +4 -1
  5. package/initramfs/bin/ip +630 -0
  6. package/initramfs/bin/mount +401 -0
  7. package/initramfs/bin/poweroff +10 -0
  8. package/initramfs/bin/ps +256 -0
  9. package/initramfs/bin/reboot +10 -0
  10. package/initramfs/bin/tar +169 -0
  11. package/initramfs/bin/top +129 -0
  12. package/initramfs/bin/umount +71 -0
  13. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +36 -0
  14. package/initramfs/buninu/apps/bunmsh/README.md +7 -0
  15. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  16. package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +29 -14
  17. package/initramfs/buninu/apps/bunmsh/src/line-edit.js +43 -0
  18. package/initramfs/buninu/apps/bunmsh/src/main.js +15 -1
  19. package/initramfs/buninu/apps/bunmsh/src/mouse.js +29 -4
  20. package/initramfs/buninu/apps/bunmsh/src/shell.js +9 -1
  21. package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +70 -0
  22. package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +10 -0
  23. package/initramfs/buninu/apps/jsmdcui/README.md +2 -0
  24. package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
  25. package/initramfs/buninu/apps/jsmdcui/runtime/help/commands.md +4 -1
  26. package/initramfs/buninu/apps/jsmdcui/runtime/help/help.md +2 -0
  27. package/initramfs/buninu/apps/jsmdcui/src/index.js +40 -13
  28. package/initramfs/buninu/package.json +1 -1
  29. package/initramfs/init.js +164 -11
  30. package/initramfs/lib/dlopen.js +121 -0
  31. package/initramfs/lib/document.js +6 -0
  32. package/initramfs/lib/modprobe.js +235 -0
  33. package/initramfs/lib/power.js +35 -0
  34. package/initramfs/usr/share/doc/buninu-linux/ip.md +155 -0
  35. package/initramfs/usr/share/doc/buninu-linux/mount.md +155 -0
  36. package/initramfs/usr/share/doc/buninu-linux/poweroff.md +15 -0
  37. package/initramfs/usr/share/doc/buninu-linux/ps.md +94 -0
  38. package/initramfs/usr/share/doc/buninu-linux/reboot.md +15 -0
  39. package/initramfs/usr/share/doc/buninu-linux/tar.md +73 -0
  40. package/initramfs/usr/share/doc/buninu-linux/top.md +23 -0
  41. package/initramfs/usr/share/doc/buninu-linux/umount.md +48 -0
  42. package/package.json +2 -2
@@ -0,0 +1,121 @@
1
+ // /lib/dlopen.js — the shared libc binding for Buninu Linux commands.
2
+ //
3
+ // init.js opens musl on its own because it runs before /proc exists and must
4
+ // not import anything. Every other command imports this module instead, so
5
+ // the symbol table, the errno plumbing and the error messages live in one
6
+ // place. The library is located relative to this file (/lib in the guest),
7
+ // which also lets the commands run from a checkout for testing.
8
+
9
+ import { dlopen, FFIType, ptr, read, CString } from "bun:ffi";
10
+
11
+ export { ptr, read };
12
+
13
+ // Everything that differs per CPU lives in this table: the musl file name
14
+ // and the syscall numbers musl has no wrapper for. Struct layouts used by
15
+ // the commands (ifreq, rtentry) are the LP64 ones and hold on both.
16
+ const architectures = {
17
+ x64: { libc: "libc.musl-x86_64.so.1", SYS_finit_module: 313 },
18
+ arm64: { libc: "libc.musl-aarch64.so.1", SYS_finit_module: 273 },
19
+ };
20
+ export const arch = architectures[process.arch]
21
+ ?? (() => { throw new Error(`unsupported CPU architecture ${process.arch}`); })();
22
+
23
+ // BUNINU_LIBC lets the commands run on a glibc host for testing:
24
+ // BUNINU_LIBC=/lib/aarch64-linux-gnu/libc.so.6 bun initramfs/bin/ip addr
25
+ export const libcPath = process.env.BUNINU_LIBC ?? `${import.meta.dir}/${arch.libc}`;
26
+
27
+ export const libc = dlopen(libcPath, {
28
+ mount: {
29
+ args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u64, FFIType.ptr],
30
+ returns: FFIType.i32,
31
+ },
32
+ umount2: {
33
+ args: [FFIType.ptr, FFIType.i32],
34
+ returns: FFIType.i32,
35
+ },
36
+ reboot: {
37
+ args: [FFIType.i32],
38
+ returns: FFIType.i32,
39
+ },
40
+ sync: {
41
+ args: [],
42
+ returns: FFIType.void,
43
+ },
44
+ __errno_location: {
45
+ args: [],
46
+ returns: FFIType.ptr,
47
+ },
48
+ strerror: {
49
+ args: [FFIType.i32],
50
+ returns: FFIType.ptr,
51
+ },
52
+ open: {
53
+ args: [FFIType.ptr, FFIType.i32, FFIType.i32],
54
+ returns: FFIType.i32,
55
+ },
56
+ close: {
57
+ args: [FFIType.i32],
58
+ returns: FFIType.i32,
59
+ },
60
+ ioctl: {
61
+ args: [FFIType.i32, FFIType.u64, FFIType.ptr],
62
+ returns: FFIType.i32,
63
+ },
64
+ socket: {
65
+ args: [FFIType.i32, FFIType.i32, FFIType.i32],
66
+ returns: FFIType.i32,
67
+ },
68
+ syscall: {
69
+ args: [FFIType.i64, FFIType.i64, FFIType.ptr, FFIType.i64],
70
+ returns: FFIType.i32,
71
+ },
72
+ waitpid: {
73
+ args: [FFIType.i32, FFIType.ptr, FFIType.i32],
74
+ returns: FFIType.i32,
75
+ },
76
+ });
77
+
78
+ export const cString = (value) => new TextEncoder().encode(`${value}\0`);
79
+
80
+ // musl reports failure as -1 plus errno; the raw syscall() wrapper does too.
81
+ const errnoLocation = libc.symbols.__errno_location();
82
+ export const errno = () => read.i32(errnoLocation);
83
+ export const strerror = (code) => new CString(libc.symbols.strerror(code)).toString();
84
+
85
+ export const Errno = {
86
+ EPERM: 1, ENOENT: 2, ESRCH: 3, EIO: 5, ENXIO: 6, EBADF: 9, EAGAIN: 11, ENOMEM: 12,
87
+ EACCES: 13, EBUSY: 16, EEXIST: 17, ENODEV: 19, ENOTDIR: 20, EISDIR: 21,
88
+ EINVAL: 22, EROFS: 30, ENOTEMPTY: 39, ENOPKG: 65, ENOTSUP: 95,
89
+ EADDRNOTAVAIL: 99, ENETUNREACH: 101,
90
+ };
91
+
92
+ export class SysError extends Error {
93
+ constructor(operation, code = errno()) {
94
+ super(`${operation}: ${strerror(code)}`);
95
+ this.name = "SysError";
96
+ this.operation = operation;
97
+ this.errno = code;
98
+ }
99
+ }
100
+
101
+ // Wraps a libc call: throws SysError on -1 unless errno is in `allowed`,
102
+ // in which case the negated errno comes back so the caller can tell.
103
+ export const check = (operation, result, allowed = []) => {
104
+ if (result !== -1) return result;
105
+ const code = errno();
106
+ if (allowed.includes(code)) return -code;
107
+ throw new SysError(operation, code);
108
+ };
109
+
110
+ // An AF_INET datagram socket is the handle every SIOC* interface and route
111
+ // ioctl wants. `use` receives an ioctl(request, buffer, operation) closure.
112
+ export const withInetSocket = (use) => {
113
+ const fd = check("socket(AF_INET, SOCK_DGRAM)", libc.symbols.socket(2, 2, 0));
114
+ try {
115
+ const ioctl = (request, buffer, operation, allowed) =>
116
+ check(operation, libc.symbols.ioctl(fd, request, ptr(buffer)), allowed);
117
+ return use(ioctl, fd);
118
+ } finally {
119
+ libc.symbols.close(fd);
120
+ }
121
+ };
@@ -0,0 +1,6 @@
1
+ // Shared renderer for /usr/share/doc/buninu-linux/<name>.md command manuals.
2
+ export const showDocument = async (name) => {
3
+ const path = `${import.meta.dir}/../usr/share/doc/buninu-linux/${name}.md`;
4
+ const rendered = Bun.markdown.ansi(await Bun.file(path).text(), { hyperlinks: true });
5
+ process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}\n`);
6
+ };
@@ -0,0 +1,235 @@
1
+ // /lib/modprobe.js — kernel module loading shared by the Buninu Linux commands.
2
+ //
3
+ // There is no /sbin/modprobe for the kernel to call, so anything that needs a
4
+ // driver (a filesystem, a block device, a NIC) has to load it, and its
5
+ // dependencies, itself. fetch-alpine.sh ships the modules under
6
+ // /lib/modules/<release>/ together with a modules.dep trimmed to that set and
7
+ // the `fs-*` lines of modules.alias, and this module reads those.
8
+
9
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
10
+ import { libc, cString, check, ptr, Errno, arch } from "./dlopen.js";
11
+
12
+ export const kernelRelease = () => {
13
+ if (process.env.KERNEL_RELEASE) return process.env.KERNEL_RELEASE;
14
+ try {
15
+ return readFileSync("/proc/sys/kernel/osrelease", "utf8").trim();
16
+ } catch {
17
+ return "unknown";
18
+ }
19
+ };
20
+
21
+ // /lib/modules/<release> in the guest; resolved relative to this file so a
22
+ // checkout can be tested the same way.
23
+ export const modulesDirectory = () => `${import.meta.dir}/modules/${kernelRelease()}`;
24
+
25
+ // Module names are canonical with underscores: usb-common.ko is usb_common in
26
+ // /proc/modules.
27
+ const canonical = (name) => name.replaceAll("-", "_");
28
+
29
+ const readIndex = (file) => {
30
+ try {
31
+ return readFileSync(`${modulesDirectory()}/${file}`, "utf8");
32
+ } catch {
33
+ return "";
34
+ }
35
+ };
36
+
37
+ // name -> { path, dependencies: [names] } from modules.dep.
38
+ let dependencyTable;
39
+ const dependencies = () => {
40
+ if (dependencyTable) return dependencyTable;
41
+ dependencyTable = new Map();
42
+ for (const line of readIndex("modules.dep").split("\n")) {
43
+ const separator = line.indexOf(":");
44
+ if (separator < 0) continue;
45
+ const path = line.slice(0, separator);
46
+ const deps = line.slice(separator + 1).trim().split(/\s+/).filter(Boolean);
47
+ dependencyTable.set(canonical(moduleName(path)), {
48
+ path,
49
+ dependencies: deps.map((dep) => canonical(moduleName(dep))),
50
+ });
51
+ }
52
+ return dependencyTable;
53
+ };
54
+
55
+ const moduleName = (path) => path.slice(path.lastIndexOf("/") + 1).replace(/\.ko(\.gz)?$/, "");
56
+
57
+ // modules.alias: exact names (fs-ntfs -> ntfs3) in a map, and the device
58
+ // patterns (pci:v000010ECd00008168sv*sd*bc*sc*i* -> r8169) as regexps.
59
+ let aliasTable;
60
+ const aliases = () => {
61
+ if (aliasTable) return aliasTable;
62
+ aliasTable = { exact: new Map(), patterns: [] };
63
+ for (const line of readIndex("modules.alias").split("\n")) {
64
+ const [keyword, pattern, target] = line.trim().split(/\s+/);
65
+ if (keyword !== "alias" || !target) continue;
66
+ const module = canonical(target);
67
+ if (/[*?]/.test(pattern)) {
68
+ const source = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*").replaceAll("?", ".");
69
+ aliasTable.patterns.push({ regexp: new RegExp(`^${source}$`), module });
70
+ } else {
71
+ aliasTable.exact.set(pattern, module);
72
+ }
73
+ }
74
+ return aliasTable;
75
+ };
76
+
77
+ export const resolveAlias = (alias) => {
78
+ const { exact } = aliases();
79
+ return exact.get(alias) ?? exact.get(canonical(alias)) ?? canonical(alias);
80
+ };
81
+
82
+ // The modules whose alias patterns match a device's modalias string.
83
+ export const modulesForModalias = (modalias) => {
84
+ const found = [];
85
+ for (const { regexp, module } of aliases().patterns) {
86
+ if (regexp.test(modalias) && !found.includes(module)) found.push(module);
87
+ }
88
+ return found;
89
+ };
90
+
91
+ // What udev's coldplug does: read each device's modalias under
92
+ // /sys/bus/<bus>/devices and load the drivers that claim it. `accept`
93
+ // filters by modalias (e.g. PCI class 02 = network: /bc02/), `acceptModule`
94
+ // by module name and path (e.g. only kernel/drivers/net/). A driver built
95
+ // into the kernel has no alias line, so its device simply goes unreported.
96
+ // Returns one
97
+ // entry per device with the modules loaded, or the error, for it.
98
+ export const modulePath = (name) => dependencies().get(resolveAlias(name))?.path ?? null;
99
+
100
+ export const autoload = ({ buses = ["pci", "virtio", "usb"], accept = () => true, acceptModule = () => true } = {}) => {
101
+ const report = [];
102
+ for (const bus of buses) {
103
+ let devices;
104
+ try {
105
+ devices = readdirSync(`/sys/bus/${bus}/devices`);
106
+ } catch {
107
+ continue;
108
+ }
109
+ for (const device of devices) {
110
+ let modalias;
111
+ try {
112
+ modalias = readFileSync(`/sys/bus/${bus}/devices/${device}/modalias`, "utf8").trim();
113
+ } catch {
114
+ continue;
115
+ }
116
+ if (!accept(modalias, device)) continue;
117
+ const modules = modulesForModalias(modalias).filter((module) => acceptModule(module, modulePath(module) ?? ""));
118
+ if (modules.length === 0) continue;
119
+ const entry = { bus, device, modalias, modules, loaded: [], error: null };
120
+ for (const module of modules) {
121
+ try {
122
+ entry.loaded.push(...modprobe(module));
123
+ } catch (error) {
124
+ entry.error = error.message;
125
+ }
126
+ }
127
+ report.push(entry);
128
+ }
129
+ }
130
+ return report;
131
+ };
132
+
133
+ export const loadedModules = () => {
134
+ const loaded = new Set();
135
+ try {
136
+ for (const line of readFileSync("/proc/modules", "utf8").split("\n")) {
137
+ const name = line.split(" ")[0];
138
+ if (name) loaded.add(name);
139
+ }
140
+ } catch {}
141
+ return loaded;
142
+ };
143
+
144
+ export const isLoaded = (name) => loadedModules().has(canonical(name));
145
+
146
+ // Built-in drivers are absent from /proc/modules and from modules.dep, so
147
+ // the ones that matter to callers are listed in modules.builtin.
148
+ let builtinTable;
149
+ export const isBuiltin = (name) => {
150
+ if (!builtinTable) {
151
+ builtinTable = new Set(
152
+ readIndex("modules.builtin").split("\n").filter(Boolean).map((path) => canonical(moduleName(path))),
153
+ );
154
+ }
155
+ return builtinTable.has(canonical(name));
156
+ };
157
+
158
+ // finit_module(2) on one .ko file. EEXIST (loaded meanwhile, or built in
159
+ // and registered under the same name) is not an error.
160
+ export const insertModule = (path, parameters = "") => {
161
+ const fd = check(`open(${path})`, libc.symbols.open(ptr(cString(path)), 0, 0));
162
+ try {
163
+ const flags = path.endsWith(".gz") ? 4 : 0; // MODULE_INIT_COMPRESSED_FILE
164
+ // musl has no finit_module wrapper, so use its generic syscall(2).
165
+ const result = check(
166
+ `finit_module(${path})`,
167
+ libc.symbols.syscall(arch.SYS_finit_module, fd, ptr(cString(parameters)), flags),
168
+ [Errno.EEXIST],
169
+ );
170
+ return result === 0;
171
+ } finally {
172
+ libc.symbols.close(fd);
173
+ }
174
+ };
175
+
176
+ export class ModuleNotFound extends Error {
177
+ constructor(name) {
178
+ super(`module ${name} not found in ${modulesDirectory()}`);
179
+ this.name = "ModuleNotFound";
180
+ this.module = name;
181
+ }
182
+ }
183
+
184
+ // Loads `name` (a module name or a modules.alias key) and, first, whatever
185
+ // modules.dep says it needs. Returns the list of modules actually inserted.
186
+ export const modprobe = (name, parameters = "", { loaded = loadedModules(), inserted = [] } = {}) => {
187
+ const module = resolveAlias(name);
188
+ if (loaded.has(module) || isBuiltin(module)) return inserted;
189
+
190
+ const entry = dependencies().get(module);
191
+ if (!entry) throw new ModuleNotFound(module);
192
+
193
+ // modules.dep lists dependencies in reverse load order; recursing on each
194
+ // one covers the deps-of-deps case no matter how the line is ordered.
195
+ for (const dependency of [...entry.dependencies].reverse()) {
196
+ modprobe(dependency, "", { loaded, inserted });
197
+ }
198
+
199
+ const path = `${modulesDirectory()}/${entry.path}`;
200
+ if (!existsSync(path)) throw new ModuleNotFound(module);
201
+ if (insertModule(path, parameters)) inserted.push(module);
202
+ loaded.add(module);
203
+ return inserted;
204
+ };
205
+
206
+ // Load every selected module shipped in the initramfs (all modules by
207
+ // default). This is useful on the small real-machine image, where there is no
208
+ // userspace modprobe helper to satisfy a driver's later request for an
209
+ // optional PHY or bus module. Keep going after an individual failure: a
210
+ // module for absent hardware commonly rejects initialization, and that must
211
+ // not prevent the remaining modules loading.
212
+ export const modprobeAll = ({ accept = () => true } = {}) => {
213
+ const loaded = loadedModules();
214
+ const inserted = [];
215
+ const errors = [];
216
+ for (const [module, entry] of dependencies()) {
217
+ if (!accept(module, entry.path)) continue;
218
+ try {
219
+ modprobe(module, "", { loaded, inserted });
220
+ } catch (error) {
221
+ errors.push({ module, error: error.message });
222
+ }
223
+ }
224
+ return { inserted, errors };
225
+ };
226
+
227
+ // Best-effort variant for optional drivers: a missing module is not an error.
228
+ export const tryModprobe = (name, parameters = "") => {
229
+ try {
230
+ return modprobe(name, parameters);
231
+ } catch (error) {
232
+ if (error instanceof ModuleNotFound) return null;
233
+ throw error;
234
+ }
235
+ };
@@ -0,0 +1,35 @@
1
+ // Shared implementation for /bin/poweroff and /bin/reboot.
2
+
3
+ import { libc, check } from "./dlopen.js";
4
+ import { showDocument } from "./document.js";
5
+
6
+ export const LINUX_REBOOT_CMD_RESTART = 0x01234567;
7
+ export const LINUX_REBOOT_CMD_POWER_OFF = 0x4321fedc;
8
+
9
+ export const runPowerCommand = async ({ name, command, action }) => {
10
+ const arguments_ = process.argv.slice(2);
11
+
12
+ if (arguments_.length === 1 && (arguments_[0] === "-h" || arguments_[0] === "--help")) {
13
+ await showDocument(name);
14
+ return;
15
+ }
16
+
17
+ if (arguments_.length !== 0) {
18
+ console.error(`Usage: ${name} [-h | --help]`);
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log(`${name}: syncing filesystems`);
23
+ libc.symbols.sync();
24
+ console.log(`${name}: ${action}`);
25
+ try {
26
+ check(`reboot(${command.name})`, libc.symbols.reboot(command.value));
27
+ } catch (error) {
28
+ console.error(`${name}: ${error.message}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ // A successful reboot(2) never returns.
33
+ console.error(`${name}: reboot returned without ${action}`);
34
+ process.exit(1);
35
+ };
@@ -0,0 +1,155 @@
1
+ # ip — show and configure network interfaces, addresses and routes
2
+
3
+ `/bin/ip` is a Bun script with the iproute2 command grammar. It reads
4
+ `/sys/class/net` and `/proc/net`, and configures the kernel through the
5
+ classic `SIOC*` ioctls on an AF_INET socket bound in `/lib/dlopen.js` — the
6
+ same calls `init.js` uses to bring up `lo` and `eth0` at boot. It does not
7
+ speak rtnetlink, which keeps it small; the limits that come with that are
8
+ listed at the end.
9
+
10
+ ## Synopsis
11
+
12
+ ```sh
13
+ ip [-4 | -6] [-br] OBJECT [COMMAND] [ARGUMENTS]
14
+ ip -h | --help | help # this page
15
+ OBJECT := link | address | route | neighbour
16
+ ```
17
+
18
+ Any unambiguous prefix works, as in iproute2: `ip a`, `ip addr`, `ip li`,
19
+ `ip r`, `ip n`, and `show`/`list`/`ls`, `del`/`delete`.
20
+
21
+ ## Global options
22
+
23
+ | Option | Meaning |
24
+ |---|---|
25
+ | `-4` / `-6` | Show only IPv4 / IPv6 addresses. |
26
+ | `-br`, `-brief` | One line per interface. |
27
+ | `-f inet\|inet6` | Same as `-4` / `-6`. |
28
+ | `-c`, `-s`, `-d`, `-o`, `-p` | Accepted for muscle memory, no effect. |
29
+
30
+ ## Links
31
+
32
+ ```sh
33
+ ip link # every interface: flags, mtu, state, MAC
34
+ ip -br link # eth0 UP 52:54:00:12:34:56 <BROADCAST,...>
35
+ ip link show eth0
36
+ ip link set eth0 up
37
+ ip link set eth0 down
38
+ ip link set eth0 mtu 1400
39
+ ip link set eth0 address 02:00:00:00:00:01 # interface must be down
40
+ ip link set eth0 name lan0
41
+ ip link set eth0 promisc on
42
+ ip link set eth0 up mtu 9000 # several changes at once
43
+ ```
44
+
45
+ `ip link add`/`delete` (vlan, bridge, dummy, ...) need rtnetlink and are not
46
+ available.
47
+
48
+ ## Addresses
49
+
50
+ ```sh
51
+ ip addr # links plus their inet / inet6 addresses
52
+ ip -br addr # eth0 UP 10.0.2.15/24 fec0::5054:ff:fe12:3456/64
53
+ ip -4 addr show eth0
54
+ ip addr add 10.0.2.15/24 dev eth0
55
+ ip addr add 192.168.1.10/24 brd + dev eth0
56
+ ip addr replace 192.168.1.11/24 dev eth0 # change the address in place
57
+ ip addr del 10.0.2.15/24 dev eth0
58
+ ip addr flush dev eth0
59
+ ```
60
+
61
+ One IPv4 address per interface: `add` refuses when one is already set (use
62
+ `replace`, or `del` first). IPv6 addresses are shown from
63
+ `/proc/net/if_inet6` but cannot be added or removed here.
64
+
65
+ ## Routes
66
+
67
+ ```sh
68
+ ip route # default via 10.0.2.2 dev eth0
69
+ # 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15
70
+ ip route add default via 10.0.2.2
71
+ ip route add default via 10.0.2.2 dev eth0 metric 100
72
+ ip route add 192.168.5.0/24 via 10.0.2.1
73
+ ip route add 10.10.0.0/16 dev eth0 # on-link, no gateway
74
+ ip route add 10.0.2.99 dev eth0 # host route (/32)
75
+ ip route replace default via 10.0.2.1
76
+ ip route del default
77
+ ip route del 192.168.5.0/24
78
+ ```
79
+
80
+ Without `dev`, the interface whose subnet contains the gateway is picked.
81
+ IPv6 routes and `ip route get` are not available.
82
+
83
+ ## Neighbours
84
+
85
+ ```sh
86
+ ip neigh # 10.0.2.2 dev eth0 lladdr 52:55:0a:00:02:02 REACHABLE
87
+ ```
88
+
89
+ Read from `/proc/net/arp`. Entries cannot be added or flushed here.
90
+
91
+ ## Bringing up a wired network by hand
92
+
93
+ The QEMU user-mode network `init.js` configures at boot, done manually:
94
+
95
+ ```sh
96
+ ip link set eth0 up
97
+ ip addr add 10.0.2.15/24 dev eth0
98
+ ip route add default via 10.0.2.2
99
+ echo 'nameserver 10.0.2.3' > /etc/resolv.conf
100
+ ip -br addr && ip route
101
+ ```
102
+
103
+ A real Ethernet port is the same three commands once its driver is loaded
104
+ and the cable shows `LOWER_UP` in `ip link`; the address and gateway come
105
+ from your network (there is no DHCP client yet). If `ip link` shows no
106
+ `eth0`, load the driver first from the Bun REPL:
107
+
108
+ ```js
109
+ cfg.net // loads network modules and matches PCI/USB network devices
110
+ ```
111
+
112
+ It uses `/lib/modprobe.js`'s `autoload()`, which reads each device's
113
+ `modalias` under `/sys/bus/{pci,virtio,usb}/devices` and loads whatever
114
+ `modules.alias` says claims it. An image built with `--real` carries Intel
115
+ `e1000`/`e1000e`/`igb`/`igc`, Realtek `r8169` (+ PHY), Atheros `alx`,
116
+ Broadcom `tg3` and the USB dongles `r8152`, `ax88179_178a`, `cdc_ether`.
117
+ A single module by name: `require("/lib/modprobe.js").modprobe("e1000e")`.
118
+
119
+ ## Output format
120
+
121
+ `ip link` / `ip addr` print the iproute2 layout:
122
+
123
+ ```
124
+ 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
125
+ link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff
126
+ inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0
127
+ inet6 fe80::5054:ff:fe12:3456/64 scope link
128
+ ```
129
+
130
+ `NO-CARRIER` appears when the link is up but has no carrier (unplugged, or
131
+ the driver never attached).
132
+
133
+ ## Limits of the ioctl approach
134
+
135
+ - One IPv4 address per interface, no secondary addresses, no labels.
136
+ - IPv6: display only.
137
+ - No `link add`/`del`, no `route get`, no `neigh add`, no policy routing,
138
+ no netns.
139
+ - Route `proto`, `scope`, `src` and `table` words are accepted and ignored.
140
+
141
+ ## Exit status
142
+
143
+ `0` ok, `1` bad usage or unknown object, `2` the kernel refused
144
+ (`RTNETLINK answers: ...` style messages, plus the errno text).
145
+
146
+ ## See also
147
+
148
+ - `mount --help`, `umount --help`
149
+ - [ip(8)](https://man7.org/linux/man-pages/man8/ip.8.html),
150
+ [netdevice(7)](https://man7.org/linux/man-pages/man7/netdevice.7.html),
151
+ [route(7)](https://man7.org/linux/man-pages/man7/route.7.html)
152
+ - [/proc/net/route](https://docs.kernel.org/filesystems/proc.html#networking-info-in-proc-net),
153
+ [sysfs net class](https://docs.kernel.org/ABI/testing/sysfs-class-net.html)
154
+ - Source: `initramfs/bin/ip`, `initramfs/lib/dlopen.js` in
155
+ [buninu-linux](https://github.com/jjtseng93/buninu-linux)