diff --git a/instance/control.ts b/instance/control.ts index 0942da3..0bdd825 100755 --- a/instance/control.ts +++ b/instance/control.ts @@ -1,7 +1,7 @@ #!/bin/env -S deno run -A --unstable import { daemon_send, new_cmd_disable, new_cmd_enable, new_cmd_status, new_cmd_stop } from "./src/lib.ts"; -import { load_all_container_configs, new_container_config, save_container_config } from "./src/lib/config.ts"; +import { load_all_configs, new_container_config, save_container_config } from "./src/lib/config.ts"; import { load_base, new_container_context } from "./src/lib/create.ts"; import { container_paths, socket_path } from "./src/lib/paths.ts"; import { log_from, run } from "./src/lib/utils.ts"; @@ -78,7 +78,7 @@ Commands: export async function create(name: string, base_name: string) { log("loading base", base_name); const base = await load_base(base_name); - const known_containers = await load_all_container_configs(); + const known_containers = await load_all_configs(); const paths = container_paths(name); await Deno.mkdir(paths.base); diff --git a/instance/local/.gitignore b/instance/local/.gitignore index 5375a10..33f9f5e 100644 --- a/instance/local/.gitignore +++ b/instance/local/.gitignore @@ -1,3 +1,2 @@ /containers/* -/config.json /state.json diff --git a/instance/src/bin/proxy.ts b/instance/src/bin/proxy.ts new file mode 100755 index 0000000..16611d1 --- /dev/null +++ b/instance/src/bin/proxy.ts @@ -0,0 +1,32 @@ +#!/bin/env -S deno run -A --unstable + +if (import.meta.main) await main(); +async function main() { + const [from_port, to_ip, to_port] = Deno.args; + if (to_port === undefined) { + console.error("[proxy] usage: ./proxy.ts "); + Deno.exit(2); + } + + const server = Deno.listen({ transport: "tcp", port: parseInt(from_port) }); + console.log("[proxy] listening on port", from_port, "redirecting to", to_ip, "port", to_port); + + for await (const connection of server) { + serve(to_ip, to_port, connection); + } +} + +async function serve(to_ip: string, to_port: string, connection: Deno.Conn) { + try { + const client = await Deno.connect({ transport: "tcp", hostname: to_ip, port: parseInt(to_port) }); + const promise_connection_done = connection.readable.pipeTo(client.writable); + const promise_client_done = client.readable.pipeTo(connection.writable); + await Promise.all([promise_client_done, promise_connection_done]); + } catch (_) { /* isok */ } +} + +export function proxy_command(from_port: number, to_ip: string, to_port: number) { + const path = new URL("", import.meta.url).pathname; + const args = [from_port, to_ip, to_port].map((v) => v.toString()); + return new Deno.Command(path, { args }); +} diff --git a/instance/src/lib.ts b/instance/src/lib.ts index c63c2e3..6e1e18f 100644 --- a/instance/src/lib.ts +++ b/instance/src/lib.ts @@ -1,10 +1,11 @@ export type { Base, BaseContext } from "./lib/create.ts"; import { toText } from "https://deno.land/std@0.208.0/streams/to_text.ts"; -import { lines, log_from, loop_process, run } from "./lib/utils.ts"; +import { lines, log_from, loop_process, LoopProcess, run, sleep } from "./lib/utils.ts"; import { ContainerConfig } from "./lib/config.ts"; import { container_paths } from "./lib/paths.ts"; -import { container_command } from "./lib/nspawn.ts"; +import { container_command, get_container_addresses } from "./lib/nspawn.ts"; +import { proxy_command } from "./bin/proxy.ts"; const log = log_from("lib"); @@ -73,21 +74,45 @@ export function start_runner(config: ContainerConfig) { const command = container_command(name, paths.root, { boot: true, veth: true, - ports: config.redirects, cmd_opts: { stdin: "null", stdout: "null", }, - syscall_filter: ["add_key", "keyctl", "bpf"], }); + const proxies = [] as LoopProcess[]; const container_loop = loop_process(command, { - on_start: () => log("container", name, "started"), - on_stop: () => log("container", name, "stopped"), + on_start: () => { + log("container", name, "started"); + (async () => { + await sleep(1_000); + const [address] = await get_container_addresses(name); + proxies.push(...start_proxies(address, config.redirects)); + })(); + }, + on_stop: async () => { + log("container", name, "stopped"); + for (const p of proxies) await p.kill(); + await sleep(500); + proxies.splice(0, proxies.length); + }, }); return { name, config, container_loop, - stop: () => container_loop.kill(), + stop: async () => { + for (const p of proxies) await p.kill(); + await container_loop.kill(); + }, }; } + +function start_proxies(address: string, redirects: [number, number][]) { + const redirections = redirects + .map(([from_port, to_port]) => { + return loop_process(proxy_command(from_port, address, to_port), { + on_start: () => console.log("starting proxy", from_port, "to", address, "port", to_port), + }); + }); + return redirections; +} diff --git a/instance/src/lib/config.ts b/instance/src/lib/config.ts index 166b584..4d411ff 100644 --- a/instance/src/lib/config.ts +++ b/instance/src/lib/config.ts @@ -1,10 +1,10 @@ -import { container_paths, containers_path, state_config_path } from "./paths.ts"; +import { container_paths, containers_path } from "./paths.ts"; import { exists, log_from } from "./utils.ts"; -import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"; const log = log_from("config"); -export function new_container_config(name: string): ContainerConfig { +export type ContainerConfig = ReturnType; +export function new_container_config(name: string) { return { name, version: 0, @@ -12,32 +12,12 @@ export function new_container_config(name: string): ContainerConfig { }; } -export type ContainerConfig = ReturnType; -export function parse_container_config(json: string) { - return z.object({ - name: z.string(), - version: z.number(), - redirects: z.array( - z - .tuple([ - z.number(), - z.number(), - ]) - .or(z.tuple([ - z.number(), - z.number(), - z.literal("tcp").or(z.literal("udp")), - ])), - ), - }).parse(JSON.parse(json)); -} - export async function load_container_config(name: string) { const config_path = container_paths(name).configuration; log("loading config for", name); if (!exists(config_path)) return null; const content = await Deno.readTextFile(config_path); - const read = parse_container_config(content); + const read = JSON.parse(content); const default_ = new_container_config(name); if (read.version < default_.version) throw new Error("read conf version is outdated"); return { ...default_, ...read } as ContainerConfig; @@ -50,7 +30,7 @@ export async function save_container_config(config: ContainerConfig) { await Deno.writeTextFile(config_path, serialized); } -export async function load_all_container_configs() { +export async function load_all_configs() { const names = Array.from(Deno.readDirSync(containers_path())) .filter((e) => e.isDirectory) .map((e) => e.name); diff --git a/instance/src/lib/nspawn.ts b/instance/src/lib/nspawn.ts index 491f486..d5fc189 100644 --- a/instance/src/lib/nspawn.ts +++ b/instance/src/lib/nspawn.ts @@ -7,9 +7,7 @@ const log = log_from("nspawn"); export function container_command(name: string, directory: string, opts?: { veth?: boolean; boot?: boolean; - ports?: ([number, number] | [number, number, "tcp" | "udp"])[]; cmd_opts?: Deno.CommandOptions; - syscall_filter?: string[]; }) { const args = [ `--machine=${name}`, @@ -17,10 +15,6 @@ export function container_command(name: string, directory: string, opts?: { ]; if (opts?.veth ?? false) args.push("--network-veth"); if (opts?.boot ?? false) args.push("--boot"); - for (const [from, to, proto] of opts?.ports ?? []) { - args.push(proto === undefined ? `--port=${from}:${to}` : `--port=${proto}:${from}:${to}`); - } - for (const call of opts?.syscall_filter ?? []) args.push(`--system-call-filter=${call}`); const command = new Deno.Command("systemd-nspawn", { ...opts?.cmd_opts, args }); return command; }