This commit is contained in:
Cat /dev/Nulo 2023-07-20 15:23:52 -03:00
parent e349bc7cad
commit bc33fd38cb
5 changed files with 127 additions and 21 deletions

3
server/.gitignore vendored
View file

@ -3,3 +3,6 @@ container-baby.js
# cache, will move later # cache, will move later
cache/ cache/
# jailer
jailer/

View file

@ -1,11 +1,13 @@
import { delay } from "nanodelay"; import { delay } from "nanodelay";
import { nanoid } from "nanoid"; import { customAlphabet as nanoidCustomAlphabet, nanoid } from "nanoid";
import which from "which";
import { spawn, execFile as _execFile } from "node:child_process"; import { spawn, execFile as _execFile } from "node:child_process";
import { createServer, request as _request } from "node:http"; import { createServer, request as _request } from "node:http";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { basename, join } from "node:path";
import { downloadImage, parseImageRef } from "./container-baby.js"; import { downloadImage, parseImageRef } from "./container-baby.js";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { access, chmod, chown, link, mkdir } from "node:fs/promises";
const execFile = promisify(_execFile); const execFile = promisify(_execFile);
const server = createServer(listener); const server = createServer(listener);
@ -55,52 +57,115 @@ async function runVm(image) {
* @prop {boolean} is_read_only * @prop {boolean} is_read_only
*/ */
/**
* gets absolute path to firecracker executable
* @returns {Promise<string>}
*/
async function getFirecrackerExe() {
const firecrackerExe = await which("firecracker", { nothrow: true });
console.debug(
`[fireactions] Using firecracker executable '${firecrackerExe}'`
);
return firecrackerExe;
}
const jailerDir = "./jailer";
const [jailerUid, jailerGid] = [65534, 65534];
// we need to use a custom alphabet because jailer is picky
const jailerNanoid = nanoidCustomAlphabet(
"0123456789abcdefghijklmnopqrstuvwxyz-",
64
);
class FirecrackerInstance { class FirecrackerInstance {
proc; vmid;
socketPath; socketPath;
constructor() { proc;
const vmid = nanoid(); /**
this.socketPath = join(tmpdir(), `firecracker-${vmid}.sock`); * @param {{ firecrackerExe: string }} param0
* @private
*/
constructor({ firecrackerExe }) {
this.vmid = jailerNanoid();
this.socketPath = join(this.chrootPath, `firecracker.sock`);
console.debug(this.socketPath); console.debug(this.socketPath);
// TODO: jailer
this.proc = spawn( this.proc = spawn(
"firecracker", "jailer",
[ [
...["--api-sock", this.socketPath], ...["--id", this.vmid],
...["--level", "Debug"], ...["--exec-file", firecrackerExe],
...["--log-path", "/dev/stderr"], ...["--uid", "" + jailerUid],
"--show-level", ...["--gid", "" + jailerGid],
"--show-log-origin", ...["--chroot-base-dir", jailerDir],
"--",
...["--api-sock", "/firecracker.sock"],
// ...["--level", "Debug"],
// ...["--log-path", "/stderr.log"],
// "--show-level",
// "--show-log-origin",
], ],
{ {
stdio: "inherit", stdio: "inherit",
} }
); );
console.debug(
`[fireactions] Started firecracker with jailer at ${this.chrootPath}`
);
} }
get chrootPath() {
return join(jailerDir, "firecracker", this.vmid, "root");
}
/**
* @param {string} path path to file to hardlink to root of chroot
* @param {number} mod mod inside the chroot
*/
async hardlinkToChroot(path, mod = 0o600) {
const chrootPath = join(this.chrootPath, basename(path));
let accesed = false;
try {
await access(chrootPath);
accesed = true;
} catch {}
if (accesed)
throw new Error("file with same name already exists inside chroot");
await link(path, chrootPath);
await chown(chrootPath, jailerUid, jailerGid);
await chmod(chrootPath, mod);
}
/** /**
* @param [opts] {{ drives?: Drive[] }} * @param [opts] {{ drives?: Drive[] }}
*/ */
static async run(opts) { static async run(opts) {
const self = new FirecrackerInstance(); await mkdir(jailerDir, { recursive: true });
const self = new FirecrackerInstance({
firecrackerExe: await getFirecrackerExe(),
});
await mkdir(self.chrootPath, { recursive: true });
// TODO: retry until success // TODO: retry until success
await delay(15); await delay(15);
const { ifname, hostAddr, guestAddr } = await createNetworkInterface(); const { ifname, hostAddr, guestAddr } = await createNetworkInterface();
await self.hardlinkToChroot("../vmlinux.bin", 0o400);
await self.request("PUT", "/boot-source", { await self.request("PUT", "/boot-source", {
kernel_image_path: "../vmlinux.bin", kernel_image_path: "/vmlinux.bin",
boot_args: `console=ttyS0 reboot=k panic=1 pci=off fireactions_ip=${guestAddr}:${hostAddr}`, boot_args: `console=ttyS0 reboot=k panic=1 pci=off fireactions_ip=${guestAddr}:${hostAddr}`,
}); });
await self.request("PUT", "/drives/rootfs", { // TODO: readonly
self.attachDrive({
drive_id: "rootfs", drive_id: "rootfs",
path_on_host: "../rootfs.ext4", path_on_host: "/rootfs.ext4",
is_root_device: true, is_root_device: true,
// TODO: readonly
is_read_only: false, is_read_only: false,
}); });
if (opts?.drives) { if (opts?.drives) {
for (const drive of opts.drives) { for (const drive of opts.drives) {
await self.request("PUT", "/drives/" + drive.drive_id, drive); await self.attachDrive(drive);
} }
} }
await self.request("PUT", "/network-interfaces/eth0", { await self.request("PUT", "/network-interfaces/eth0", {
@ -119,6 +184,18 @@ class FirecrackerInstance {
}); });
} }
/**
* @param {Drive} drive
*/
async attachDrive(drive) {
const perms = drive.is_read_only ? 0o400 : 0o600;
await this.hardlinkToChroot(drive.path_on_host, perms);
await this.request("PUT", "/drives/" + drive.drive_id, {
...drive,
path_on_host: "/" + basename(drive.path_on_host),
});
}
/** /**
* @param {string} method * @param {string} method
* @param {string} path * @param {string} path

View file

@ -5,7 +5,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "esbuild --bundle index.js --platform=node --sourcemap > build.cjs && sudo node --enable-source-maps build.cjs" "start": "esbuild --bundle index.js --platform=node --sourcemap > build.cjs && sudo env PATH=\"$PATH:$HOME/.local/bin\" node --enable-source-maps build.cjs"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -15,12 +15,14 @@
"@types/gunzip-maybe": "^1.4.0", "@types/gunzip-maybe": "^1.4.0",
"@types/node": "^20.2.5", "@types/node": "^20.2.5",
"@types/tar-stream": "^2.2.2", "@types/tar-stream": "^2.2.2",
"@types/which": "^3.0.0",
"undici": "^5.22.1" "undici": "^5.22.1"
}, },
"dependencies": { "dependencies": {
"gunzip-maybe": "^1.4.2", "gunzip-maybe": "^1.4.2",
"nanodelay": "^2.0.2", "nanodelay": "^2.0.2",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"tar-stream": "^3.0.0" "tar-stream": "^3.0.0",
"which": "^3.0.1"
} }
} }

View file

@ -17,6 +17,9 @@ dependencies:
tar-stream: tar-stream:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
which:
specifier: ^3.0.1
version: 3.0.1
devDependencies: devDependencies:
'@tsconfig/node16': '@tsconfig/node16':
@ -31,6 +34,9 @@ devDependencies:
'@types/tar-stream': '@types/tar-stream':
specifier: ^2.2.2 specifier: ^2.2.2
version: 2.2.2 version: 2.2.2
'@types/which':
specifier: ^3.0.0
version: 3.0.0
undici: undici:
specifier: ^5.22.1 specifier: ^5.22.1
version: 5.22.1 version: 5.22.1
@ -57,6 +63,10 @@ packages:
'@types/node': 20.2.5 '@types/node': 20.2.5
dev: true dev: true
/@types/which@3.0.0:
resolution: {integrity: sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ==}
dev: true
/abort-controller@3.0.0: /abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'} engines: {node: '>=6.5'}
@ -170,6 +180,10 @@ packages:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: false dev: false
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: false
/nanodelay@2.0.2: /nanodelay@2.0.2:
resolution: {integrity: sha512-6AS5aCSXsjoxq2Jr9CdaAeT60yoYDOTp6po9ziqeOeY6vf6uTEHYSqWql6EFILrM3fEfXgkZ4KqE9L0rTm/wlA==} resolution: {integrity: sha512-6AS5aCSXsjoxq2Jr9CdaAeT60yoYDOTp6po9ziqeOeY6vf6uTEHYSqWql6EFILrM3fEfXgkZ4KqE9L0rTm/wlA==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@ -301,6 +315,14 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false dev: false
/which@3.0.1:
resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
hasBin: true
dependencies:
isexe: 2.0.0
dev: false
/wrappy@1.0.2: /wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: false dev: false

View file

@ -2,6 +2,8 @@
// TODO: sandboxear mkquashfs // TODO: sandboxear mkquashfs
// TODO: fijarse como hace firecracker-containerd // TODO: fijarse como hace firecracker-containerd
// TODO: fijarse como hace [ignite](https://github.com/weaveworks/ignite) // TODO: fijarse como hace [ignite](https://github.com/weaveworks/ignite)
// TODO: fijarse como hace [thi](https://github.com/thi-startup/init)
// TODO: fijarse como hace [firebuild](https://combust-labs.github.io/firebuild-docs/)
import gunzip from "gunzip-maybe"; import gunzip from "gunzip-maybe";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";