the-wireguard-effect
Version:
Cross platform wireguard api client for nodejs built on wireguard-go with effect-ts
399 lines (368 loc) • 15.5 kB
text/typescript
/**
* Wireguard peer schema definitions
*
* @since 1.0.0
*/
import type * as Brand from "effect/Brand";
import * as Array from "effect/Array";
import * as DateTime from "effect/DateTime";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Function from "effect/Function";
import * as Number from "effect/Number";
import * as Option from "effect/Option";
import * as ParseResult from "effect/ParseResult";
import * as Schema from "effect/Schema";
import * as ini from "ini";
import * as InternetSchemas from "./InternetSchemas.js";
import * as WireguardKey from "./WireguardKey.js";
import * as internalWireguardPeer from "./internal/wireguardPeer.js";
/**
* A wireguard peer configuration.
*
* @since 1.0.0
* @category Datatypes
* @example
* import * as Schema from "effect/Schema";
* import * as Duration from "effect/Duration";
* import * as Option from "effect/Option";
* import * as InternetSchemas from "the-wireguard-effect/InternetSchemas";
* import * as WireguardKey from "the-wireguard-effect/WireguardKey";
*
* import { WireguardPeer } from "the-wireguard-effect/WireguardPeer";
*
* const preshareKey = WireguardKey.generatePreshareKey();
* const { publicKey, privateKey: _privateKey } =
* WireguardKey.generateKeyPair();
*
* // WireguardPeer
* const peerDirectInstantiation = new WireguardPeer({
* PublicKey: publicKey,
* AllowedIPs: [
* InternetSchemas.CidrBlock({
* ipv4: InternetSchemas.IPv4("192.168.0.0"),
* mask: InternetSchemas.IPv4CidrMask(24),
* }),
* ],
* Endpoint: InternetSchemas.Endpoint(
* InternetSchemas.IPv4Endpoint({
* ip: InternetSchemas.IPv4("192.168.0.1"),
* natPort: InternetSchemas.Port(51820),
* listenPort: InternetSchemas.Port(51820),
* })
* ),
* PersistentKeepalive: Duration.seconds(20),
* PresharedKey: Option.none(),
* });
*
* // Effect.Effect<WireguardPeer, ParseResult.ParseError, never>
* const peerSchemaInstantiation = Schema.decode(WireguardPeer)({
* PublicKey: publicKey,
* PresharedKey: preshareKey,
* Endpoint: "192.168.0.1:51820",
* AllowedIPs: ["192.168.0.0/24"],
* PersistentKeepalive: Duration.seconds(20),
* });
*/
export class WireguardPeer extends internalWireguardPeer.WireguardPeerConfigVariantSchema.Class<WireguardPeer>(
"WireguardPeer"
)({
/**
* The persistent keepalive interval in seconds, 0 disables it. The
* difference between Option.none and Option.some(0) is important when
* performing something like a config update operation on an interface
* because Option.none will not include the keep alive setting in the update
* request, so it will remain the same as it was before the update, whereas
* Option.some(0) will disable the keep alive setting.
*/
PersistentKeepalive: Schema.optionalWith(InternetSchemas.DurationFromSeconds, {
as: "Option",
nullable: true,
}),
/**
* The collection of IPs/masks that will be accepted from this peer. If an
* identical value already exists as part of a prior peer, the allowed IP
* entry will be removed from that peer and added to this peer.
*/
AllowedIPs: Schema.optionalWith(Schema.ReadonlySetFromSelf(InternetSchemas.CidrBlockFromString), {
nullable: true,
default: () => new Set([]),
}),
/**
* The value for this key is either IP:port for IPv4 or [IP]:port for IPv6
* or some.hostname.com:port for a hostname endpoint. The endpoint is
* optional and, if not supplied, the remote peer must connect first (so
* they should have the endpoint set to know where to connect to) and this
* client will just send requests back to the remote peer's source IP and
* port.
*/
Endpoint: Schema.optionalWith(InternetSchemas.Endpoint, { nullable: true }),
/** Lowercase hex-encoded public key of the new peer entry. */
PublicKey: WireguardKey.WireguardKey,
/**
* The preshared key is optional and is a lowercase hex-encoded key. The
* value may be an all zero string in the case of a set operation, in which
* case it indicates that the preshared-key should be removed.
*/
PresharedKey: Schema.optionalWith(WireguardKey.WireguardKey, { nullable: true, as: "Option" }),
/** The number of received bytes. */
rxBytes: internalWireguardPeer.WireguardPeerConfigVariantSchema.FieldOnly("uapi")(Schema.NumberFromString),
/** The number of transmitted bytes. */
txBytes: internalWireguardPeer.WireguardPeerConfigVariantSchema.FieldOnly("uapi")(Schema.NumberFromString),
/**
* The number of seconds since the most recent handshake, expressed relative
* to the Unix epoch.
*/
lastHandshake: internalWireguardPeer.WireguardPeerConfigVariantSchema.FieldOnly("uapi")(
Function.pipe(
Schema.NumberFromString,
Schema.compose(
Schema.transform(Schema.Number, Schema.Number, {
decode: Function.identity,
encode: Number.multiply(1000),
})
),
Schema.compose(Schema.DateTimeUtcFromNumber)
)
),
}) {}
/**
* A wireguard peer configuration encoded in INI format.
*
* @since 1.0.0
* @category Transformations
* @example
* import * as Effect from "effect/Effect";
* import * as Function from "effect/Function";
* import * as Schema from "effect/Schema";
* import * as WireguardKey from "the-wireguard-effect/WireguardKey";
* import * as WireguardPeer from "the-wireguard-effect/WireguardPeer";
*
* const preshareKey = WireguardKey.generatePreshareKey();
* const { publicKey, privateKey: _privateKey } =
* WireguardKey.generateKeyPair();
*
* const peer = Schema.decode(WireguardPeer.WireguardPeer)({
* PublicKey: publicKey,
* PresharedKey: preshareKey,
* AllowedIPs: new Set(["192.168.0.0/24"]),
* Endpoint: "192.168.0.1:51820",
* PersistentKeepalive: 20,
* });
*
* const iniPeer = Function.pipe(
* peer,
* Effect.flatMap(Schema.encode(WireguardPeer.WireguardPeer)),
* Effect.flatMap(Schema.decode(WireguardPeer.WireguardIniPeer))
* );
*
* @see {@link WireguardPeer}
*/
export class WireguardIniPeer extends Schema.transformOrFail(WireguardPeer, Schema.String, {
// The types really help make sure that everything in the output will be in the right format
decode: (peer: WireguardPeer, _options, _ast) => {
const publicKey: `PublicKey = ${string & Brand.Brand<"WireguardKey">}\n` =
`PublicKey = ${peer.PublicKey}\n` as const;
const presharedKey: "" | `PresharedKey = ${string & Brand.Brand<"WireguardKey">}\n` = Function.pipe(
peer.PresharedKey,
Option.map((key) => `PresharedKey = ${key}\n` as const),
Option.getOrElse(() => "" as const)
);
const endpoint: "" | `Endpoint = ${string}:${InternetSchemas.PortBrand}\n` = Function.pipe(
peer.Endpoint,
Option.fromNullable,
Option.map((endpoint) => {
const host = "address" in endpoint ? endpoint.address.ip : endpoint.host;
return `Endpoint = ${host}:${endpoint.natPort}\n` as const;
}),
Option.getOrElse(() => "" as const)
);
const keepAlive: "" | `PersistentKeepalive = ${number}\n` = Function.pipe(
peer.PersistentKeepalive,
Option.map((keepalive) => `PersistentKeepalive = ${Duration.toSeconds(keepalive)}\n` as const),
Option.getOrElse(() => "" as const)
);
const aps: `AllowedIPs = ${string}` = Function.pipe(
peer.AllowedIPs,
Array.fromIterable,
Array.map((ap) => `${ap.address.ip}/${ap.mask}` as const),
Array.map((ap) => `${ap}` as const),
Array.join(", "),
(x) => `AllowedIPs = ${x}` as const
);
return ParseResult.succeed(`[Peer]\n${publicKey}${presharedKey}${endpoint}${keepAlive}${aps}\n` as const);
},
// Encoding is trivial using the ini library
encode: (iniPeer: string, _options, _ast) =>
Function.pipe(
iniPeer,
ini.decode,
(data) =>
data as {
PublicKey: string;
PresharedKey?: string | undefined | null;
PersistentKeepalive?: string | undefined | null;
Endpoint?: InternetSchemas.EndpointEncoded | undefined | null;
AllowedIPs?:
| Array<InternetSchemas.CidrBlockFromStringEncoded>
| InternetSchemas.CidrBlockFromStringEncoded
| undefined
| null;
},
({ AllowedIPs, Endpoint, PersistentKeepalive, PresharedKey, PublicKey }) => ({
PublicKey,
PresharedKey,
Endpoint,
PersistentKeepalive: Function.pipe(
PersistentKeepalive,
Option.fromNullable,
Option.flatMap(Number.parse),
Option.getOrUndefined
),
AllowedIPs: Function.pipe(
AllowedIPs,
Option.fromNullable,
Option.map((aps) => new Set(Array.isArray(aps) ? aps : [aps])),
Option.getOrUndefined
),
}),
Schema.decode(WireguardPeer, { onExcessProperty: "error" }),
Effect.mapError(({ issue }) => issue)
),
}).annotations({
identifier: "WireguardIniPeer",
description: "A wireguard ini peer configuration",
}) {}
/**
* @since 1.0.0
* @category Schemas
* @see https://www.wireguard.com/xplatform/
*/
export class WireguardUapiSetPeer extends Schema.transformOrFail(WireguardPeer, Schema.String, {
encode: (uapiPeer: string, _options, ast) =>
ParseResult.fail(new ParseResult.Forbidden(ast, uapiPeer, "Can not encode a UAPI set peer")),
decode: (peer: WireguardPeer, _options, _ast) => {
const publicKey: `public_key=${string}\n` = Function.pipe(
peer.PublicKey,
(key) => Buffer.from(key, "base64").toString("hex"),
(hex) => `public_key=${hex}\n` as const
);
const presharedKeyHex: "" | `preshared_key=${string}\n` = Function.pipe(
peer.PresharedKey,
Option.map((key) => Buffer.from(key, "base64").toString("hex")),
Option.map((hex) => `preshared_key=${hex}\n` as const),
Option.getOrElse(() => "" as const)
);
const endpoint: "" | `endpoint=${string}:${InternetSchemas.PortBrand}\n` = Function.pipe(
peer.Endpoint,
Option.fromNullable,
Option.map((endpoint) => {
const host = "address" in endpoint ? endpoint.address.ip : endpoint.host;
return `endpoint=${host}:${endpoint.natPort}\n` as const;
}),
Option.getOrElse(() => "" as const)
);
const keepAlive: "" | `persistent_keepalive_interval=${number}\n` = Function.pipe(
peer.PersistentKeepalive,
Option.map(Duration.toSeconds),
Option.map((seconds) => `persistent_keepalive_interval=${seconds}\n` as const),
Option.getOrElse(() => "" as const)
);
const aps: Array<`allowed_ip=${string}/${number}`> = Function.pipe(
peer.AllowedIPs,
Array.fromIterable,
Array.map((ap) => `${ap.address.ip}/${ap.mask}` as const),
Array.map((ap) => `allowed_ip=${ap}` as const)
);
return ParseResult.succeed(`${publicKey}${presharedKeyHex}${endpoint}${keepAlive}${aps.join("\n")}\n`);
},
}).annotations({
identifier: "WireguardUapiSetPeer",
description: "A wireguard uapi peer configuration",
}) {}
/**
* @since 1.0.0
* @category Schemas
* @see https://www.wireguard.com/xplatform/
*/
export class WireguardUapiGetPeer extends Schema.transformOrFail(Schema.String, WireguardPeer["uapi"], {
encode: (uapiPeer, _options, ast) =>
ParseResult.fail(new ParseResult.Forbidden(ast, uapiPeer, "Can not decode a UAPI get peer")),
decode: (uapiPeer: string) => {
const data = ini.decode(uapiPeer);
const {
allowed_ip,
endpoint,
last_handshake_time_sec,
persistent_keepalive_interval,
preshared_key,
public_key,
rx_bytes,
tx_bytes,
} = data as {
allowed_ip:
| InternetSchemas.CidrBlockFromStringEncoded
| Array<InternetSchemas.CidrBlockFromStringEncoded>
| undefined
| null;
endpoint: InternetSchemas.EndpointEncoded | undefined | null;
last_handshake_time_sec: string;
persistent_keepalive_interval: string | undefined | null;
preshared_key: string | undefined | null;
public_key: string;
rx_bytes: string;
tx_bytes: string;
};
const publicKey = Buffer.from(public_key, "hex").toString("base64");
const presharedKey = Function.pipe(
preshared_key,
Option.fromNullable,
Option.map((hex) => Buffer.from(hex, "hex").toString("base64")),
Option.getOrUndefined
);
const allowedIps = Function.pipe(
allowed_ip,
Option.fromNullable,
Option.map((aps) => new Set(Array.isArray(aps) ? aps : [aps])),
Option.getOrUndefined
);
const keepAlive = Function.pipe(
persistent_keepalive_interval,
Option.fromNullable,
Option.flatMap(Number.parse),
Option.getOrUndefined
);
return ParseResult.succeed({
rxBytes: rx_bytes,
txBytes: tx_bytes,
Endpoint: endpoint,
PublicKey: publicKey,
PresharedKey: presharedKey,
lastHandshake: last_handshake_time_sec,
AllowedIPs: allowedIps,
PersistentKeepalive: keepAlive,
});
},
}).annotations({
identifier: "WireguardUapiGetPeer",
description: "A wireguard uapi peer configuration",
}) {}
/**
* @since 1.0.0
* @category Refinements
*/
export const hasBidirectionalTraffic = (
wireguardPeer: Schema.Schema.Type<(typeof WireguardPeer)["uapi"]>
): Effect.Effect<boolean, never, never> => Effect.succeed(wireguardPeer.rxBytes > 0 && wireguardPeer.txBytes > 0);
/**
* @since 1.0.0
* @category Refinements
*/
export const hasHandshakedRecently = (
wireguardPeer: Schema.Schema.Type<(typeof WireguardPeer)["uapi"]>
): Effect.Effect<boolean, never, never> =>
Effect.Do.pipe(
Effect.bind("now", () => DateTime.now),
Effect.let("threshold", () => Duration.lessThanOrEqualTo(Duration.seconds(30))),
Effect.map(({ now, threshold }) => threshold(DateTime.distanceDuration(wireguardPeer.lastHandshake, now)))
);