UNPKG

the-wireguard-effect

Version:

Cross platform wireguard api client for nodejs built on wireguard-go with effect-ts

223 lines 10.6 kB
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 = /*#__PURE__*/Symbol.for("@leonitousconforti/the-wireguard-effect/WireguardControl"); /** @internal */ export const WireguardControl = /*#__PURE__*/Context.GenericTag("@leonitousconforti/the-wireguard-effect/WireguardControl"); /** * @since 1.0.0 * @category Constructors */ export const makeUserspaceLayer = () => { const up = (wireguardConfig, wireguardInterface) => wireguardInterface.setConfig(wireguardConfig); const down = (_wireguardConfig, wireguardInterface) => Effect.map(Effect.flatMap(FileSystem.FileSystem, fs => fs.remove(wireguardInterface.SocketLocation)), () => wireguardInterface); const upScoped = (wireguardConfig, 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 => { // process.platform === "win32" && command.includes("-wireguard-go.exe") const execCommandWireguardGoWindows = (command, ...args) => Effect.asyncEffect(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) { 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(PlatformError.SystemError({ reason: "Unknown", module: "Command", method: "wireguard-go.exe", pathOrDescriptor: command, message: `${error.message}\n${stdoutString}\n${stderrString}` })); })); } function onExit(code) { 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) { 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, ...args) => 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(PlatformError.SystemError({ reason: "Unknown", module: "Command", pathOrDescriptor: command, method: `${command} ${args.join(" ")}`, message: `Process exited with code ${exitCode}: ${stdout.join("\n")}, ${stderr.join("\n")}` })); }), Effect.timeout("10 seconds"), Effect.scoped); const internalUp = (wireguardConfig, 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]; const wgQuickCommandWin = [bundledWgWindowsExecutablePath, "/installtunnelservice", file]; 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 = (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]; const wgQuickCommandNix = [bundledWgQuickExecutablePath, "down", file]; const wgQuickCommand = process.platform === "win32" ? wgQuickCommandWin : wgQuickCommandNix; yield* execCommand(wgQuickCommand[0], ...wgQuickCommand.slice(1)); return wireguardInterface; }); const upScoped = (wireguardConfig, wireguardInterface) => { const _down = wireguardGoSubprocess => 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 = (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 = /*#__PURE__*/Layer.sync(WireguardControl, makeUserspaceLayer); /** * @since 1.0.0 * @category Layers */ export const BundledWgQuickLayer = /*#__PURE__*/Layer.sync(WireguardControl, () => makeBundledWgQuickLayer({ sudo: true })); //# sourceMappingURL=wireguardControl.js.map