import type * as Socket from "@effect/platform/Socket"; import type * as Cause from "effect/Cause"; import type * as ParseResult from "effect/ParseResult"; import type * as WireguardConfig from "../WireguardConfig.ts"; import type * as _WireguardControl from "../WireguardControl.ts"; import type * as WireguardInterface from "../WireguardInterface.ts"; import * as Command from "@effect/platform/Command"; import * as CommandExecutor from "@effect/platform/CommandExecutor"; import * as PlatformError from "@effect/platform/Error"; import * as FileSystem from "@effect/platform/FileSystem"; import * as Path from "@effect/platform/Path"; import * as Array from "effect/Array"; import * as Chunk from "effect/Chunk"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Function from "effect/Function"; import * as Layer from "effect/Layer"; import * as Predicate from "effect/Predicate"; import * as Schedule from "effect/Schedule"; import * as Stream from "effect/Stream"; import * as String from "effect/String"; import * as Tuple from "effect/Tuple"; import * as exec from "node:child_process"; /** @internal */ export const TypeId: _WireguardControl.TypeId = Symbol.for( "@leonitousconforti/the-wireguard-effect/WireguardControl" ) as _WireguardControl.TypeId; /** @internal */ export const WireguardControl = Context.GenericTag<_WireguardControl.WireguardControl>( "@leonitousconforti/the-wireguard-effect/WireguardControl" ); /** * @since 1.0.0 * @category Constructors */ export const makeUserspaceLayer = (): _WireguardControl.WireguardControl => { const up: _WireguardControl.WireguardControl["up"] = ( wireguardConfig: WireguardConfig.WireguardConfig, wireguardInterface: WireguardInterface.WireguardInterface ) => wireguardInterface.setConfig(wireguardConfig); const down: _WireguardControl.WireguardControl["down"] = ( _wireguardConfig: WireguardConfig.WireguardConfig, wireguardInterface: WireguardInterface.WireguardInterface ) => Effect.map( Effect.flatMap(FileSystem.FileSystem, (fs) => fs.remove(wireguardInterface.SocketLocation)), () => wireguardInterface ); const upScoped: _WireguardControl.WireguardControl["upScoped"] = ( wireguardConfig: WireguardConfig.WireguardConfig, wireguardInterface: WireguardInterface.WireguardInterface ) => { const _down = down(wireguardConfig, wireguardInterface).pipe(Effect.orDie); const _up = wireguardInterface.setConfig(wireguardConfig); return Effect.acquireRelease(_up, () => _down); }; return WireguardControl.of({ [TypeId]: TypeId, up, down, upScoped, }); }; /** * @since 1.0.0 * @category Constructors */ export const makeBundledWgQuickLayer = (options: { sudo: boolean }): _WireguardControl.WireguardControl => { // process.platform === "win32" && command.includes("-wireguard-go.exe") const execCommandWireguardGoWindows = ( command: string, ...args: Array ): Effect.Effect => Effect.asyncEffect< exec.ChildProcess, PlatformError.PlatformError, never, never, PlatformError.PlatformError, FileSystem.FileSystem >((resume) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const stdout = yield* fileSystem.makeTempFile(); const stderr = yield* fileSystem.makeTempFile(); const { fd: stdoutFd } = yield* fileSystem.open(stdout, { flag: "r+" }); const { fd: stderrFd } = yield* fileSystem.open(stderr, { flag: "r+" }); const subprocess = exec.spawn(command, args, { detached: true, stdio: ["ignore", stdoutFd, stderrFd], }); subprocess.unref(); subprocess.on("exit", onExit); subprocess.on("close", onClose); subprocess.on("error", onError); subprocess.on("disconnect", onDisconnect); yield* Function.pipe( fileSystem.stream(stdout), Stream.decodeText("utf-8"), Stream.splitLines, Stream.takeUntil(String.includes("UAPI listener started")), Stream.runDrain ); subprocess.off("exit", onExit); subprocess.off("close", onClose); subprocess.off("error", onError); subprocess.off("disconnect", onDisconnect); resume(Effect.succeed(subprocess)); function onError(error: Error) { subprocess.off("exit", onExit); subprocess.off("close", onClose); subprocess.off("error", onError); subprocess.off("disconnect", onDisconnect); resume( Effect.gen(function* () { const stdoutString = yield* fileSystem.readFileString(stdout, "utf-8"); const stderrString = yield* fileSystem.readFileString(stderr, "utf-8"); return yield* Effect.fail( new PlatformError.SystemError({ reason: "Unknown", module: "Command", method: "wireguard-go.exe", pathOrDescriptor: command, description: `${error.message}\n${stdoutString}\n${stderrString}`, }) ); }) ); } function onExit(code: number | null) { if (Predicate.isNotNull(code)) { return onError(new Error(`Process exited unexpectedly with code ${code}`)); } else { return onError(new Error("Process exited unexpectedly.")); } } function onClose(code: number | null) { if (Predicate.isNotNull(code)) { return onError(new Error(`Process closed unexpectedly with code ${code}`)); } else { return onError(new Error("Process closed unexpectedly.")); } } function onDisconnect() { return onError(new Error("Process disconnected unexpectedly.")); } return Effect.sync(() => { subprocess.off("exit", onExit); subprocess.off("close", onClose); subprocess.off("error", onError); subprocess.off("disconnect", onDisconnect); }); }).pipe(Effect.scoped) ).pipe(Effect.timeout("1 minutes")); const execCommand = ( command: string, ...args: Array ): Effect.Effect< void, PlatformError.PlatformError | Cause.TimeoutException, CommandExecutor.CommandExecutor | FileSystem.FileSystem > => Function.pipe( CommandExecutor.CommandExecutor, Effect.flatMap((executor) => executor.start( options.sudo && process.platform !== "win32" ? Command.make("sudo", command, ...args) : Command.make(command, ...args) ) ), Effect.flatMap((process) => Effect.all( [ process.stdout.pipe( Stream.decodeText("utf-8"), Stream.splitLines, Stream.runCollect, Effect.map(Chunk.toReadonlyArray) ), process.stderr.pipe( Stream.decodeText("utf-8"), Stream.splitLines, Stream.runCollect, Effect.map(Chunk.toReadonlyArray) ), process.exitCode, ], { concurrency: 3 } ) ), Effect.flatMap(([stdout, stderr, exitCode]) => { return exitCode === 0 ? Effect.void : Effect.fail( new PlatformError.SystemError({ reason: "Unknown", module: "Command", pathOrDescriptor: command, method: `${command} ${args.join(" ")}`, description: `Process exited with code ${exitCode}: ${stdout.join("\n")}, ${stderr.join("\n")}`, }) ); }), Effect.timeout("10 seconds"), Effect.scoped ); const internalUp: ( wireguardConfig: WireguardConfig.WireguardConfig, wireguardInterface: WireguardInterface.WireguardInterface ) => Effect.Effect< | readonly [wireguardInterface: WireguardInterface.WireguardInterface] | readonly [ wireguardInterface: WireguardInterface.WireguardInterface, wireguardGoSubprocess: exec.ChildProcess, ], Socket.SocketError | ParseResult.ParseError | PlatformError.PlatformError | Cause.TimeoutException, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor > = (wireguardConfig: WireguardConfig.WireguardConfig, wireguardInterface: WireguardInterface.WireguardInterface) => Effect.gen(function* () { const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; const tempDirectory = yield* fs.makeTempDirectory(); const file = path.join(tempDirectory, `${wireguardInterface.Name}.conf`); yield* wireguardConfig.writeToFile(file); const arch = process.arch === "x64" ? "amd64" : process.arch; const extension = process.platform === "win32" ? ".exe" : ""; const wireguardGoUrl = new URL(`../${process.platform}-${arch}-wireguard-go${extension}`, import.meta.url); const bundledWireguardGoExecutablePath = yield* path.fromFileUrl(wireguardGoUrl); yield* fs.access(bundledWireguardGoExecutablePath, { ok: true }); const wgQuickUrl = new URL(`../${process.platform}-wg-quick`, import.meta.url); const bundledWgQuickExecutablePath = yield* path.fromFileUrl(wgQuickUrl); if (process.platform !== "win32") yield* fs.access(bundledWgQuickExecutablePath, { ok: true }); const wgWindowsUrl = new URL(`../win32-${arch}-wireguard.exe`, import.meta.url); const bundledWgWindowsExecutablePath = yield* path.fromFileUrl(wgWindowsUrl); if (process.platform === "win32") yield* fs.access(bundledWgWindowsExecutablePath, { ok: true }); const wgQuickCommandNix = [bundledWgQuickExecutablePath, "up", file] as const; const wgQuickCommandWin = [bundledWgWindowsExecutablePath, "/installtunnelservice", file] as const; const wgQuickCommand = process.platform === "win32" ? wgQuickCommandWin : wgQuickCommandNix; if (process.platform === "win32") { const retrySchedule = Schedule.compose(Schedule.recurs(5), Schedule.exponential(2_000)); const runningWireguardGoProcess = yield* execCommandWireguardGoWindows( bundledWireguardGoExecutablePath, wireguardInterface.Name ); yield* Effect.retry(wireguardInterface.setConfig(wireguardConfig), retrySchedule); yield* execCommand(wgQuickCommand[0], ...wgQuickCommand.slice(1)); return Tuple.make(wireguardInterface, runningWireguardGoProcess); } else { yield* execCommand(bundledWireguardGoExecutablePath, wireguardInterface.Name); yield* wireguardInterface.setConfig(wireguardConfig); yield* execCommand(wgQuickCommand[0], ...wgQuickCommand.slice(1)); return Tuple.make(wireguardInterface); } }); const down: _WireguardControl.WireguardControl["down"] = ( wireguardConfig, wireguardInterface, wireguardGoSubprocess ) => Effect.gen(function* () { const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; if (wireguardGoSubprocess) wireguardGoSubprocess.kill(); const tempDirectory = yield* fs.makeTempDirectory(); const file = path.join(tempDirectory, `${wireguardInterface.Name}.conf`); yield* wireguardConfig.writeToFile(file); const arch = process.arch === "x64" ? "amd64" : process.arch; const wgQuickUrl = new URL(`../${process.platform}-wg-quick`, import.meta.url); const bundledWgQuickExecutablePath = yield* path.fromFileUrl(wgQuickUrl); if (process.platform !== "win32") yield* fs.access(bundledWgQuickExecutablePath, { ok: true }); const wgWindowsUrl = new URL(`../win32-${arch}-wireguard.exe`, import.meta.url); const bundledWgWindowsExecutablePath = yield* path.fromFileUrl(wgWindowsUrl); if (process.platform === "win32") yield* fs.access(bundledWgWindowsExecutablePath, { ok: true }); const wgQuickCommandWin = [ bundledWgWindowsExecutablePath, "/uninstalltunnelservice", wireguardInterface.Name, ] as const; const wgQuickCommandNix = [bundledWgQuickExecutablePath, "down", file] as const; const wgQuickCommand = process.platform === "win32" ? wgQuickCommandWin : wgQuickCommandNix; yield* execCommand(wgQuickCommand[0], ...wgQuickCommand.slice(1)); return wireguardInterface; }); const upScoped: _WireguardControl.WireguardControl["upScoped"] = (wireguardConfig, wireguardInterface) => { const _down = (wireguardGoSubprocess?: exec.ChildProcess | undefined) => down(wireguardConfig, wireguardInterface, wireguardGoSubprocess).pipe(Effect.orDie); const _up = internalUp(wireguardConfig, wireguardInterface); return Effect.map( Effect.acquireRelease(_up, (data) => { if (Array.isArray(data) && Tuple.isTupleOf(data, 2)) { const wireguardGoSubprocess = Tuple.getSecond(data); return _down(wireguardGoSubprocess); } else { return _down(); } }), (data) => data[0] ); }; const up: _WireguardControl.WireguardControl["up"] = (wireguardConfig, wireguardInterface) => Effect.map(internalUp(wireguardConfig, wireguardInterface), (data) => data[0]); return WireguardControl.of({ [TypeId]: TypeId, up, down, upScoped, }); }; /** * @since 1.0.0 * @category Layers */ export const UserspaceLayer = Layer.sync(WireguardControl, makeUserspaceLayer); /** * @since 1.0.0 * @category Layers */ export const BundledWgQuickLayer = Layer.sync(WireguardControl, () => makeBundledWgQuickLayer({ sudo: true }));