the-wireguard-effect
Version:
Cross platform wireguard api client for nodejs built on wireguard-go with effect-ts
845 lines (793 loc) • 32.5 kB
text/typescript
/**
* Tools to help generate wireguard configs for common situations.
*
* @since 1.0.0
*/
import type * as ParseResult from "effect/ParseResult";
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 Schema from "effect/Schema";
import * as Tuple from "effect/Tuple";
import * as assert from "node:assert";
import * as InternetSchemas from "./InternetSchemas.js";
import * as WireguardConfig from "./WireguardConfig.js";
import * as WireguardErrors from "./WireguardErrors.js";
import * as WireguardKey from "./WireguardKey.js";
import * as internalInternalSchemas from "./internal/internetSchemas.js";
/** A node in the network can either have an ipv4 or ipv6 address. */
type WireguardIPv4RoamingPeer = InternetSchemas.IPv4;
type WireguardIPv6RoamingPeer = InternetSchemas.IPv6;
type WireguardRoamingPeer = WireguardIPv4RoamingPeer | WireguardIPv6RoamingPeer;
/**
* A server in the network can either have an ipv4 address pool or ipv6 address
* pool.
*/
type WireguardIPv4Server = InternetSchemas.IPv4SetupData | InternetSchemas.HostnameIPv4SetupData;
type WireguardIPv6Server = InternetSchemas.IPv6SetupData | InternetSchemas.HostnameIPv6SetupData;
type WireguardServer = WireguardIPv4Server | WireguardIPv6Server;
/**
* Every node in the network must have a public+private key pair and possibly a
* preshare key as well.
*/
type Keys = {
publicKey: WireguardKey.WireguardKey;
privateKey: WireguardKey.WireguardKey;
preshareKey?: WireguardKey.WireguardKey;
};
/**
* The nodes in the network must be homogeneous (either all ipv4 or all ipv6).
* If you want dual stack, you must have two separate networks.
*/
type WireguardIPv4Node = WireguardIPv4Server | WireguardIPv4RoamingPeer;
type WireguardIPv6Node = WireguardIPv6Server | WireguardIPv6RoamingPeer;
type WireguardNode = WireguardIPv4Node | WireguardIPv6Node;
/**
* Base layer containing just the nodes in the network.
*
* @since 1.0.0
* @category WireguardGenerate
*/
export type NodesLayer<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = {
nodes: Nodes;
wireguardNetworkCidr: Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never;
};
/**
* Layer containing the keys for each node in the network.
*
* @since 1.0.0
* @category WireguardGenerate
*/
export type keysLayer<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = NodesLayer<Nodes> & {
keys: Record.ReadonlyRecord<
Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
Keys
>;
};
/**
* Layer containing the connections for each node in the network.
*
* @since 1.0.0
* @category WireguardGenerate
*/
export type ConnectionsLayer<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = keysLayer<Nodes> & {
connections: Record.ReadonlyRecord<
Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
Array.NonEmptyReadonlyArray<
Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"]
>
>;
};
/**
* Layer containing the allowed IPs for each node in the network.
*
* @since 1.0.0
* @category WireguardGenerate
*/
export type AllowedIPsLayer<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = ConnectionsLayer<Nodes> & {
allowedIPs: Record.ReadonlyRecord<
Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
Array.NonEmptyReadonlyArray<{
block: InternetSchemas.CidrBlockFromStringEncoded;
from: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"];
}>
>;
};
/**
* The final network type.
*
* @since 1.0.0
* @category WireguardGenerate
*/
export type WireguardNetwork<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = AllowedIPsLayer<Nodes>;
/** @internal */
export const ipForNode = Function.pipe(
Match.type<WireguardIPv4Node | WireguardIPv6Node>(),
Match.when(Schema.is(InternetSchemas.IPv4), ({ ip }) => ip),
Match.when(Schema.is(InternetSchemas.IPv6), ({ ip }) => ip),
Match.whenOr(
Schema.is(InternetSchemas.IPv4SetupData),
Schema.is(InternetSchemas.IPv6SetupData),
Schema.is(InternetSchemas.HostnameIPv4SetupData),
Schema.is(InternetSchemas.HostnameIPv6SetupData),
([_, { ip }]) => ip
),
Match.exhaustive
);
/** @internal */
export const nodesByIp = <
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
nodes: Nodes
): Record.ReadonlyRecord<
Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
Nodes[number]
> =>
Function.pipe(
nodes,
Array.map((node) => Tuple.make(ipForNode(node), node)),
Record.fromEntries
);
/** @internal */
export const makeEmptyContext = <
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
nodes: Nodes
): Record.ReadonlyRecord<
Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
undefined
> => 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 = <
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
nodesLayer: NodesLayer<Nodes>
): keysLayer<Nodes> => ({
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 = <
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
keysLayer: keysLayer<Nodes>
): keysLayer<Nodes> => ({
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 = <
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
keysLayer: keysLayer<Nodes>
): ConnectionsLayer<Nodes> => {
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.isNonEmptyReadonlyArray(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 = <
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
keysLayer: keysLayer<Nodes>
): ConnectionsLayer<Nodes> => ({
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 = Function.dual<
<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
from: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
to: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"]
) => (connectionsLayer: ConnectionsLayer<Nodes>) => ConnectionsLayer<Nodes>,
<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
connectionsLayer: ConnectionsLayer<Nodes>,
from: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
to: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"]
) => ConnectionsLayer<Nodes>
>(
3,
<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
connectionsLayer: ConnectionsLayer<Nodes>,
from: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
to: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"]
): ConnectionsLayer<Nodes> => {
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 = <
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
connectionsLayer: ConnectionsLayer<Nodes>
): AllowedIPsLayer<Nodes> => ({
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` as const }))
),
});
/**
* Adds an allowed IP to a node in the network.
*
* @since 1.0.0
* @category AllowedIPs Transformers
*/
export const addAllowedIPs = Function.dual<
<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
nodeToIp: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
nodeFromIp:
| Extract<Nodes[number], WireguardRoamingPeer>["ip"]
| Extract<Nodes[number], WireguardServer>[1]["ip"],
cidrs: Array.NonEmptyReadonlyArray<
InternetSchemas.IPv4CidrBlock | InternetSchemas.IPv6CidrBlock | InternetSchemas.CidrBlockFromStringEncoded
>
) => (allowedIPsLayer: AllowedIPsLayer<Nodes>) => AllowedIPsLayer<Nodes>,
<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
allowedIPsLayer: AllowedIPsLayer<Nodes>,
nodeToIp: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
nodeFromIp:
| Extract<Nodes[number], WireguardRoamingPeer>["ip"]
| Extract<Nodes[number], WireguardServer>[1]["ip"],
cidrs: Array.NonEmptyReadonlyArray<
InternetSchemas.IPv4CidrBlock | InternetSchemas.IPv6CidrBlock | InternetSchemas.CidrBlockFromStringEncoded
>
) => AllowedIPsLayer<Nodes>
>(
4,
<
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
allowedIPsLayer: AllowedIPsLayer<Nodes>,
nodeToIp: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
nodeFromIp:
| Extract<Nodes[number], WireguardRoamingPeer>["ip"]
| Extract<Nodes[number], WireguardServer>[1]["ip"],
cidrs: Array.NonEmptyReadonlyArray<
InternetSchemas.IPv4CidrBlock | InternetSchemas.IPv6CidrBlock | InternetSchemas.CidrBlockFromStringEncoded
>
): AllowedIPsLayer<Nodes> => {
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 }): InternetSchemas.CidrBlockFromStringEncoded => `${address.ip}/${mask}` as const
),
Match.when(
Schema.is(InternetSchemas.IPv6CidrBlock),
({ address, mask }): InternetSchemas.CidrBlockFromStringEncoded => `${address.ip}/${mask}` as const
),
Match.when(Predicate.isString, Function.identity<InternetSchemas.CidrBlockFromStringEncoded>),
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 = <
Nodes extends
| readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
| readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>({
allowedIPs,
connections,
keys,
nodes,
wireguardNetworkCidr,
}: WireguardNetwork<Nodes>): Effect.Effect<
readonly [
WireguardConfig.WireguardConfig,
WireguardConfig.WireguardConfig,
...ReadonlyArray<WireguardConfig.WireguardConfig>,
],
ParseResult.ParseError | WireguardErrors.WireguardError,
never
> => {
const endpointsByIp = nodesByIp(nodes) as Record.ReadonlyRecord<
Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
WireguardNode
>;
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(InternetSchemas.SetupData)(endpointForNode)
? Schema.encode(InternetSchemas.SetupData)(endpointForNode).pipe(Effect.map((x) => x[0]))
: Effect.succeed(undefined);
const address = Schema.is(InternetSchemas.SetupData)(endpointForNode)
? endpointForNode[1].ip
: endpointForNode.ip;
return {
Address: address,
Endpoint: endpointForNodeEncoded,
PublicKey: keysForNode.publicKey,
PresharedKey: keysForNode.preshareKey,
} as const;
})
)
);
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(InternetSchemas.SetupData)(endpointForNode)
? endpointForNode[1].ip
: endpointForNode.ip;
const listenPort = Schema.is(InternetSchemas.SetupData)(endpointForNode)
? endpointForNode[0].listenPort
: 0;
const directConnectionsForNode = yield* Function.pipe(
connections,
Record.get(ipForNode(node)),
Option.getOrThrow,
Array.filterMap((peerAddress) => Record.get(peerConfigsByIp, peerAddress)),
Effect.allWith()
);
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.decode(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 = <
Nodes extends
| readonly [server: WireguardIPv4Server, client: WireguardIPv4Node]
| readonly [server: WireguardIPv6Server, client: WireguardIPv6Node],
NetworkCidr extends Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never,
>(options: {
nodes: Nodes;
wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> =>
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 = <
Nodes extends
| readonly [server: WireguardIPv4Server, client: WireguardIPv4Node]
| readonly [server: WireguardIPv6Server, client: WireguardIPv6Node],
NetworkCidr1 extends Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never,
NetworkCidr2 extends Nodes[0] extends WireguardIPv4Server
? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
: Nodes[0] extends WireguardIPv6Server
? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
: never,
>(options: {
nodes: Nodes;
wireguardNetworkCidr: NetworkCidr1;
lanNetworkCidr: NetworkCidr2;
}): WireguardNetwork<Nodes> =>
Function.flow(
generateRemoteAccessToServer,
addAllowedIPs(
ipForNode(options.nodes[1]),
ipForNode(options.nodes[0]),
Array.isArray(options.lanNetworkCidr)
? options.lanNetworkCidr
: Array.of(options.lanNetworkCidr as InternetSchemas.CidrBlockBase<InternetSchemas.Family>)
)
)(options);
/**
* Allows two servers to connect to each other.
*
* @since 1.0.0
* @category Generators
*/
export const generateServerToServerAccess = <
Nodes extends
| readonly [server1: WireguardIPv4Server, server2: WireguardIPv4Server]
| readonly [server1: WireguardIPv6Server, server2: WireguardIPv6Server],
NetworkCidr extends Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never,
>(options: {
nodes: Nodes;
wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> =>
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 = <
Nodes extends
| readonly [server1: WireguardIPv4Server, server2: WireguardIPv4Server]
| readonly [server1: WireguardIPv6Server, server2: WireguardIPv6Server],
NetworkCidr1 extends Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never,
NetworkCidr2 extends Nodes[0] extends WireguardIPv4Server
? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
: Nodes[0] extends WireguardIPv6Server
? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
: never,
NetworkCidr3 extends Nodes[1] extends WireguardIPv4Server
? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
: Nodes[1] extends WireguardIPv6Server
? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
: never,
>(options: {
nodes: Nodes;
server1Lan: NetworkCidr2;
server2Lan: NetworkCidr3;
wireguardNetworkCidr: NetworkCidr1;
}): WireguardNetwork<Nodes> =>
Function.flow(
generateServerToServerAccess,
addAllowedIPs(
ipForNode(options.nodes[1]),
ipForNode(options.nodes[0]),
Array.isArray(options.server1Lan)
? options.server1Lan
: Array.of(options.server1Lan as InternetSchemas.CidrBlockBase<InternetSchemas.Family>)
),
addAllowedIPs(
ipForNode(options.nodes[0]),
ipForNode(options.nodes[1]),
Array.isArray(options.server2Lan)
? options.server2Lan
: Array.of(options.server2Lan as InternetSchemas.CidrBlockBase<InternetSchemas.Family>)
)
)(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 = <
Nodes extends
| readonly [server: WireguardIPv4Server, ...nodes: Array.NonEmptyReadonlyArray<WireguardIPv4Node>]
| readonly [server: WireguardIPv6Server, ...nodes: Array.NonEmptyReadonlyArray<WireguardIPv6Node>],
NetworkCidr extends Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never,
>(options: {
nodes: Nodes;
wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> => {
const clientNodes = Function.pipe(options.nodes, Array.map(ipForNode), Array.tailNonEmpty);
const firstFoldOnClientNodes = Array.reduce(
clientNodes,
Function.identity<AllowedIPsLayer<Nodes>>,
(outerAcc, node) => {
const secondFoldOverOtherNodes = Array.reduce(
Array.filter(clientNodes, (n) => n !== node),
Function.identity<AllowedIPsLayer<Nodes>>,
(innerAcc, otherNode) =>
Function.compose(
addAllowedIPs(node, ipForNode(options.nodes[0]), [`${otherNode}/32`] as const),
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 = <
Nodes extends
| readonly [server: WireguardIPv4Server, ...nodes: Array.NonEmptyReadonlyArray<WireguardIPv4Node>]
| readonly [server: WireguardIPv6Server, ...nodes: Array.NonEmptyReadonlyArray<WireguardIPv6Node>],
NetworkCidr extends Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never,
NetworkCidr2 extends Nodes[0] extends WireguardIPv4Server
? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
: Nodes[0] extends WireguardIPv6Server
? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
: never,
>(options: {
nodes: Nodes;
lanNetworkCidr: NetworkCidr2;
wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> => {
const addAllowedIPsToAllNodes = Function.pipe(
options.nodes,
Array.map(ipForNode),
Array.tailNonEmpty,
Array.reduce(Function.identity<AllowedIPsLayer<Nodes>>, (acc, node) =>
Function.compose(
addAllowedIPs(
node,
ipForNode(options.nodes[0]),
Array.isArray(options.lanNetworkCidr)
? options.lanNetworkCidr
: Array.of(options.lanNetworkCidr as InternetSchemas.CidrBlockBase<InternetSchemas.Family>)
),
acc
)
)
);
return addAllowedIPsToAllNodes(generateServerHubAndSpokeAccess(options));
};
/**
* Route specific traffic through a commercial WireGuard VPN provider.
*
* @since 1.0.0
* @category Generators
*/
export const generateVpnTunneledAccess = <
Nodes extends
| readonly [server: WireguardIPv4Server, client: WireguardIPv4Node]
| readonly [server: WireguardIPv6Server, client: WireguardIPv6Node],
NetworkCidr extends Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never,
>(options: {
nodes: Nodes;
wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> =>
Function.flow(
generateRemoteAccessToServer,
addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"] as const)
)(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 = <
Nodes extends
| readonly [server: WireguardIPv4Server, client: WireguardIPv4Node]
| readonly [server: WireguardIPv6Server, client: WireguardIPv6Node],
NetworkCidr1 extends Nodes[0] extends WireguardIPv4Node
? InternetSchemas.IPv4CidrBlock
: Nodes[0] extends WireguardIPv6Node
? InternetSchemas.IPv6CidrBlock
: never,
NetworkCidr2 extends Nodes[0] extends WireguardIPv4Server
? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
: Nodes[0] extends WireguardIPv6Server
? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
: never,
>(options: {
nodes: Nodes;
lanNetworkCidr: NetworkCidr2;
wireguardNetworkCidr: NetworkCidr1;
}): WireguardNetwork<Nodes> =>
Function.flow(
generateRemoteAccessToLan,
addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"] as const)
)(options);