UNPKG

the-wireguard-effect

Version:

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

291 lines 13 kB
/** * Tools to help generate wireguard configs for common situations. * * @since 1.0.0 */ import * as Array from "effect/Array"; import * as Effect from "effect/Effect"; import * as Function from "effect/Function"; import * as Match from "effect/Match"; import * as Option from "effect/Option"; import * as Predicate from "effect/Predicate"; import * as Record from "effect/Record"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import * as Tuple from "effect/Tuple"; import * as assert from "node:assert"; import * as InternetSchemas from "effect-schemas/Internet"; import * as internalInternalSchemas from "./internal/internetSchemas.js"; import * as WireguardInternetSchemas from "./InternetSchemas.js"; import * as WireguardConfig from "./WireguardConfig.js"; import * as WireguardErrors from "./WireguardErrors.js"; import * as WireguardKey from "./WireguardKey.js"; /** @internal */ export const ipForNode = /*#__PURE__*/Function.pipe(/*#__PURE__*/Match.type(), /*#__PURE__*/Match.when(/*#__PURE__*/Schema.is(InternetSchemas.IPv4), ({ ip }) => ip), /*#__PURE__*/Match.when(/*#__PURE__*/Schema.is(InternetSchemas.IPv6), ({ ip }) => ip), /*#__PURE__*/Match.whenOr(/*#__PURE__*/Schema.is(WireguardInternetSchemas.IPv4SetupData), /*#__PURE__*/Schema.is(WireguardInternetSchemas.IPv6SetupData), /*#__PURE__*/Schema.is(WireguardInternetSchemas.HostnameIPv4SetupData), /*#__PURE__*/Schema.is(WireguardInternetSchemas.HostnameIPv6SetupData), ([_, { ip }]) => ip), Match.exhaustive); /** @internal */ export const nodesByIp = nodes => Function.pipe(nodes, Array.map(node => Tuple.make(ipForNode(node), node)), Record.fromEntries); /** @internal */ export const makeEmptyContext = nodes => Record.map(nodesByIp(nodes), Function.constUndefined); /** * Generates private+public keys for all nodes in the network. * * @since 1.0.0 * @category Key Transformers */ export const generateKeys = nodesLayer => ({ nodes: nodesLayer.nodes, wireguardNetworkCidr: nodesLayer.wireguardNetworkCidr, keys: Function.pipe(nodesLayer.nodes, makeEmptyContext, Record.map(WireguardKey.generateKeyPair)) }); /** * Generates preshare keys for all nodes in the network. * * @since 1.0.0 * @category Key Transformers */ export const addPreshareKeys = keysLayer => ({ nodes: keysLayer.nodes, wireguardNetworkCidr: keysLayer.wireguardNetworkCidr, keys: Record.map(keysLayer.keys, (keys, node) => node === ipForNode(keysLayer.nodes[0]) ? keys : { ...keys, preshareKey: WireguardKey.generatePreshareKey() }) }); /** * Generates connections in a star pattern for all nodes in the network. * * @since 1.0.0 * @category Connection Transformers */ export const generateStarConnections = keysLayer => { const allNodeIps = Array.map(keysLayer.nodes, ipForNode); const connections = Function.pipe(keysLayer.nodes, makeEmptyContext, Record.map((_, currentNode) => { const neighbors = Array.filter(allNodeIps, ip => ip !== currentNode); assert.ok(Array.isReadonlyArrayNonEmpty(neighbors), "There must be at least one neighbor"); return neighbors; })); return { connections, nodes: keysLayer.nodes, keys: keysLayer.keys, wireguardNetworkCidr: keysLayer.wireguardNetworkCidr }; }; /** * Generates connections in a hub and spoke pattern for all nodes in the * network. * * @since 1.0.0 * @category Connection Transformers */ export const generateHubAndSpokeConnections = keysLayer => ({ nodes: keysLayer.nodes, keys: keysLayer.keys, wireguardNetworkCidr: keysLayer.wireguardNetworkCidr, connections: Function.pipe(keysLayer.nodes, makeEmptyContext, Record.map((_, node) => { if (node === ipForNode(keysLayer.nodes[0])) { return internalInternalSchemas.tail(keysLayer.nodes); } else { return Array.of(keysLayer.nodes[0]); } }), Record.map(Array.map(ipForNode))) }); /** * Adds a direct connection between two nodes in the network. * * @since 1.0.0 * @category Connection Transformers */ export const addConnection = /*#__PURE__*/Function.dual(3, (connectionsLayer, from, to) => { const oldConnections = Record.get(connectionsLayer.connections, from); const newConnections = Function.pipe(oldConnections, Option.map(connections => Array.append(connections, to)), Option.getOrElse(() => Array.of(to))); return { nodes: connectionsLayer.nodes, keys: connectionsLayer.keys, wireguardNetworkCidr: connectionsLayer.wireguardNetworkCidr, connections: Record.set(connectionsLayer.connections, from, newConnections) }; }); /** * Generates the allowed IPs for each node in the network based on the * connections is has to the other nodes. * * @since 1.0.0 * @category AllowedIPs Transformers */ export const computeAllowedIPsFromConnections = connectionsLayer => ({ nodes: connectionsLayer.nodes, keys: connectionsLayer.keys, connections: connectionsLayer.connections, wireguardNetworkCidr: connectionsLayer.wireguardNetworkCidr, allowedIPs: Record.map(connectionsLayer.connections, Array.map(ip => ({ from: ip, block: `${ip}/32` }))) }); /** * Adds an allowed IP to a node in the network. * * @since 1.0.0 * @category AllowedIPs Transformers */ export const addAllowedIPs = /*#__PURE__*/Function.dual(4, (allowedIPsLayer, nodeToIp, nodeFromIp, cidrs) => { const oldCidrs = Record.get(allowedIPsLayer.allowedIPs, nodeToIp); const cidrsEncoded = Array.map(cidrs, cidr => Function.pipe(Match.value(cidr), Match.when(Schema.is(InternetSchemas.IPv4CidrBlock), ({ address, mask }) => `${address.ip}/${mask}`), Match.when(Schema.is(InternetSchemas.IPv6CidrBlock), ({ address, mask }) => `${address.ip}/${mask}`), Match.when(Predicate.isString, Function.identity), Match.exhaustive)); const mappedCidrs = Array.map(cidrsEncoded, cidr => ({ from: nodeFromIp, block: cidr })); const newCidrs = Function.pipe(oldCidrs, Option.map(oldCidrs => Array.appendAll(oldCidrs, mappedCidrs)), Option.getOrElse(() => mappedCidrs)); return { nodes: allowedIPsLayer.nodes, keys: allowedIPsLayer.keys, connections: allowedIPsLayer.connections, wireguardNetworkCidr: allowedIPsLayer.wireguardNetworkCidr, allowedIPs: Record.set(allowedIPsLayer.allowedIPs, nodeToIp, newCidrs) }; }); /** * Converts a network into configs. * * @since 1.0.0 * @category Generator */ export const toConfigs = ({ allowedIPs, connections, keys, nodes, wireguardNetworkCidr }) => { const endpointsByIp = nodesByIp(nodes); const peerConfigsByIp = Function.pipe(nodes, nodesByIp, Record.map(node => Effect.gen(function* () { const keysForNode = Record.get(keys, ipForNode(node)).pipe(Option.getOrThrow); const endpointForNode = Record.get(endpointsByIp, ipForNode(node)).pipe(Option.getOrThrow); const endpointForNodeEncoded = yield* Schema.is(WireguardInternetSchemas.SetupData)(endpointForNode) ? Schema.encodeEffect(WireguardInternetSchemas.SetupData)(endpointForNode).pipe(Effect.map(x => x[0])) : Effect.sync(() => undefined); const address = Schema.is(WireguardInternetSchemas.SetupData)(endpointForNode) ? endpointForNode[1].ip : endpointForNode.ip; return { Address: address, Endpoint: endpointForNodeEncoded, PublicKey: keysForNode.publicKey, PresharedKey: keysForNode.preshareKey }; }))); const configs = Array.map(nodes, node => Effect.gen(function* () { const keysForNode = Record.get(keys, ipForNode(node)).pipe(Option.getOrThrow); const endpointForNode = Record.get(endpointsByIp, ipForNode(node)).pipe(Option.getOrThrow); const allowedIPsForNode = Record.get(allowedIPs, ipForNode(node)).pipe(Option.getOrThrow); const address = Schema.is(WireguardInternetSchemas.SetupData)(endpointForNode) ? endpointForNode[1].ip : endpointForNode.ip; const listenPort = Schema.is(WireguardInternetSchemas.SetupData)(endpointForNode) ? endpointForNode[0].listenPort : 0; const directConnectionsForNode = yield* Function.pipe(connections, Record.get(ipForNode(node)), Option.getOrThrow, Array.map(peerAddress => Record.get(peerConfigsByIp, peerAddress)), Array.filterMap(Result.fromOption(() => Result.failVoid)), Effect.all); const dcsWithAllowedIPs = Array.map(directConnectionsForNode, dc => ({ Endpoint: dc.Endpoint, PublicKey: dc.PublicKey, PresharedKey: node === nodes[0] ? dc.PresharedKey : keysForNode.preshareKey, AllowedIPs: Function.pipe(allowedIPsForNode, Array.filter(({ from }) => from === dc.Address), Array.map(({ block }) => block), x => new Set(x)) })); return yield* Schema.decodeEffect(WireguardConfig.WireguardConfig)({ ListenPort: listenPort, Peers: dcsWithAllowedIPs, PrivateKey: keysForNode.privateKey, Address: `${address}/${wireguardNetworkCidr.mask}` }); })); if (Predicate.isTupleOfAtLeast(configs, 2)) { return Effect.all(configs); } else { return Effect.fail(new WireguardErrors.WireguardError({ message: "There must be at least two nodes" })); } }; /** * Use your phone or computer to remotely access just the wireguard server. * * @since 1.0.0 * @category Generators */ export const generateRemoteAccessToServer = options => Function.flow(generateKeys, addPreshareKeys, generateStarConnections, computeAllowedIPsFromConnections)(options); /** * Builds on "Remote access to server", allowing you to access your entire LAN * as well. * * @since 1.0.0 * @category Generators */ export const generateRemoteAccessToLan = options => Function.flow(generateRemoteAccessToServer, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), Array.isArray(options.lanNetworkCidr) ? options.lanNetworkCidr : Array.of(options.lanNetworkCidr)))(options); /** * Allows two servers to connect to each other. * * @since 1.0.0 * @category Generators */ export const generateServerToServerAccess = options => Function.flow(generateKeys, addPreshareKeys, generateHubAndSpokeConnections, computeAllowedIPsFromConnections)(options); /** * Builds on "Server to server access", allowing two entire networks to * communicate. * * @since 1.0.0 * @category Generators */ export const generateLanToLanAccess = options => Function.flow(generateServerToServerAccess, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), Array.isArray(options.server1Lan) ? options.server1Lan : Array.of(options.server1Lan)), addAllowedIPs(ipForNode(options.nodes[0]), ipForNode(options.nodes[1]), Array.isArray(options.server2Lan) ? options.server2Lan : Array.of(options.server2Lan)))(options); /** * Builds on "Remote access to server", except that all of the VPN clients can * connect to each other as well. Note: all traffic between nodes must pass * through the server. * * @since 1.0.0 * @category Generators */ export const generateServerHubAndSpokeAccess = options => { const clientNodes = Function.pipe(options.nodes, Array.map(ipForNode), Array.tailNonEmpty); const firstFoldOnClientNodes = Array.reduce(clientNodes, Function.identity, (outerAcc, node) => { const secondFoldOverOtherNodes = Array.reduce(Array.filter(clientNodes, n => n !== node), Function.identity, (innerAcc, otherNode) => Function.compose(addAllowedIPs(node, ipForNode(options.nodes[0]), [`${otherNode}/32`]), innerAcc)); return Function.compose(secondFoldOverOtherNodes, outerAcc); }); const networkWithoutSpokeAccess = Function.flow(generateKeys, addPreshareKeys, generateHubAndSpokeConnections, computeAllowedIPsFromConnections)(options); return firstFoldOnClientNodes(networkWithoutSpokeAccess); }; /** * Builds on "Server hub and spoke access", allowing you to access your entire * LAN as well. * * @since 1.0.0 * @category Generators */ export const generateLanHubAndSpokeAccess = options => { const addAllowedIPsToAllNodes = Function.pipe(options.nodes, Array.map(ipForNode), Array.tailNonEmpty, Array.reduce(Function.identity, (acc, node) => Function.compose(addAllowedIPs(node, ipForNode(options.nodes[0]), Array.isArray(options.lanNetworkCidr) ? options.lanNetworkCidr : Array.of(options.lanNetworkCidr)), acc))); return addAllowedIPsToAllNodes(generateServerHubAndSpokeAccess(options)); }; /** * Route specific traffic through a commercial WireGuard VPN provider. * * @since 1.0.0 * @category Generators */ export const generateVpnTunneledAccess = options => Function.flow(generateRemoteAccessToServer, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"]))(options); /** * Securely access the Internet from untrusted networks by routing all of your * traffic through the VPN and out the server's internet connection. * * @since 1.0.0 * @category Generators */ export const generateRemoteTunneledAccess = options => Function.flow(generateRemoteAccessToLan, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"]))(options); //# sourceMappingURL=WireguardGenerate.js.map