UNPKG

the-wireguard-effect

Version:

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

368 lines (367 loc) 18.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.upScoped = exports.up = exports.streamPeerStats = exports.setConfig = exports.removePeer = exports.getConfig = exports.down = exports.addPeer = exports.WireguardInterface = exports.WireguardIniConfig = exports.WireguardConfig = void 0; var FileSystem = _interopRequireWildcard(require("@effect/platform/FileSystem")); var Path = _interopRequireWildcard(require("@effect/platform/Path")); var Array = _interopRequireWildcard(require("effect/Array")); var Chunk = _interopRequireWildcard(require("effect/Chunk")); var Effect = _interopRequireWildcard(require("effect/Effect")); var Either = _interopRequireWildcard(require("effect/Either")); var Function = _interopRequireWildcard(require("effect/Function")); var Match = _interopRequireWildcard(require("effect/Match")); var Number = _interopRequireWildcard(require("effect/Number")); var Option = _interopRequireWildcard(require("effect/Option")); var ParseResult = _interopRequireWildcard(require("effect/ParseResult")); var Predicate = _interopRequireWildcard(require("effect/Predicate")); var Schedule = _interopRequireWildcard(require("effect/Schedule")); var Schema = _interopRequireWildcard(require("effect/Schema")); var Stream = _interopRequireWildcard(require("effect/Stream")); var String = _interopRequireWildcard(require("effect/String")); var ini = _interopRequireWildcard(require("ini")); var assert = _interopRequireWildcard(require("node:assert")); var os = _interopRequireWildcard(require("node:os")); var InternetSchemas = _interopRequireWildcard(require("../InternetSchemas.js")); var WireguardControl = _interopRequireWildcard(require("../WireguardControl.js")); var WireguardErrors = _interopRequireWildcard(require("../WireguardErrors.js")); var WireguardKey = _interopRequireWildcard(require("../WireguardKey.js")); var WireguardPeer = _interopRequireWildcard(require("../WireguardPeer.js")); var internalWireguardConfig = _interopRequireWildcard(require("./wireguardConfig.js")); var internalInterface = _interopRequireWildcard(require("./wireguardInterface.js")); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } // -------------------------------------------- // WireguardConfig.ts // -------------------------------------------- class WireguardConfig extends /*#__PURE__*/internalWireguardConfig.WireguardConfigVariantSchema.Class("WireguardIniConfig")({ /** The Address of this peer. */ Address: InternetSchemas.CidrBlockFromString, /** DNS for this peer. */ Dns: /*#__PURE__*/Schema.optional(InternetSchemas.Address), /** * The value for this is a decimal-string integer corresponding to the * listening port of the interface. */ ListenPort: /*#__PURE__*/Schema.Union(InternetSchemas.Port, /*#__PURE__*/Schema.transformOrFail(Schema.String, InternetSchemas.Port, { decode: (str, _options, ast) => Either.fromOption(Number.parse(str), () => new ParseResult.Type(ast, str)), encode: port => Effect.succeed(`${port}`) })), /** * The value for this is a decimal-string integer corresponding to the * fwmark of the interface. The value may 0 in the case of a set operation, * in which case it indicates that the fwmark should be removed. */ FirewallMark: /*#__PURE__*/Schema.optionalWith(Schema.Number, { nullable: true }), /** * The value for this key should be a lowercase hex-encoded private key of * the interface. The value may be an all zero string in the case of a set * operation, in which case it indicates that the private key should be * removed. */ PrivateKey: WireguardKey.WireguardKey, /** List of peers to add. */ Peers: /*#__PURE__*/internalWireguardConfig.WireguardConfigVariantSchema.Field({ json: /*#__PURE__*/Schema.optionalWith(/*#__PURE__*/Schema.Array(WireguardPeer.WireguardPeer), { default: () => [], nullable: true }), uapi: /*#__PURE__*/Schema.optionalWith(/*#__PURE__*/Schema.Array(WireguardPeer.WireguardPeer["uapi"]), { default: () => [], nullable: true }) }) }) { /** * Writes a wireguard interface configuration to an INI file. * * @since 1.0.0 * @category Constructors * @param file - The path to the INI file. */ writeToFile = file => Effect.gen(this, function* () { const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; const configEncoded = yield* Schema.encode(WireguardConfig)(this); const iniConfigDecoded = yield* Schema.decode(WireguardIniConfig)(configEncoded); yield* fs.makeDirectory(path.dirname(file), { recursive: true }); yield* fs.writeFileString(file, iniConfigDecoded); }); /** * Starts a wireguard tunnel that will continue to run and serve traffic * even after the nodejs process exits. * * @since 1.0.0 * @category Wireguard */ up = wireguardInterface => Function.pipe(wireguardInterface, Option.fromNullable, Option.map(Effect.succeed), Option.getOrElse(() => WireguardInterface.getNextAvailableInterface), Effect.flatMap(io => up(io, this))); /** * Starts a wireguard tunnel that will be gracefully shutdown and stop * serving traffic once the scope is closed. * * @since 1.0.0 * @category Wireguard */ upScoped = wireguardInterface => Function.pipe(wireguardInterface, Option.fromNullable, Option.map(Effect.succeed), Option.getOrElse(() => WireguardInterface.getNextAvailableInterface), Effect.flatMap(io => upScoped(io, this))); } exports.WireguardConfig = WireguardConfig; class WireguardIniConfig extends /*#__PURE__*/Schema.transformOrFail(WireguardConfig, Schema.String, { // Encoding is non-trivial, as we need to handle all the peers individually. decode: (config, _options, _ast) => Effect.gen(function* () { const listenPort = `ListenPort = ${config.ListenPort}\n`; const privateKey = `PrivateKey = ${config.PrivateKey}\n`; const address = `Address = ${config.Address.address.ip}/${config.Address.mask}\n`; const dns = Predicate.isNotUndefined(config.Dns) ? `Dns = ${config.Dns?.ip}\n` : ""; const fwmark = Predicate.isNotUndefined(config.FirewallMark) ? `FirewallMark = ${config.FirewallMark}\n` : ""; const peersConfig = yield* Function.pipe(config.Peers, Array.map(peer => Schema.encode(WireguardPeer.WireguardPeer)(peer)), Array.map(Effect.flatMap(Schema.decode(WireguardPeer.WireguardIniPeer))), Effect.allWith(), Effect.map(Array.join("\n"))); return `[Interface]\n${dns}${listenPort}${fwmark}${address}${privateKey}\n${peersConfig}`; }).pipe(Effect.mapError(({ issue }) => issue)), // Decoding is likewise non-trivial, as we need to parse all the peers from the ini config. encode: (iniConfig, _options, _ast) => Effect.gen(function* () { const sections = iniConfig.split(/(?=\[Peer\])/g); const maybeInterfaceSection = Array.findFirst(sections, text => text.startsWith("[Interface]")); const interfaceSection = Option.getOrThrowWith(maybeInterfaceSection, () => new WireguardErrors.WireguardError({ message: "No [Interface] section found" })); const peerSections = Function.pipe(sections, Array.filter(text => text.startsWith("[Peer]")), Array.map(text => text.replace("[Peer]", ""))); const parsePeers = yield* Function.pipe(peerSections, Array.map(peer => Schema.encode(WireguardPeer.WireguardIniPeer)(peer)), Effect.allWith()); const parseInterface = Function.pipe(interfaceSection, ini.parse, jsonConfig => ({ ...jsonConfig["Interface"], Peers: parsePeers }), ({ Address, Dns, FirewallMark, ListenPort, Peers, PrivateKey }) => ({ Dns, Peers, Address, PrivateKey, ListenPort, FirewallMark: Number.parse(FirewallMark || "").pipe(Option.getOrUndefined) }), Schema.decode(WireguardConfig)); return yield* parseInterface; }).pipe(Effect.mapError(({ issue }) => issue)) }).annotations({ identifier: "WireguardIniConfig", description: "A wireguard ini configuration" }) {} // -------------------------------------------- // WireguardInterface.ts // -------------------------------------------- /** * A wireguard interface name. * * @since 1.0.0 * @category Datatypes */ exports.WireguardIniConfig = WireguardIniConfig; class WireguardInterface extends /*#__PURE__*/Schema.Class("WireguardInterface")({ /** * Ensures the interface name matches the platform's interface name regex. * These functions need to be fully typed as we are accessing a static * method on this same class and otherwise typescript really complains about * inference. */ Name: /*#__PURE__*/Schema.transformOrFail(Schema.String, Schema.String, { decode: (str, _options, ast) => Function.pipe(WireguardInterface.InterfaceRegExpForPlatform, Effect.mapError(error => new ParseResult.Type(ast, str, error.message)), Effect.flatMap(x => x.test(str) ? Effect.succeed(str) : Effect.fail(new ParseResult.Type(ast, str, `Expected interface name to match ${x}`)))), encode: s => Effect.succeed(s) }) }) { /** * @since 1.0.0 * @category Constructors */ static getNextAvailableInterface = /*#__PURE__*/Effect.gen(function* () { // Determine all the used interface indexes const regex = yield* WireguardInterface.InterfaceRegExpForPlatform; const usedInterfaceIndexes = Function.pipe(os.networkInterfaces(), Object.keys, Array.filter(name => regex.test(name)), Array.map(String.replaceAll(/\D/g, "")), Array.map(Number.parse), Array.filterMap(Function.identity)); // Find the next available interface index const nextAvailableInterfaceIndex = yield* Function.pipe(Stream.iterate(0, x => x + 1), Stream.find(x => !Array.contains(usedInterfaceIndexes, x)), Stream.take(1), Stream.runCollect, Effect.map(Chunk.head), Effect.map(Option.getOrThrow)); // We know this will be a supported platform now because otherwise // the WireguardInterface.InterfaceRegExpForPlatform would have failed const platform = Function.unsafeCoerce(process.platform); // Construct the next available interface name const fromString = Schema.decodeSync(WireguardInterface); switch (platform) { case "win32": return fromString({ Name: `eth${nextAvailableInterfaceIndex}` }); case "linux": return fromString({ Name: `wg${nextAvailableInterfaceIndex}` }); case "darwin": return fromString({ Name: `utun${nextAvailableInterfaceIndex}` }); default: return Function.absurd(platform); } }); static InterfaceRegExpForPlatform = /*#__PURE__*/Function.pipe(/*#__PURE__*/Match.value(`${process.arch}:${process.platform}`), /*#__PURE__*/Match.not(/*#__PURE__*/Predicate.some(/*#__PURE__*/Array.map(internalInterface.SupportedArchitectures, arch => String.startsWith(`${arch}:`))), bad => Effect.fail(new WireguardErrors.WireguardError({ message: `Unsupported architecture ${bad}` }))), /*#__PURE__*/Match.when(/*#__PURE__*/String.endsWith(":linux"), () => Effect.succeed(internalInterface.LinuxInterfaceNameRegExp)), /*#__PURE__*/Match.when(/*#__PURE__*/String.endsWith(":win32"), () => Effect.succeed(internalInterface.WindowsInterfaceNameRegExp)), /*#__PURE__*/Match.when(/*#__PURE__*/String.endsWith(":darwin"), () => Effect.succeed(internalInterface.DarwinInterfaceNameRegExp)), /*#__PURE__*/Match.orElse(bad => Effect.fail(new WireguardErrors.WireguardError({ message: `Unsupported platform ${bad}` })))); /** * @since 1.0.0 * @category Userspace api */ SocketLocation = /*#__PURE__*/Function.pipe(Match.type(), Match.when("linux", () => `/var/run/wireguard/${this.Name}.sock`), Match.when("darwin", () => `/var/run/wireguard/${this.Name}.sock`), Match.when("win32", () => `\\\\.\\pipe\\ProtectedPrefix\\Administrators\\WireGuard\\${this.Name}`), Match.exhaustive)(/*#__PURE__*/Function.unsafeCoerce(process.platform)); /** * Starts a wireguard tunnel that will be gracefully shutdown and stop * serving traffic once the scope is closed. * * @since 1.0.0 * @category Wireguard control */ upScoped = config => upScoped(this, config); /** * Starts a wireguard tunnel that will continue to run and serve traffic * even after the nodejs process exits. * * @since 1.0.0 * @category Wireguard control */ up = config => up(this, config); /** * Stops a previously started wireguard tunnel. * * @since 1.0.0 * @category Wireguard control */ down = config => down(this, config); /** * Sets the config for this wireguard interface. * * @since 1.0.0 * @category Wireguard control */ setConfig = wireguardConfig => setConfig(this, wireguardConfig); /** * Retrieves the config from this wireguard interface. * * @since 1.0.0 */ getConfig = address => getConfig(this, address); /** * Adds a peer to this interface. * * @since 1.0.0 * @category Wireguard control */ addPeer = peer => addPeer(this, peer); /** * Removes a peer from this interface. * * @since 1.0.0 * @category Wireguard control */ removePeer = peer => removePeer(this, peer); /** * Streams the stats from all the peers on this interface. * * @since 1.0.0 * @category Wireguard control */ streamPeerStats = () => streamPeerStats(this); } // -------------------------------------------- // WireguardRpc.ts // -------------------------------------------- /** @internal */ exports.WireguardInterface = WireguardInterface; const up = (wireguardInterface, wireguardConfig) => Effect.flatMap(WireguardControl.WireguardControl, control => control.up(wireguardConfig, wireguardInterface)); /** @internal */ exports.up = up; const upScoped = (wireguardInterface, wireguardConfig) => Effect.flatMap(WireguardControl.WireguardControl, control => control.upScoped(wireguardConfig, wireguardInterface)); /** @internal */ exports.upScoped = upScoped; const down = (wireguardInterface, wireguardConfig) => Effect.flatMap(WireguardControl.WireguardControl, control => control.down(wireguardConfig, wireguardInterface)); /** @internal */ exports.down = down; const setConfig = (wireguardInterface, wireguardConfig) => Effect.gen(function* () { const listenPort = `listen_port=${wireguardConfig.ListenPort}\n`; const privateKeyHex = Buffer.from(wireguardConfig.PrivateKey, "base64").toString("hex"); const privateKey = `private_key=${privateKeyHex}\n`; const fwmark = Predicate.isNotUndefined(wireguardConfig.FirewallMark) ? `fwmark=${wireguardConfig.FirewallMark}\n` : String.empty; const peers = yield* Function.pipe(wireguardConfig.Peers, Array.map(peer => Schema.encode(WireguardPeer.WireguardPeer)(peer)), Array.map(Effect.flatMap(peer => Schema.decode(WireguardPeer.WireguardUapiSetPeer)(peer))), Effect.allWith(), Effect.map(Array.join("\n"))); const uapiConfig = `${privateKey}${listenPort}${fwmark}${peers}\n`; yield* internalInterface.userspaceContact(wireguardInterface, `set=1\n${uapiConfig}\n`); return wireguardInterface; }); /** @internal */ exports.setConfig = setConfig; const getConfig = (wireguardInterface, address) => Effect.gen(function* () { const uapiConfig = yield* internalInterface.userspaceContact(wireguardInterface, "get=1\n\n"); const [interfaceConfig, ...peers] = uapiConfig.split("public_key="); const { fwmark, listen_port, private_key } = ini.decode(interfaceConfig ?? ""); const peerConfigs = yield* Function.pipe(peers, Array.map(peer => `public_key=${peer}`), Array.map(x => Schema.decode(WireguardPeer.WireguardUapiGetPeer, { onExcessProperty: "error" })(x)), Array.map(Effect.flatMap(x => Schema.encode(WireguardPeer.WireguardPeer["uapi"])(x))), Effect.allWith()); return yield* Schema.decode(WireguardConfig["uapi"], { onExcessProperty: "error" })({ Address: address, ListenPort: listen_port, PrivateKey: Buffer.from(private_key, "hex").toString("base64"), FirewallMark: Number.parse(fwmark || "").pipe(Option.getOrUndefined), Peers: peerConfigs }); }); /** @internal */ exports.getConfig = getConfig; const addPeer = (wireguardInterface, peer) => Effect.gen(function* () { // Get the config before adding this peer and ensure this peer is not present const configBefore = yield* getConfig(wireguardInterface, "0.0.0.0/0"); assert.ok(configBefore.Peers.find(p => p.PublicKey === peer.PublicKey) === undefined); // Add the peer to the interface const a = yield* Schema.encode(WireguardPeer.WireguardPeer)(peer); const b = yield* Schema.decode(WireguardPeer.WireguardUapiSetPeer)(a); yield* internalInterface.userspaceContact(wireguardInterface, `set=1\n${b}`); // Get the config after adding this peer and ensure this peer is present const configAfter = yield* getConfig(wireguardInterface, "0.0.0.0/0"); assert.ok(configAfter.Peers.find(p => p.PublicKey === peer.PublicKey) !== undefined); }); /** @internal */ exports.addPeer = addPeer; const removePeer = (wireguardInterface, peer) => Effect.gen(function* () { // Get the config before removing this peer and ensure this peer is present const configBefore = yield* getConfig(wireguardInterface, "0.0.0.0/0"); assert.ok(configBefore.Peers.find(p => p.PublicKey === peer.PublicKey) !== undefined); // Remove the peer from the interface const a = yield* Schema.encode(WireguardPeer.WireguardPeer)(peer); const b = yield* Schema.decode(WireguardPeer.WireguardUapiSetPeer)(a); yield* internalInterface.userspaceContact(wireguardInterface, `set=1\n${b}remove=true\n`); // Get the config after removing this peer and ensure this peer is not present const configAfter = yield* getConfig(wireguardInterface, "0.0.0.0/0"); assert.ok(configAfter.Peers.find(p => p.PublicKey === peer.PublicKey) === undefined); }); /** @internal */ exports.removePeer = removePeer; const streamPeerStats = wireguardInterface => { const pull = getConfig(wireguardInterface, "0.0.0.0/0"); const schedule = Schedule.spaced("1 second"); const stream = Stream.repeatEffectWithSchedule(pull, schedule); return Stream.map(stream, ({ Peers: peers }) => peers); }; exports.streamPeerStats = streamPeerStats; //# sourceMappingURL=circular.js.map