From e1bf4f381bd1a277f2100529d566983c247d8d38 Mon Sep 17 00:00:00 2001 From: Bernd Storath <32197462+kaaax0815@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:05:26 +0200 Subject: [PATCH] Feat: Server AllowedIPs, MTU (#1356) * add wireguard helpers * improve wireguard helpers * add server mtu * fix wg0.conf formatting * add ipv6 support to docker compose and readme --- README.md | 30 +++-- docker-compose.yml | 14 ++ src/server/utils/WireGuard.ts | 135 +++++++------------ src/server/utils/wgHelper.ts | 131 ++++++++++++++++++ src/services/database/migrations/1.ts | 10 +- src/services/database/repositories/client.ts | 1 + src/services/database/repositories/system.ts | 1 + 7 files changed, 220 insertions(+), 102 deletions(-) create mode 100644 src/server/utils/wgHelper.ts diff --git a/README.md b/README.md index 91089f7e..ea951bce 100644 --- a/README.md +++ b/README.md @@ -65,25 +65,39 @@ And log in again. ### 2. Run WireGuard Easy +To setup the IPv6 Network, simply run once: + +```bash + docker network create \ + -d bridge --ipv6 \ + -d default \ + --subnet 10.42.42.0/24 \ + --subnet fdcc:ad94:bacf:61a3::/64 wg \ +``` + To automatically install & run wg-easy, simply run: ```bash docker run -d \ - --name=wg-easy \ + --net wg \ -e PORT=51821 \ + --name wg-easy \ + --ip6 fdcc:ad94:bacf:61a3::2a \ + --ip 10.42.42.42 \ -v ~/.wg-easy:/etc/wireguard \ -p 51820:51820/udp \ -p 51821:51821/tcp \ - --cap-add=NET_ADMIN \ - --cap-add=SYS_MODULE \ - --sysctl="net.ipv4.conf.all.src_valid_mark=1" \ - --sysctl="net.ipv4.ip_forward=1" \ + --cap-add NET_ADMIN \ + --cap-add SYS_MODULE \ + --sysctl net.ipv4.ip_forward=1 \ + --sysctl net.ipv4.conf.all.src_valid_mark=1 \ + --sysctl net.ipv6.conf.all.disable_ipv6=0 \ + --sysctl net.ipv6.conf.all.forwarding=1 \ + --sysctl net.ipv6.conf.default.forwarding=1 \ --restart unless-stopped \ ghcr.io/wg-easy/wg-easy ``` -> 💡 Replace `<🚨YOUR_SERVER_IP>` with your WAN IP, or a Dynamic DNS hostname. - The Web UI will now be available on `http://0.0.0.0:51821`. The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) @@ -92,7 +106,7 @@ The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. WireGuard Easy can be launched with Docker Compose as well - just download [`docker-compose.yml`](docker-compose.yml), make necessary adjustments and -execute `docker compose up --detach`. +execute `docker compose up -d`. ### 3. Sponsor diff --git a/docker-compose.yml b/docker-compose.yml index c5a6b346..c1e00063 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,10 @@ services: image: ghcr.io/wg-easy/wg-easy container_name: wg-easy + networks: + wg: + ipv4_address: 10.42.42.42 + ipv6_address: fdcc:ad94:bacf:61a3::2a volumes: - etc_wireguard:/etc/wireguard ports: @@ -27,3 +31,13 @@ services: - net.ipv6.conf.all.disable_ipv6=0 - net.ipv6.conf.all.forwarding=1 - net.ipv6.conf.default.forwarding=1 + +networks: + wg: + driver: bridge + enable_ipv6: true + ipam: + driver: default + config: + - subnet: 10.42.42.0/24 + - subnet: fdcc:ad94:bacf:61a3::/64 diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts index 99093c0d..55e56b0e 100644 --- a/src/server/utils/WireGuard.ts +++ b/src/server/utils/WireGuard.ts @@ -21,37 +21,18 @@ class WireGuard { async #saveWireguardConfig() { const system = await Database.getSystem(); const clients = await Database.getClients(); - const cidr4Block = parseCidr(system.userConfig.address4Range).prefix; - const cidr6Block = parseCidr(system.userConfig.address6Range).prefix; - let result = ` -# Note: Do not edit this file directly. -# Your changes will be overwritten! - -# Server -[Interface] -PrivateKey = ${system.interface.privateKey} -Address = ${system.interface.address4}/${cidr4Block}, ${system.interface.address6}/${cidr6Block} -ListenPort = ${system.wgPort} -PreUp = ${system.iptables.PreUp} -PostUp = ${system.iptables.PostUp} -PreDown = ${system.iptables.PreDown} -PostDown = ${system.iptables.PostDown} -`; - - for (const [clientId, client] of Object.entries(clients)) { - if (!client.enabled) continue; - - result += ` - -# Client: ${client.name} (${clientId}) -[Peer] -PublicKey = ${client.publicKey} -PresharedKey = ${client.preSharedKey} -AllowedIPs = ${client.address4}/32, ${client.address6}/128`; + const result = []; + result.push(wg.generateServerInterface(system)); + + for (const client of Object.values(clients)) { + if (!client.enabled) { + continue; + } + result.push(wg.generateServerPeer(client)); } DEBUG('Config saving...'); - await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, { + await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result.join('\n\n'), { mode: 0o600, }); DEBUG('Config saved.'); @@ -59,7 +40,7 @@ AllowedIPs = ${client.address4}/32, ${client.address6}/128`; async #syncWireguardConfig() { DEBUG('Config syncing...'); - await exec('wg syncconf wg0 <(wg-quick strip wg0)'); + await wg.sync(); DEBUG('Config synced.'); } @@ -86,37 +67,28 @@ AllowedIPs = ${client.address4}/32, ${client.address6}/128`; })); // Loop WireGuard status - const dump = await exec('wg show wg0 dump', { - log: false, - }); - dump - .trim() - .split('\n') - .slice(1) - .forEach((line) => { - const [ - publicKey, - _preSharedKey, - endpoint, - _allowedIps, - latestHandshakeAt, - transferRx, - transferTx, - persistentKeepalive, - ] = line.split('\t'); - + const dump = await wg.dump(); + dump.forEach( + ({ + publicKey, + latestHandshakeAt, + endpoint, + transferRx, + transferTx, + persistentKeepalive, + }) => { const client = clients.find((client) => client.publicKey === publicKey); - if (!client) return; - - client.latestHandshakeAt = - latestHandshakeAt === '0' - ? null - : new Date(Number(`${latestHandshakeAt}000`)); - client.endpoint = endpoint === '(none)' ? null : (endpoint ?? null); - client.transferRx = Number(transferRx); - client.transferTx = Number(transferTx); - client.persistentKeepalive = persistentKeepalive ?? null; - }); + if (!client) { + return; + } + + client.latestHandshakeAt = latestHandshakeAt; + client.endpoint = endpoint; + client.transferRx = transferRx; + client.transferTx = transferTx; + client.persistentKeepalive = persistentKeepalive; + } + ); return clients; } @@ -136,22 +108,8 @@ AllowedIPs = ${client.address4}/32, ${client.address6}/128`; async getClientConfiguration({ clientId }: { clientId: string }) { const system = await Database.getSystem(); const client = await this.getClient({ clientId }); - const cidr4Block = parseCidr(system.userConfig.address4Range).prefix; - const cidr6Block = parseCidr(system.userConfig.address6Range).prefix; - - return ` -[Interface] -PrivateKey = ${client.privateKey} -Address = ${client.address4}/${cidr4Block}, ${client.address6}/${cidr6Block} -DNS = ${system.userConfig.defaultDns.join(', ')} -MTU = ${system.userConfig.mtu} - -[Peer] -PublicKey = ${system.interface.publicKey} -PresharedKey = ${client.preSharedKey} -AllowedIPs = ${client.allowedIPs.join(', ')} -PersistentKeepalive = ${client.persistentKeepalive} -Endpoint = ${system.wgHost}:${system.wgConfigPort}`; + + return wg.generateClientConfig(system, client); } async getClientQRCodeSVG({ clientId }: { clientId: string }) { @@ -172,11 +130,9 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`; const system = await Database.getSystem(); const clients = await Database.getClients(); - const privateKey = await exec('wg genkey'); - const publicKey = await exec(`echo ${privateKey} | wg pubkey`, { - log: 'echo ***hidden*** | wg pubkey', - }); - const preSharedKey = await exec('wg genpsk'); + const privateKey = await wg.generatePrivateKey(); + const publicKey = await wg.getPublicKey(privateKey); + const preSharedKey = await wg.generatePresharedKey(); // Calculate next IP const cidr4 = parseCidr(system.userConfig.address4Range); @@ -239,6 +195,7 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`; expiresAt: null, enabled: true, allowedIPs: system.userConfig.allowedIps, + serverAllowedIPs: null, persistentKeepalive: system.userConfig.persistentKeepalive, }; @@ -374,8 +331,8 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`; }); DEBUG('Starting Wireguard'); await this.#saveWireguardConfig(); - await exec('wg-quick down wg0').catch(() => {}); - await exec('wg-quick up wg0').catch((err) => { + await wg.down().catch(() => {}); + await wg.up().catch((err) => { if ( err && err.message && @@ -407,7 +364,7 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`; // Shutdown wireguard async Shutdown() { - await exec('wg-quick down wg0').catch(() => {}); + await wg.down().catch(() => {}); } async cronJob() { @@ -465,15 +422,15 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`; returnText += '\n# HELP wireguard_configured_peers\n'; returnText += '# TYPE wireguard_configured_peers gauge\n'; - returnText += `wireguard_configured_peers{interface="wg0"} ${Number(wireguardPeerCount)}\n`; + returnText += `wireguard_configured_peers{interface="wg0"} ${wireguardPeerCount}\n`; returnText += '\n# HELP wireguard_enabled_peers\n'; returnText += '# TYPE wireguard_enabled_peers gauge\n'; - returnText += `wireguard_enabled_peers{interface="wg0"} ${Number(wireguardEnabledPeersCount)}\n`; + returnText += `wireguard_enabled_peers{interface="wg0"} ${wireguardEnabledPeersCount}\n`; returnText += '\n# HELP wireguard_connected_peers\n'; returnText += '# TYPE wireguard_connected_peers gauge\n'; - returnText += `wireguard_connected_peers{interface="wg0"} ${Number(wireguardConnectedPeersCount)}\n`; + returnText += `wireguard_connected_peers{interface="wg0"} ${wireguardConnectedPeersCount}\n`; returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n'; returnText += '# TYPE wireguard_sent_bytes counter\n'; @@ -507,9 +464,9 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`; } } return { - wireguard_configured_peers: Number(wireguardPeerCount), - wireguard_enabled_peers: Number(wireguardEnabledPeersCount), - wireguard_connected_peers: Number(wireguardConnectedPeersCount), + wireguard_configured_peers: wireguardPeerCount, + wireguard_enabled_peers: wireguardEnabledPeersCount, + wireguard_connected_peers: wireguardConnectedPeersCount, }; } } diff --git a/src/server/utils/wgHelper.ts b/src/server/utils/wgHelper.ts new file mode 100644 index 00000000..2ec9eb57 --- /dev/null +++ b/src/server/utils/wgHelper.ts @@ -0,0 +1,131 @@ +import { parseCidr } from 'cidr-tools'; +import type { Client } from '~~/services/database/repositories/client'; +import type { System } from '~~/services/database/repositories/system'; + +export const wg = { + generateServerPeer: (client: Client) => { + const allowedIps = [ + `${client.address4}/32`, + `${client.address6}/128`, + ...(client.serverAllowedIPs ?? []), + ]; + + return `# Client: ${client.name} (${client.id}) +[Peer] +PublicKey = ${client.publicKey} +PresharedKey = ${client.preSharedKey} +AllowedIPs = ${allowedIps.join(', ')}`; + }, + + generateServerInterface: (system: System) => { + const cidr4Block = parseCidr(system.userConfig.address4Range).prefix; + const cidr6Block = parseCidr(system.userConfig.address6Range).prefix; + + return `# Note: Do not edit this file directly. +# Your changes will be overwritten! + +# Server +[Interface] +PrivateKey = ${system.interface.privateKey} +Address = ${system.interface.address4}/${cidr4Block}, ${system.interface.address6}/${cidr6Block} +ListenPort = ${system.wgPort} +MTU = ${system.userConfig.serverMtu} +PreUp = ${system.iptables.PreUp} +PostUp = ${system.iptables.PostUp} +PreDown = ${system.iptables.PreDown} +PostDown = ${system.iptables.PostDown}`; + }, + + generateClientConfig: (system: System, client: Client) => { + const cidr4Block = parseCidr(system.userConfig.address4Range).prefix; + const cidr6Block = parseCidr(system.userConfig.address6Range).prefix; + + return `[Interface] +PrivateKey = ${client.privateKey} +Address = ${client.address4}/${cidr4Block}, ${client.address6}/${cidr6Block} +DNS = ${system.userConfig.defaultDns.join(', ')} +MTU = ${system.userConfig.mtu} + +[Peer] +PublicKey = ${system.interface.publicKey} +PresharedKey = ${client.preSharedKey} +AllowedIPs = ${client.allowedIPs.join(', ')} +PersistentKeepalive = ${client.persistentKeepalive} +Endpoint = ${system.wgHost}:${system.wgConfigPort}`; + }, + + generatePrivateKey: () => { + return exec('wg genkey'); + }, + + getPublicKey: (privateKey: string) => { + return exec(`echo ${privateKey} | wg pubkey`, { + log: 'echo ***hidden*** | wg pubkey', + }); + }, + + generatePresharedKey: () => { + return exec('wg genpsk'); + }, + + up: () => { + return exec('wg-quick up wg0'); + }, + + down: () => { + return exec('wg-quick down wg0'); + }, + + sync: () => { + return exec('wg syncconf wg0 <(wg-quick strip wg0)'); + }, + + dump: async () => { + const rawDump = await exec('wg show wg0 dump', { + log: false, + }); + + type wgDumpLine = [ + string, + string, + string, + string, + string, + string, + string, + string, + ]; + + return rawDump + .trim() + .split('\n') + .slice(1) + .map((line) => { + const splitLines = line.split('\t'); + const [ + publicKey, + preSharedKey, + endpoint, + allowedIPs, + latestHandshakeAt, + transferRx, + transferTx, + persistentKeepalive, + ] = splitLines as wgDumpLine; + + return { + publicKey, + preSharedKey, + endpoint: endpoint === '(none)' ? null : endpoint, + allowedIPs, + latestHandshakeAt: + latestHandshakeAt === '0' + ? null + : new Date(Number.parseInt(`${latestHandshakeAt}000`)), + transferRx: Number.parseInt(transferRx), + transferTx: Number.parseInt(transferTx), + persistentKeepalive: persistentKeepalive, + }; + }); + }, +}; diff --git a/src/services/database/migrations/1.ts b/src/services/database/migrations/1.ts index 79da292b..cf930703 100644 --- a/src/services/database/migrations/1.ts +++ b/src/services/database/migrations/1.ts @@ -5,14 +5,14 @@ import { parseCidr } from 'cidr-tools'; import { stringifyIp } from 'ip-bigint'; export async function run1(db: Low) { - const privateKey = await exec('wg genkey'); - const publicKey = await exec(`echo ${privateKey} | wg pubkey`, { - log: 'echo ***hidden*** | wg pubkey', - }); + const privateKey = await wg.generatePrivateKey(); + const publicKey = await wg.getPublicKey(privateKey); + const address4Range = '10.8.0.0/24'; const address6Range = 'fdcc:ad94:bacf:61a4::cafe:0/112'; const cidr4 = parseCidr(address4Range); const cidr6 = parseCidr(address6Range); + const database: Database = { migrations: [], system: { @@ -26,6 +26,7 @@ export async function run1(db: Low) { lang: 'en', userConfig: { mtu: 1420, + serverMtu: 1420, persistentKeepalive: 0, address4Range: address4Range, address6Range: address6Range, @@ -70,7 +71,6 @@ export async function run1(db: Low) { clients: {}, }; - // TODO: use variables inside up/down script // TODO: properly check if ipv6 support database.system.iptables.PostUp = ` iptables -t nat -A POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE; diff --git a/src/services/database/repositories/client.ts b/src/services/database/repositories/client.ts index 3d641c23..4122c0ca 100644 --- a/src/services/database/repositories/client.ts +++ b/src/services/database/repositories/client.ts @@ -16,6 +16,7 @@ export type Client = { expiresAt: string | null; endpoint: string | null; allowedIPs: string[]; + serverAllowedIPs: string[] | null; oneTimeLink: OneTimeLink | null; /** ISO String */ createdAt: string; diff --git a/src/services/database/repositories/system.ts b/src/services/database/repositories/system.ts index aba8129a..6fae9c8f 100644 --- a/src/services/database/repositories/system.ts +++ b/src/services/database/repositories/system.ts @@ -18,6 +18,7 @@ export type WGInterface = { export type WGConfig = { mtu: number; + serverMtu: number; persistentKeepalive: number; address4Range: string; address6Range: string;