the-wireguard-effect
Version:
Cross platform wireguard api client for nodejs built on wireguard-go with effect-ts
291 lines • 13 kB
JavaScript
/**
* 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