From 19b525d1e65e4dc2afd243cdb9088f32658139b9 Mon Sep 17 00:00:00 2001 From: Shahrad Elahi Date: Mon, 25 Sep 2023 01:16:49 +0330 Subject: [PATCH] adds features for adding/removing peers and a few minor changes. --- src/lib/typings.ts | 17 +-- src/lib/wireguard.ts | 196 +++++++++++++++++++++++++++- src/pages/api/auth/[...nextauth].ts | 23 ++-- src/pages/index.tsx | 4 +- 4 files changed, 212 insertions(+), 28 deletions(-) diff --git a/src/lib/typings.ts b/src/lib/typings.ts index ee92a0c..98edbd7 100644 --- a/src/lib/typings.ts +++ b/src/lib/typings.ts @@ -10,14 +10,6 @@ export const WgKeySchema = z.object({ export type WgKey = z.infer -export interface WgPeerConfig { - publicKey: string - preSharedKey: string - endpoint: string - allowedIps: string[] - persistentKeepalive: number | null -} - const WgPeerSchema = z.object({ id: z.string().uuid(), name: z.string().regex(/^[A-Za-z\d\s]{3,32}$/), @@ -39,7 +31,7 @@ export type WgPeer = z.infer export const WgServerSchema = z.object({ id: z.string().uuid(), confId: z.number(), - type: z.enum([ 'default', 'bridge', 'tor' ]), + type: z.enum([ 'direct', 'bridge', 'tor' ]), name: z.string().regex(/^[A-Za-z\d\s]{3,32}$/), address: z.string().regex(IPV4_REGEX), listen: z.number(), @@ -48,7 +40,12 @@ export const WgServerSchema = z.object({ preDown: z.string().nullable(), postDown: z.string().nullable(), dns: z.string().regex(IPV4_REGEX).nullable(), - peers: z.array(WgPeerSchema), + peers: z.array(z.object({ + publicKey: z.string(), + preSharedKey: z.string().nullable(), + allowedIps: z.string().regex(IPV4_REGEX), + persistentKeepalive: z.number().nullable(), + })), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), status: z.enum([ 'up', 'down' ]), diff --git a/src/lib/wireguard.ts b/src/lib/wireguard.ts index 2cb3685..737d3c8 100644 --- a/src/lib/wireguard.ts +++ b/src/lib/wireguard.ts @@ -2,7 +2,7 @@ import { promises as fs } from "fs"; import path from "path"; import { WG_PATH } from "@lib/constants"; import Shell from "@lib/shell"; -import { WgKey, WgPeer, WgServer } from "@lib/typings"; +import { WgKey, WgServer } from "@lib/typings"; import { client, WG_SEVER_PATH } from "@lib/redis"; import { isJson } from "@lib/utils"; import deepmerge from "deepmerge"; @@ -67,6 +67,176 @@ export class WGServer { return res === 'OK' } + static async findAttachedUuid(confId: number): Promise { + const server = await getServers() + return server.find((s) => s.confId === confId)?.id + } + + static async addPeer(id: string, peer: WgServer['peers'][0]): Promise { + const server = await findServer(id) + if (!server) { + console.error('server could not be updated (reason: not exists)') + return false + } + const peerLines = [ + `[Peer]`, + `PublicKey = ${peer.publicKey}`, + `AllowedIPs = ${peer.allowedIps}/32` + ] + if (peer.persistentKeepalive) { + peerLines.push(`PersistentKeepalive = ${peer.persistentKeepalive}`) + } + if (peer.preSharedKey) { + peerLines.push(`PresharedKey = ${peer.preSharedKey}`) + } + const confPath = path.join(WG_PATH, `wg${server.confId}.conf`) + const conf = await fs.readFile(confPath, 'utf-8') + const lines = conf.split('\n') + lines.push(...peerLines) + await fs.writeFile(confPath, lines.join('\n')) + await WGServer.update(server.id, { + peers: [ ...server.peers, peer ] + }) + await WGServer.stop(server.id) + await WGServer.start(server.id) + return true + } + + static async removePeer(id: string, publicKey: string): Promise { + const server = await findServer(id) + if (!server) { + console.error('server could not be updated (reason: not exists)') + return false + } + const peers = await wgPeersStr(server.confId) + const peerIndex = peers.findIndex((p) => p + .replace(/\s/g, '') + .includes(`PublicKey=${publicKey}`) + ) + if (peerIndex === -1) { + console.warn('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]') ? + conf.split('[Peer]')[0] : + 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 + } + +} + +/** + * This function is for checking out WireGuard server is running + */ +async function wgCheckout(configId: number): Promise { + const res = await Shell.exec(`ip link show | grep wg${configId}`, true) + return res.includes(`wg${configId}`) +} + +export async function readWgConf(configId: number): Promise { + const confPath = path.join(WG_PATH, `wg${configId}.conf`) + const conf = await fs.readFile(confPath, 'utf-8') + const lines = conf.split('\n') + const server: WgServer = { + id: crypto.randomUUID(), + confId: configId, + type: 'direct', + name: '', + address: '', + listen: 0, + dns: null, + privateKey: '', + publicKey: '', + preUp: null, + preDown: null, + postDown: null, + postUp: null, + peers: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'down' + } + let reachedPeers = false + for (const line of lines) { + const [ key, value ] = line.split('=').map((s) => s.trim()) + if (reachedPeers) { + if (key === '[Peer]') { + server.peers.push({ + publicKey: '', + preSharedKey: null, + allowedIps: '', + persistentKeepalive: null + }) + } + if (key === 'PublicKey') { + server.peers[server.peers.length - 1].publicKey = value + } + if (key === 'PresharedKey') { + server.peers[server.peers.length - 1].preSharedKey = value + } + if (key === 'AllowedIPs') { + server.peers[server.peers.length - 1].allowedIps = value + } + if (key === 'PersistentKeepalive') { + server.peers[server.peers.length - 1].persistentKeepalive = parseInt(value) + } + } + if (key === 'PrivateKey') { + server.privateKey = value + } + if (key === 'Address') { + server.address = value + } + if (key === 'ListenPort') { + server.listen = parseInt(value) + } + if (key === 'DNS') { + server.dns = value + } + if (key === 'PreUp') { + server.preUp = value + } + if (key === 'PreDown') { + server.preDown = value + } + if (key === 'PostUp') { + server.postUp = value + } + if (key === 'PostDown') { + server.postDown = value + } + if (key === 'PublicKey') { + server.publicKey = value + } + if (key === '[Peer]') { + reachedPeers = true + } + } + server.status = await wgCheckout(configId) ? 'up' : 'down' + return server +} + +/** + * This function checks if a WireGuard config exists in file system + * @param configId + */ +async function wgConfExists(configId: number): Promise { + const confPath = path.join(WG_PATH, `wg${configId}.conf`) + try { + await fs.access(confPath) + return true + } catch (e) { + return false + } } /** @@ -74,7 +244,25 @@ export class WGServer { * redis server. */ async function syncServers(): Promise { - throw new Error('Yet not implanted!'); + // get files in /etc/wireguard + const files = await fs.readdir(WG_PATH) + // filter files that start with wg and end with .conf + const reg = new RegExp(/^wg(\d+)\.conf$/) + const confs = files.filter((f) => reg.test(f)) + // read all confs + const servers = await Promise.all(confs.map((f) => readWgConf(parseInt(f.match(reg)![1])))) + // remove old servers + await client.del(WG_SEVER_PATH) + // save all servers to redis + await client.lpush(WG_SEVER_PATH, ...servers.map((s) => JSON.stringify(s))) + return true +} + +async function wgPeersStr(configId: number): Promise { + const confPath = path.join(WG_PATH, `wg${configId}.conf`) + const conf = await fs.readFile(confPath, 'utf-8') + const rawPeers = conf.split('[Peer]') + return rawPeers.slice(1).map((p) => `[Peer]\n${p}`) } export async function generateWgKey(): Promise { @@ -216,12 +404,12 @@ ${server.peers.map(getPeerConf).join('\n')} ` } -export function getPeerConf(peer: WgPeer): string { +export function getPeerConf(peer: WgServer['peers'][0]): string { return ` [Peer] PublicKey = ${peer.publicKey} ${peer.preSharedKey ? `PresharedKey = ${peer.preSharedKey}` : ''} -AllowedIPs = ${peer.address}/32 +AllowedIPs = ${peer.allowedIps}/32 ${peer.persistentKeepalive ? `PersistentKeepalive = ${peer.persistentKeepalive}` : ''} ` } diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index 5b2d60a..165243b 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -8,20 +8,19 @@ export default NextAuth({ // get user and password from .env.local // https://next-auth.js.org/configuration/providers#credentials-based-authentication-providers CredentialsProvider({ - async authorize(credentials, request) { - const { HASHED_PASSWORD } = process.env - if (!HASHED_PASSWORD) { - return { - - } - } - const { password } = request.query || {} - if (!password ) { - return null - } - }, name: 'Credentials', credentials: {}, + async authorize(_, request) { + const { HASHED_PASSWORD } = process.env + if (!HASHED_PASSWORD) { + return { id: crypto.randomUUID() } + } + const { password } = request.query || {} + if (!password) { + return null + } + return null + } }), ] diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6bc5884..d2e69ef 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Badge, Button, Card, List } from "antd"; +import { Button, Card, List } from "antd"; import BasePage from "@ui/pages/BasePage"; import { APIResponse, WgServer } from "@lib/typings"; import { PlusOutlined } from "@ant-design/icons"; @@ -105,7 +105,7 @@ function ServerIcon(props: ServerIconProps) { width={34} height={34} /> - {props.type !== 'default' && ( + {props.type !== 'direct' && (
{props.type === 'tor' && (