the-wireguard-effect
Version:
Cross platform wireguard api client for nodejs built on wireguard-go with effect-ts
336 lines (310 loc) • 14.5 kB
text/typescript
/**
* Utilities for connecting to the Wireguard demo server at demo.wireguard.com
*
* @since 1.0.0
*/
import type * as Cause from "effect/Cause";
import type * as FileSystem from "effect/FileSystem";
import type * as Path from "effect/Path";
import type * as PlatformError from "effect/PlatformError";
import type * as Scope from "effect/Scope";
import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import * as Array from "effect/Array";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Function from "effect/Function";
import * as Layer from "effect/Layer";
import * as HashMap from "effect/MutableHashMap";
import * as Option from "effect/Option";
import * as Queue from "effect/Queue";
import * as Schema from "effect/Schema";
import * as SchemaGetter from "effect/SchemaGetter";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as String from "effect/String";
import * as Tuple from "effect/Tuple";
import * as HttpServer from "effect/unstable/http/HttpServer";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import * as Socket from "effect/unstable/socket/Socket";
import * as SocketServer from "effect/unstable/socket/SocketServer";
import * as dns from "node:dns";
import * as http from "node:http";
import type * as WireguardInternetSchemas from "./InternetSchemas.ts";
import type * as WireguardControl from "./WireguardControl.ts";
import type * as WireguardErrors from "./WireguardErrors.ts";
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
import * as NodeSocket from "@effect/platform-node/NodeSocket";
import * as InternetSchemas from "effect-schemas/Internet";
import * as internalInternetSchemas from "./internal/internetSchemas.ts";
import * as WireguardConfig from "./WireguardConfig.ts";
import * as WireguardInterface from "./WireguardInterface.ts";
import * as WireguardKey from "./WireguardKey.ts";
import * as WireguardPeer from "./WireguardPeer.ts";
/**
* @since 1.0.0
* @category Schema
*/
export const WireguardDemoServerSchema = Schema.TemplateLiteral([
Schema.Literal("OK"),
Schema.Literal(":"),
Schema.String,
Schema.Literal(":"),
Schema.Number,
Schema.Literal(":"),
Schema.String,
Schema.Literal("\n"),
]).pipe(
Schema.decodeTo(
Schema.Struct({
serverPort: InternetSchemas.Port,
serverPublicKey: WireguardKey.WireguardKey,
yourWireguardAddress: InternetSchemas.Address,
}),
{
decode: SchemaGetter.transform((input) => {
const [_status, key, port, address] = internalInternetSchemas.splitLiteral(input, ":");
return {
serverPublicKey: key,
serverPort: Number.parseInt(port),
yourWireguardAddress: address.slice(0, -1),
};
}),
encode: SchemaGetter.transform(
({ serverPort, serverPublicKey, yourWireguardAddress }) =>
`OK:${serverPublicKey}:${serverPort}:${yourWireguardAddress}\n` as const
),
}
),
Schema.annotate({
identifier: "WireguardDemoSchema",
description: "Wireguard demo server response",
})
);
/**
* Attempts a DNS lookup of the given host (needed because wireguard will not
* perform dns lookups).
*
* @internal
*/
const dnsLookup = (host: string): Effect.Effect<string, Socket.SocketError, never> =>
Effect.callback<string, Socket.SocketError>((resume) => {
dns.lookup(host, (err, address, _family) => {
if (err) {
const error = new Socket.SocketError({
reason: new Socket.SocketOpenError({
kind: "Unknown",
cause: err,
}),
});
return resume(Effect.fail(error));
} else {
return resume(Effect.succeed(address));
}
});
});
/**
* Creates a Wireguard configuration to connect to demo.wireguard.com. When
* connected, you should be able to see the hidden page at 192.168.4.1
*
* @since 1.0.0
* @see https://git.zx2c4.com/wireguard-tools/plain/contrib/ncat-client-server/client.sh
*/
export const requestWireguardDemoConfig = (
connectOptions = { port: 42912, host: "demo.wireguard.com" },
{ privateKey, publicKey } = WireguardKey.generateKeyPair()
): Effect.Effect<WireguardConfig.WireguardConfig, Socket.SocketError | Schema.SchemaError, never> =>
Function.pipe(
// Connect to the server and send our public key
Stream.make(`${publicKey}\n`),
Stream.concat(Stream.never),
Stream.pipeThroughChannelOrFail(NodeSocket.makeNetChannel(connectOptions)),
// Decode the server's response
Stream.decodeText(),
Stream.run(Sink.head()),
Effect.map(Option.getOrUndefined),
Effect.flatMap(Schema.decodeUnknownEffect(WireguardDemoServerSchema)),
// Create the wireguard configuration
Effect.andThen((serverResponse) =>
Effect.gen(function* () {
// TODO: Is this a safe assumption?
const netmask = "/24" as const;
const host = yield* dnsLookup(connectOptions.host);
const address = `${serverResponse.yourWireguardAddress.ip}${netmask}` as const;
const cidr = yield* Schema.decodeEffect(InternetSchemas.CidrBlockFromString)(address);
const networkAddress = cidr.networkAddress;
const allowedIps = new Set([`${networkAddress.ip}${netmask}`] as const);
return yield* Schema.decodeEffect(WireguardConfig.WireguardConfig)({
ListenPort: 0,
Dns: "1.1.1.1",
Address: address,
PrivateKey: privateKey,
Peers: [
{
AllowedIPs: allowedIps,
PersistentKeepalive: 25,
PublicKey: serverResponse.serverPublicKey,
Endpoint: `${host}:${serverResponse.serverPort}`,
},
],
});
})
)
);
/** @internal */
const hiddenPageContent = `<title>WireGuard Demo Configuration: Success!</title>
<body bgcolor="#444444">
<script src="snowstorm.js"></script>
<script src="trail.js"></script>
<center>
<blink>
<marquee width="100%" behavior="alternate" direction="right" scrollamount="10">
<marquee height="100%" behavior="alternate" direction="down">
<marquee width="100%" bgcolor="#33aadd" direction="right" behavior="alternate"><font face="comic sans ms" size="7" style="font-size: 3vw" color="#ddaa33">Congrats! You've successfully configured WireGuard!</font><br><marquee scrollamount="30"><img src="emblem.svg" width="20%"></marquee><br><marquee direction="left" scrollamount="40" behavior="alternate"><script>document.write('<iframe frameborder="0" height="80%" width="70%" src="/?' + (((document.location.search.substring(1)|0) + 1) % 4) + '"></iframe>');</script></marquee><br><br></marquee>
</marquee>
</marquee>
</blink>
</center>
</body>
`;
/**
* Mock implementation of the Wireguard demo server at demo.wireguard.com
*
* @since 1.0.0
* @see https://git.zx2c4.com/wireguard-tools/plain/contrib/ncat-client-server/server.sh
*/
export const WireguardDemoServer = (options: {
maxPeers?: number | undefined;
serverEndpoint: Schema.Schema.Type<typeof WireguardInternetSchemas.Endpoint>;
wireguardNetwork: (typeof InternetSchemas.CidrBlockFromString)["Encoded"];
}): Effect.Effect<
void,
| Socket.SocketError
| Schema.SchemaError
| Cause.TimeoutError
| WireguardErrors.WireguardError
| PlatformError.PlatformError
| PlatformError.SystemError
| PlatformError.BadArgument
| SocketServer.SocketServerError,
| Scope.Scope
| FileSystem.FileSystem
| Path.Path
| SocketServer.SocketServer
| WireguardControl.WireguardControl
| ChildProcessSpawner.ChildProcessSpawner
> =>
Effect.gen(function* () {
const server = yield* SocketServer.SocketServer;
// Generate the server's wireguard keys and network
const serverWireguardKeys = WireguardKey.generateKeyPair();
const wireguardNetwork = yield* Schema.decodeEffect(InternetSchemas.CidrBlockFromString)(
options.wireguardNetwork
);
const networkSize = wireguardNetwork.total;
// Setup the wireguard peer address pool
const serverWireguardAddressPool = yield* Queue.dropping<
Schema.Schema.Type<typeof InternetSchemas.Address>,
Cause.Done | Schema.SchemaError
>(Math.min(options?.maxPeers || 256, Number(networkSize)));
yield* Function.pipe(
wireguardNetwork.range as Stream.Stream<
Schema.Schema.Type<typeof InternetSchemas.Address>,
Schema.SchemaError,
never
>,
Stream.drop(2),
Stream.runIntoQueue(serverWireguardAddressPool)
);
const addressReservationLookup = HashMap.empty<
WireguardKey.WireguardKey,
Schema.Schema.Type<typeof InternetSchemas.Address>
>();
// Setup the wireguard interface and wireguard server config
const serverWireguardInterface = yield* WireguardInterface.WireguardInterface.getNextAvailableInterface;
const serverWireguardConfig = yield* Schema.decodeEffect(WireguardConfig.WireguardConfig)({
Address: options.wireguardNetwork,
PrivateKey: serverWireguardKeys.privateKey,
ListenPort: options.serverEndpoint.listenPort,
});
yield* serverWireguardInterface.upScoped(serverWireguardConfig);
const requestHandler = (socket: Socket.Socket) =>
Effect.gen(function* () {
const responses = yield* Queue.unbounded<(typeof WireguardDemoServerSchema)["Encoded"]>();
yield* Stream.fromQueue(responses).pipe(
Stream.pipeThroughChannel(Socket.toChannel(socket)),
Stream.decodeText(),
Stream.map(String.replace("\n", "")),
Stream.mapEffect((str) => Schema.decodeEffect(WireguardKey.WireguardKey)(str)),
Stream.mapEffect((request) =>
Schema.decodeEffect(WireguardPeer.WireguardPeer)({
PublicKey: request,
PersistentKeepalive: 25,
AllowedIPs: new Set(["0.0.0.0/0"]),
})
),
// Prune the oldest peer if we run out of addresses in the queue
Stream.mapEffect((peer) =>
Effect.gen(function* () {
const size = yield* Queue.size(serverWireguardAddressPool);
if (size > 0) return peer;
const config = yield* serverWireguardInterface.getConfig(options.wireguardNetwork);
const lastPeer = Function.pipe(
config.Peers,
Array.sort(
(
a: Schema.Schema.Type<(typeof WireguardPeer.WireguardPeer)["uapi"]>,
b: Schema.Schema.Type<(typeof WireguardPeer.WireguardPeer)["uapi"]>
) => {
const aLastHandshake = DateTime.toEpochMillis(a.lastHandshake);
const bLastHandshake = DateTime.toEpochMillis(b.lastHandshake);
if (aLastHandshake < bLastHandshake) return -1;
else if (aLastHandshake > bLastHandshake) return 1;
else return 0;
}
),
Array.head,
Option.getOrThrow
);
yield* serverWireguardInterface.removePeer(lastPeer);
const freedAddress = HashMap.get(addressReservationLookup, lastPeer.PublicKey).pipe(
Option.getOrThrow
);
yield* Queue.offer(serverWireguardAddressPool, freedAddress);
HashMap.remove(addressReservationLookup, lastPeer.PublicKey);
return peer;
})
),
Stream.mapEffect((peer) =>
Function.pipe(
serverWireguardAddressPool,
Queue.take,
Effect.map((ip) => ({
yourWireguardAddress: ip,
serverPort: options.serverEndpoint.natPort,
serverPublicKey: serverWireguardKeys.publicKey,
})),
Effect.map((response) => Tuple.make(peer, response))
)
),
Stream.runForEach(([peer, res]) =>
Effect.gen(function* () {
const encoded = yield* Schema.encodeEffect(WireguardDemoServerSchema)(res);
yield* Queue.offer(responses, encoded);
HashMap.set(addressReservationLookup, peer.PublicKey, res.yourWireguardAddress);
yield* serverWireguardInterface.addPeer(peer);
})
)
);
});
// Start the server
Layer.launch(HttpServer.serve(Effect.succeed(HttpServerResponse.html(hiddenPageContent)))).pipe(
Effect.provide(
NodeHttpServer.layer(() => http.createServer(), {
port: 8080,
host: "192.168.4.1",
})
),
Effect.runFork
);
return yield* server.run(requestHandler);
});