/** * Utilities for connecting to the Wireguard demo server at demo.wireguard.com * * @since 1.0.0 */ import type * as CommandExecutor from "@effect/platform/CommandExecutor"; import type * as PlatformError from "@effect/platform/Error"; import type * as FileSystem from "@effect/platform/FileSystem"; import type * as Path from "@effect/platform/Path"; import * as InternetSchemas from "effect-schemas/Internet"; import type * as Cause from "effect/Cause"; import type * as ParseResult from "effect/ParseResult"; import type * as Scope from "effect/Scope"; 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 HttpServer from "@effect/platform/HttpServer"; import * as HttpServerResponse from "@effect/platform/HttpServerResponse"; import * as Socket from "@effect/platform/Socket"; import * as SocketServer from "@effect/platform/SocketServer"; 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 Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as String from "effect/String"; import * as Tuple from "effect/Tuple"; import * as dns from "node:dns"; import * as http from "node:http"; import type * as WireguardInternetSchemas from "./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"; import * as internalInternetSchemas from "./internal/internetSchemas.ts"; /** * @since 1.0.0 * @category Schema */ export class WireguardDemoServerSchema extends Schema.transform( Schema.TemplateLiteral( Schema.Literal("OK"), Schema.Literal(":"), Schema.String, Schema.Literal(":"), Schema.Number, Schema.Literal(":"), Schema.String, Schema.Literal("\n") ), Schema.Struct({ serverPort: InternetSchemas.Port, serverPublicKey: WireguardKey.WireguardKey, yourWireguardAddress: InternetSchemas.Address, }), { decode: (input) => { const [_status, key, port, address] = internalInternetSchemas.splitLiteral(input, ":"); return { serverPublicKey: key, serverPort: Number.parseInt(port), yourWireguardAddress: address.slice(0, -1), }; }, encode: ({ serverPort, serverPublicKey, yourWireguardAddress }) => `OK:${serverPublicKey}:${serverPort}:${yourWireguardAddress}\n` as const, } ).annotations({ 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 => Effect.async((resume) => { dns.lookup(host, (err, address, _family) => { if (err) { const error = new Socket.SocketGenericError({ cause: `Could not lookup ${host}`, reason: "Open" }); 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 => 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.decodeUnknown(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.decode(InternetSchemas.CidrBlockFromString)(address); const networkAddress = cidr.networkAddress; const allowedIps = new Set([`${networkAddress.ip}${netmask}`] as const); return yield* Schema.decode(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 = `WireGuard Demo Configuration: Success!
Congrats! You've successfully configured WireGuard!



`; /** * 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; wireguardNetwork: Schema.Schema.Encoded; }): Effect.Effect< void, | Socket.SocketError | ParseResult.ParseError | Cause.TimeoutException | WireguardErrors.WireguardError | PlatformError.PlatformError | SocketServer.SocketServerError, | Scope.Scope | FileSystem.FileSystem | Path.Path | SocketServer.SocketServer | WireguardControl.WireguardControl | CommandExecutor.CommandExecutor > => Effect.gen(function* () { const server = yield* SocketServer.SocketServer; // Generate the server's wireguard keys and network const serverWireguardKeys = WireguardKey.generateKeyPair(); const wireguardNetwork = yield* Schema.decode(InternetSchemas.CidrBlockFromString)(options.wireguardNetwork); const networkSize = wireguardNetwork.total; // Setup the wireguard peer address pool const serverWireguardAddressPool = yield* Queue.dropping>( Math.min(options?.maxPeers || 256, Number(networkSize)) ); yield* Function.pipe( wireguardNetwork.range as Stream.Stream< Schema.Schema.Type, ParseResult.ParseError, never >, Stream.drop(2), Stream.run(Sink.fromQueue(serverWireguardAddressPool)) ); const addressReservationLookup = HashMap.empty< WireguardKey.WireguardKey, Schema.Schema.Type >(); // Setup the wireguard interface and wireguard server config const serverWireguardInterface = yield* WireguardInterface.WireguardInterface.getNextAvailableInterface; const serverWireguardConfig = yield* Schema.decode(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>(); yield* Stream.fromQueue(responses).pipe( Stream.pipeThroughChannel(Socket.toChannel(socket)), Stream.decodeText(), Stream.map(String.replace("\n", "")), Stream.mapEffect(Schema.decode(WireguardKey.WireguardKey)), Stream.mapEffect((request) => Schema.decode(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.encode(WireguardDemoServerSchema)(res); yield* responses.offer(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", }) ) ) .pipe(Effect.runFork); return yield* server.run(requestHandler); });