From 4e4449435d7673dc510f7353db1bca42b97be082 Mon Sep 17 00:00:00 2001 From: Shahrad Elahi Date: Mon, 25 Sep 2023 15:27:43 +0330 Subject: [PATCH] updates `WireGuard` lib for creating peers --- src/lib/schemas/WireGuard.ts | 5 +- src/lib/typings.ts | 22 +++++--- src/lib/wireguard-utils.ts | 55 ++++++++++++++++++++ src/lib/wireguard.ts | 99 ++++++++++++++++++++++-------------- 4 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 src/lib/wireguard-utils.ts diff --git a/src/lib/schemas/WireGuard.ts b/src/lib/schemas/WireGuard.ts index 2b62188..55aa865 100644 --- a/src/lib/schemas/WireGuard.ts +++ b/src/lib/schemas/WireGuard.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { IPV4_REGEX } from "@lib/constants"; import { isBetween, isPrivateIP } from "@lib/utils"; -import { ZodErrorMap } from "zod/lib/ZodError"; export const NameSchema = z .string() @@ -50,6 +49,10 @@ export const ServerId = z .string() .uuid({ message: 'Server ID must be a valid UUID' }) +export const ClientId = z + .string() + .uuid({ message: 'Client ID must be a valid UUID' }) + export const ServerStatusSchema = z .enum([ 'up', 'down' ], { errorMap: issue => { diff --git a/src/lib/typings.ts b/src/lib/typings.ts index 98edbd7..b24c4cb 100644 --- a/src/lib/typings.ts +++ b/src/lib/typings.ts @@ -2,10 +2,12 @@ import { z } from "zod"; import type React from "react"; import { IPV4_REGEX } from "@lib/constants"; import { NextApiRequest as TNextApiRequest } from "next/dist/shared/lib/utils"; +import { NameSchema } from "@lib/schemas/WireGuard"; export const WgKeySchema = z.object({ privateKey: z.string(), publicKey: z.string(), + preSharedKey: z.string(), }) export type WgKey = z.infer @@ -32,7 +34,7 @@ export const WgServerSchema = z.object({ id: z.string().uuid(), confId: z.number(), type: z.enum([ 'direct', 'bridge', 'tor' ]), - name: z.string().regex(/^[A-Za-z\d\s]{3,32}$/), + name: NameSchema, address: z.string().regex(IPV4_REGEX), listen: z.number(), preUp: z.string().nullable(), @@ -40,17 +42,21 @@ export const WgServerSchema = z.object({ preDown: z.string().nullable(), postDown: z.string().nullable(), dns: z.string().regex(IPV4_REGEX).nullable(), - peers: z.array(z.object({ - publicKey: z.string(), - preSharedKey: z.string().nullable(), - allowedIps: z.string().regex(IPV4_REGEX), - persistentKeepalive: z.number().nullable(), - })), + peers: z.array( + z.object({ + id: z.string().uuid(), + name: NameSchema, + preSharedKey: z.string().nullable(), + allowedIps: z.string().regex(IPV4_REGEX), + persistentKeepalive: z.number().nullable(), + }) + .merge(WgKeySchema) + ), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), status: z.enum([ 'up', 'down' ]), }) - .merge(WgKeySchema) + .merge(WgKeySchema.omit({ preSharedKey: true })) export type WgServer = z.infer diff --git a/src/lib/wireguard-utils.ts b/src/lib/wireguard-utils.ts new file mode 100644 index 0000000..6bbd91a --- /dev/null +++ b/src/lib/wireguard-utils.ts @@ -0,0 +1,55 @@ +import { WgServer } from "@lib/typings"; + +export function getServerConf(server: WgServer): string { + const lines = [ + '# Autogenerated by WireGuard UI (WireAdmin)', + '[Interface]', + `PrivateKey = ${server.privateKey}`, + `Address = ${server.address}/24`, + `ListenPort = ${server.listen}`, + `${server.dns ? `DNS = ${server.dns}` : 'OMIT'}`, + '', + `${server.preUp ? `PreUp = ${server.preUp}` : 'OMIT'}`, + `${server.postUp ? `PostUp = ${server.postUp}` : 'OMIT'}`, + `${server.preDown ? `PreDown = ${server.preDown}` : 'OMIT'}`, + `${server.postDown ? `PostDown = ${server.postDown}` : 'OMIT'}`, + ...server.peers.map((peer, index) => ([ + '', + `## ${peer.name || `Peer #${index + 1}`}`, + '[Peer]', + `PublicKey = ${peer.publicKey}`, + `${peer.preSharedKey ? `PresharedKey = ${peer.preSharedKey}` : 'OMIT'}`, + `AllowedIPs = ${peer.allowedIps}/32`, + `${peer.persistentKeepalive ? `PersistentKeepalive = ${peer.persistentKeepalive}` : 'OMIT'}` + ])) + ] + return lines + .filter((l) => l !== 'OMIT') + .join('\n') +} + +type Peer = WgServer['peers'][0] + +interface GenPeerConParams extends Peer { + serverAddress?: string + port: number +} + +export function getPeerConf(params: GenPeerConParams): string { + const lines = [ + '# Autogenerated by WireGuard UI (WireAdmin)', + '[Interface]', + `PrivateKey = ${params.privateKey}`, + `Address = ${params.allowedIps}/32`, + '', + '[Peer]', + `PublicKey = ${params.publicKey}`, + `${params.preSharedKey ? `PresharedKey = ${params.preSharedKey}` : 'OMIT'}`, + `AllowedIPs = ${params.allowedIps}/32`, + `PersistentKeepalive = ${params.persistentKeepalive}`, + `Endpoint = ${params.serverAddress || process.env.NEXT_PUBLIC_WG_HOST}:${params.port}`, + ] + return lines + .filter((l) => l !== 'OMIT') + .join('\n') +} \ No newline at end of file diff --git a/src/lib/wireguard.ts b/src/lib/wireguard.ts index 737d3c8..e8494c1 100644 --- a/src/lib/wireguard.ts +++ b/src/lib/wireguard.ts @@ -6,6 +6,7 @@ import { WgKey, WgServer } from "@lib/typings"; import { client, WG_SEVER_PATH } from "@lib/redis"; import { isJson } from "@lib/utils"; import deepmerge from "deepmerge"; +import { getPeerConf, getServerConf } from "@lib/wireguard-utils"; export class WGServer { @@ -63,7 +64,10 @@ export class WGServer { console.warn('findServerIndex: index not found') return true } - const res = await client.lset(WG_SEVER_PATH, index, JSON.stringify(deepmerge(server, update))) + const res = await client.lset(WG_SEVER_PATH, index, JSON.stringify({ + ...deepmerge(server, update), + updatedAt: new Date().toISOString() + })) return res === 'OK' } @@ -109,14 +113,23 @@ export class WGServer { return false } const peers = await wgPeersStr(server.confId) - const peerIndex = peers.findIndex((p) => p - .replace(/\s/g, '') - .includes(`PublicKey=${publicKey}`) - ) + + const index = await findServerIndex(id) + if (typeof index !== 'number') { + console.warn('findServerIndex: index not found') + return true + } + await client.lset(WG_SEVER_PATH, index, JSON.stringify({ + ...server, + peers: server.peers.filter((p) => p.publicKey !== publicKey) + })) + + const peerIndex = peers.findIndex((p) => p.includes(`PublicKey = ${publicKey}`)) if (peerIndex === -1) { - console.warn('no peer found') + console.warn('removePeer: no peer found') return false } + const confPath = path.join(WG_PATH, `wg${server.confId}.conf`) const conf = await fs.readFile(confPath, 'utf-8') const serverConfStr = conf.includes('[Peer]') ? @@ -124,14 +137,46 @@ export class WGServer { conf const peersStr = peers.filter((_, i) => i !== peerIndex).join('\n') await fs.writeFile(confPath, `${serverConfStr}\n${peersStr}`) - await WGServer.update(server.id, { - peers: server.peers.filter((_, i) => i !== peerIndex) - }) + await WGServer.stop(server.id) await WGServer.start(server.id) + return true } + static async getFreePeerIp(id: string): Promise { + const server = await findServer(id) + if (!server) { + console.error('getFreePeerIp: server not found') + return undefined + } + const reservedIps = server.peers.map((p) => p.allowedIps) + const ips = reservedIps.map((ip) => ip.split('/')[0]) + const net = server.address.split('/')[0].split('.') + for (let i = 1; i < 255; i++) { + const ip = `${net[0]}.${net[1]}.${net[2]}.${i}` + if (!ips.includes(ip) && ip !== server.address.split('/')[0]) { + return ip + } + } + console.error('getFreePeerIp: no free ip found') + return undefined + } + + static async generatePeerConfig(id: string, peerId: string): Promise { + const server = await findServer(id) + if (!server) { + console.error('generatePeerConfig: server not found') + return undefined + } + const peer = server.peers.find((p) => p.id === peerId) + if (!peer) { + console.error('generatePeerConfig: peer not found') + return undefined + } + return getPeerConf({ ...peer, port: server.listen }) + } + } /** @@ -171,8 +216,11 @@ export async function readWgConf(configId: number): Promise { if (reachedPeers) { if (key === '[Peer]') { server.peers.push({ + id: crypto.randomUUID(), + name: `Unknown #${server.peers.length + 1}`, publicKey: '', - preSharedKey: null, + privateKey: '', // it's okay to be empty because, we not using it on server + preSharedKey: '', allowedIps: '', persistentKeepalive: null }) @@ -268,7 +316,8 @@ async function wgPeersStr(configId: number): Promise { export async function generateWgKey(): Promise { const privateKey = await Shell.exec('wg genkey'); const publicKey = await Shell.exec(`echo ${privateKey} | wg pubkey`); - return { privateKey, publicKey } + const preSharedKey = await Shell.exec('wg genkey'); + return { privateKey, publicKey, preSharedKey } } export async function generateWgServer(config: { @@ -386,34 +435,6 @@ export async function dropInterface(configId: number) { await Shell.exec(`ip link delete dev wg${configId}`, true) } -export function getServerConf(server: WgServer): string { - return ` -# Autogenerated by WireGuard UI (WireAdmin) -[Interface] -PrivateKey = ${server.privateKey} -Address = ${server.address}/24 -ListenPort = ${server.listen} -${server.dns ? `DNS = ${server.dns}` : ''} - -PreUp = ${server.preUp} -PostUp = ${server.postUp} -PreDown = ${server.preDown} -PostDown = ${server.postDown} - -${server.peers.map(getPeerConf).join('\n')} - ` -} - -export function getPeerConf(peer: WgServer['peers'][0]): string { - return ` -[Peer] -PublicKey = ${peer.publicKey} -${peer.preSharedKey ? `PresharedKey = ${peer.preSharedKey}` : ''} -AllowedIPs = ${peer.allowedIps}/32 -${peer.persistentKeepalive ? `PersistentKeepalive = ${peer.persistentKeepalive}` : ''} - ` -} - export async function maxConfId(): Promise { // get files in /etc/wireguard const files = await fs.readdir(WG_PATH)