adds features for adding/removing peers and a few minor changes.

This commit is contained in:
Shahrad Elahi 2023-09-25 01:16:49 +03:30
parent e79f52da9f
commit 19b525d1e6
4 changed files with 212 additions and 28 deletions

View File

@ -10,14 +10,6 @@ export const WgKeySchema = z.object({
export type WgKey = z.infer<typeof WgKeySchema>
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<typeof WgPeerSchema>
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' ]),

View File

@ -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<string | undefined> {
const server = await getServers()
return server.find((s) => s.confId === confId)?.id
}
static async addPeer(id: string, peer: WgServer['peers'][0]): Promise<boolean> {
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<boolean> {
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<boolean> {
const res = await Shell.exec(`ip link show | grep wg${configId}`, true)
return res.includes(`wg${configId}`)
}
export async function readWgConf(configId: number): Promise<WgServer> {
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<boolean> {
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<boolean> {
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<string[]> {
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<WgKey> {
@ -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}` : ''}
`
}

View File

@ -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
}
}),
]

View File

@ -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' && (
<div className={'absolute -bottom-1 -right-2 rounded-full bg-white'}>
{props.type === 'tor' && (
<Image