diff --git a/Dockerfile b/Dockerfile index b697b9d6..11dfabe2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -80,6 +80,9 @@ ENV HOST=0.0.0.0 ENV INSECURE=false ENV INIT_ENABLED=false ENV DISABLE_IPV6=false +ENV WG_INTERFACE_NAME=wg0 +ENV WG_PORT=51820 +ENV WG_STATE_DIR=/etc/wireguard LABEL org.opencontainers.image.source=https://github.com/wg-easy/wg-easy diff --git a/Dockerfile.dev b/Dockerfile.dev index 428e0368..c7da23e9 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -30,6 +30,9 @@ ENV HOST=0.0.0.0 ENV INSECURE=true ENV INIT_ENABLED=false ENV DISABLE_IPV6=false +ENV WG_INTERFACE_NAME=wg0 +ENV WG_PORT=51820 +ENV WG_STATE_DIR=/etc/wireguard # Install Dependencies COPY src/package.json src/pnpm-lock.yaml src/pnpm-workspace.yaml ./ diff --git a/README.md b/README.md index 7da64f43..3b4f0de4 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,49 @@ This is a quick start guide to get you up and running with WireGuard Easy. For a more detailed installation guide, please refer to the [Getting Started](https://wg-easy.github.io/wg-easy/latest/getting-started/) page. +### NixOS + +This repository exposes a NixOS module through `nixosModules.default`: + +```nix +{ + inputs.wg-easy.url = "github:/wg-easy-nix"; + + outputs = { nixpkgs, wg-easy, ... }: { + nixosConfigurations.server = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + wg-easy.nixosModules.default + { + services.wg-easy = { + enable = true; + interfaceName = "wg0"; + wireguardPort = 51820; + uiPort = 51821; + stateDir = "/var/lib/wg-easy"; + enableIPv6 = false; + defaultDns = [ "10.8.0.1" ]; + defaultAllowedIps = [ "10.8.0.1/32" ]; + defaultPersistentKeepalive = 25; + firewallEnabled = true; + defaultServerAllowedIps = [ ]; + defaultFirewallAllowedIps = [ "10.8.0.1/32" ]; + forceUpdateClients = false; + openFirewall = true; + }; + } + ]; + }; + }; +} +``` + +WireGuard's conventional default port is `51820`. Set `wireguardPort = 51280;` if you want that port instead. + +The `defaultDns`, `defaultAllowedIps`, `defaultPersistentKeepalive`, `defaultServerAllowedIps`, and `defaultFirewallAllowedIps` settings are stamped onto new clients when they are created. Existing clients keep their previous values unless `forceUpdateClients = true;` is set. + +`defaultAllowedIps` goes into generated client configs and controls what each client routes through the tunnel. `defaultServerAllowedIps` adds extra server-side peer routes for subnets behind a client; leave it empty for normal clients. `defaultFirewallAllowedIps` is the per-client destination allowlist used by wg-easy firewall filtering. Per-client firewall filtering currently applies to forwarded traffic, not local `INPUT` traffic to the server's own `10.8.0.1` address. + ### 1. Install Docker If you haven't installed Docker yet, install it by running as root: diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..ad7e9d6f --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1779508470, + "narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "29916453413845e54a65b8a1cf996842300cd299", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..c97e7e37 --- /dev/null +++ b/flake.nix @@ -0,0 +1,71 @@ +{ + description = "Nix package and NixOS module for wg-easy"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + linuxSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + formatterSystems = linuxSystems ++ [ + "aarch64-darwin" + "x86_64-darwin" + ]; + forAllSystems = nixpkgs.lib.genAttrs linuxSystems; + forFormatterSystems = nixpkgs.lib.genAttrs formatterSystems; + pkgsFor = system: import nixpkgs { inherit system; }; + in + { + packages = forAllSystems (system: { + wg-easy = (pkgsFor system).callPackage ./nix/package.nix { }; + default = self.packages.${system}.wg-easy; + }); + + checks = forAllSystems (system: { + package = self.packages.${system}.wg-easy; + }); + + devShells = forAllSystems ( + system: + let + pkgs = pkgsFor system; + in + { + default = pkgs.mkShell { + packages = with pkgs; [ + nodejs_24 + pnpm_11 + wireguard-tools + wireguard-go + iptables + nftables + ]; + }; + } + ); + + formatter = forFormatterSystems ( + system: + let + pkgs = pkgsFor system; + in + pkgs.writeShellApplication { + name = "wg-easy-nix-fmt"; + runtimeInputs = [ pkgs.nixfmt ]; + text = '' + if [ "$#" -eq 0 ]; then + set -- flake.nix nix/*.nix + fi + + exec nixfmt "$@" + ''; + } + ); + + nixosModules.default = import ./nix/module.nix; + nixosModules.wg-easy = self.nixosModules.default; + }; +} diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 00000000..8356d9b2 --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,254 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.wg-easy; + boolString = value: if value then "true" else "false"; + listString = values: lib.concatStringsSep "," values; + validListItems = + values: lib.all (value: value != "" && builtins.match ".*,.*" value == null) values; + interfaceNamePattern = "[A-Za-z0-9_.-]{1,15}"; +in +{ + options.services.wg-easy = { + enable = lib.mkEnableOption "wg-easy"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.callPackage ./package.nix { }; + defaultText = lib.literalExpression "pkgs.callPackage ./package.nix { }"; + description = "wg-easy package to run."; + }; + + interfaceName = lib.mkOption { + type = lib.types.str; + default = "wg0"; + description = "WireGuard interface name managed by wg-easy."; + }; + + wireguardPort = lib.mkOption { + type = lib.types.port; + default = 51820; + description = "UDP port used by the WireGuard interface."; + }; + + uiPort = lib.mkOption { + type = lib.types.port; + default = 51821; + description = "TCP port used by the wg-easy web UI."; + }; + + stateDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/wg-easy"; + description = "Writable directory for the SQLite database and generated WireGuard config."; + }; + + externalInterface = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Optional egress network interface used in wg-easy NAT hooks."; + }; + + enableIPv6 = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable IPv6 addresses and IPv6 firewall hooks in wg-easy."; + }; + + defaultDns = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "DNS servers to stamp onto new clients. Leave null to use wg-easy's global default."; + }; + + defaultAllowedIps = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "Allowed IPs to stamp onto new client configs. Leave null to use wg-easy's global default."; + }; + + defaultServerAllowedIps = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "Additional server-side peer AllowedIPs to stamp onto new clients."; + }; + + defaultFirewallAllowedIps = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "Firewall-enforced allowed destinations to stamp onto new clients."; + }; + + defaultPersistentKeepalive = lib.mkOption { + type = lib.types.nullOr (lib.types.ints.between 0 65535); + default = 25; + description = "Persistent keepalive to stamp onto new clients. Set null to preserve wg-easy's configured default."; + }; + + firewallEnabled = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Enable or disable wg-easy per-client firewall filtering. Leave null to preserve wg-easy's configured value."; + }; + + forceUpdateClients = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Overwrite existing clients with configured Nix defaults on service startup."; + }; + + insecure = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Serve the UI as insecure HTTP. Keep false when using HTTPS through a reverse proxy."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Open the WireGuard UDP port in the NixOS firewall."; + }; + + openWebUIFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Open the wg-easy web UI TCP port in the NixOS firewall."; + }; + + extraEnvironment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Additional environment variables for the wg-easy service."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = builtins.match interfaceNamePattern cfg.interfaceName != null; + message = "services.wg-easy.interfaceName must be 1-15 characters using only letters, numbers, '.', '_', or '-'."; + } + { + assertion = + cfg.externalInterface == null || builtins.match interfaceNamePattern cfg.externalInterface != null; + message = "services.wg-easy.externalInterface must be 1-15 characters using only letters, numbers, '.', '_', or '-'."; + } + { + assertion = lib.hasPrefix "/" cfg.stateDir; + message = "services.wg-easy.stateDir must be an absolute path."; + } + { + assertion = cfg.defaultDns == null || validListItems cfg.defaultDns; + message = "services.wg-easy.defaultDns entries must be non-empty and must not contain commas."; + } + { + assertion = + cfg.defaultAllowedIps == null + || (cfg.defaultAllowedIps != [ ] && validListItems cfg.defaultAllowedIps); + message = "services.wg-easy.defaultAllowedIps must be null or a non-empty list without commas."; + } + { + assertion = cfg.defaultServerAllowedIps == null || validListItems cfg.defaultServerAllowedIps; + message = "services.wg-easy.defaultServerAllowedIps entries must be non-empty and must not contain commas."; + } + { + assertion = + cfg.defaultFirewallAllowedIps == null + || (cfg.defaultFirewallAllowedIps != [ ] && validListItems cfg.defaultFirewallAllowedIps); + message = "services.wg-easy.defaultFirewallAllowedIps must be null or a non-empty list without commas."; + } + ]; + + boot.kernel.sysctl = { + "net.ipv4.ip_forward" = lib.mkDefault 1; + "net.ipv4.conf.all.src_valid_mark" = lib.mkDefault 1; + } + // lib.optionalAttrs cfg.enableIPv6 { + "net.ipv6.conf.all.disable_ipv6" = lib.mkDefault 0; + "net.ipv6.conf.all.forwarding" = lib.mkDefault 1; + "net.ipv6.conf.default.forwarding" = lib.mkDefault 1; + }; + + networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ cfg.wireguardPort ]; + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openWebUIFirewall [ cfg.uiPort ]; + + systemd.tmpfiles.rules = [ + "d ${cfg.stateDir} 0700 root root -" + ]; + + systemd.services.wg-easy = { + description = "WireGuard Easy"; + documentation = [ "https://wg-easy.github.io/wg-easy/latest/" ]; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + environment = { + DISABLE_IPV6 = boolString (!cfg.enableIPv6); + INSECURE = boolString cfg.insecure; + PORT = toString cfg.uiPort; + WG_FORCE_UPDATE_CLIENTS = boolString cfg.forceUpdateClients; + WG_INTERFACE_NAME = cfg.interfaceName; + WG_PORT = toString cfg.wireguardPort; + WG_STATE_DIR = cfg.stateDir; + } + // lib.optionalAttrs (cfg.defaultDns != null) { + WG_DEFAULT_DNS = listString cfg.defaultDns; + } + // lib.optionalAttrs (cfg.defaultAllowedIps != null) { + WG_DEFAULT_ALLOWED_IPS = listString cfg.defaultAllowedIps; + } + // lib.optionalAttrs (cfg.defaultServerAllowedIps != null) { + WG_DEFAULT_SERVER_ALLOWED_IPS = listString cfg.defaultServerAllowedIps; + } + // lib.optionalAttrs (cfg.defaultFirewallAllowedIps != null) { + WG_DEFAULT_FIREWALL_ALLOWED_IPS = listString cfg.defaultFirewallAllowedIps; + } + // lib.optionalAttrs (cfg.defaultPersistentKeepalive != null) { + WG_DEFAULT_PERSISTENT_KEEPALIVE = toString cfg.defaultPersistentKeepalive; + } + // lib.optionalAttrs (cfg.firewallEnabled != null) { + WG_FIREWALL_ENABLED = boolString cfg.firewallEnabled; + } + // lib.optionalAttrs (cfg.externalInterface != null) { + WG_DEVICE = cfg.externalInterface; + } + // cfg.extraEnvironment; + + path = with pkgs; [ + bash + coreutils + gawk + gnugrep + gnused + iproute2 + iptables + kmod + nftables + procps + wireguard-go + wireguard-tools + ]; + + serviceConfig = { + ExecStart = lib.getExe cfg.package; + Restart = "on-failure"; + RestartSec = "5s"; + User = "root"; + AmbientCapabilities = [ + "CAP_NET_ADMIN" + "CAP_SYS_MODULE" + ]; + CapabilityBoundingSet = [ + "CAP_NET_ADMIN" + "CAP_SYS_MODULE" + ]; + }; + }; + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 00000000..0a167cde --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,105 @@ +{ + lib, + stdenv, + nodejs_24, + pnpm_10, + pnpmConfigHook, + fetchPnpmDeps, + makeWrapper, + python3, + pkg-config, + bash, + coreutils, + gawk, + gnugrep, + gnused, + iproute2, + iptables, + kmod, + nftables, + procps, + wireguard-go, + wireguard-tools, +}: + +let + nodejs = nodejs_24; + runtimePath = lib.makeBinPath [ + bash + coreutils + gawk + gnugrep + gnused + iproute2 + iptables + kmod + nftables + procps + wireguard-go + wireguard-tools + ]; +in +stdenv.mkDerivation (finalAttrs: { + pname = "wg-easy"; + version = "15.3.0"; + + src = ../src; + + CI = true; + + nativeBuildInputs = [ + nodejs + pnpm_10 + pnpmConfigHook + makeWrapper + python3 + pkg-config + ]; + + pnpmDeps = fetchPnpmDeps { + inherit (finalAttrs) pname version src; + fetcherVersion = 3; + hash = "sha256-+fvK2wzd3AbOeUzeDhMXjW8rO2nkv1KziQrW6SNqm+g="; + }; + + buildPhase = '' + runHook preBuild + pnpm build + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/share/wg-easy $out/bin + cp -R .output/* $out/share/wg-easy/ + + mkdir -p $out/share/wg-easy/server/database + cp -R server/database/migrations $out/share/wg-easy/server/database/migrations + + if [ -e node_modules/libsql ]; then + mkdir -p $out/share/wg-easy/server/node_modules + cp -RL node_modules/libsql $out/share/wg-easy/server/node_modules/libsql + fi + + makeWrapper ${lib.getExe nodejs} $out/bin/wg-easy \ + --run "cd $out/share/wg-easy" \ + --add-flags "$out/share/wg-easy/server/index.mjs" \ + --prefix PATH : ${runtimePath} + + makeWrapper ${lib.getExe nodejs} $out/bin/wg-easy-cli \ + --run "cd $out/share/wg-easy" \ + --add-flags "$out/share/wg-easy/server/cli.mjs" \ + --prefix PATH : ${runtimePath} + + runHook postInstall + ''; + + meta = { + description = "WireGuard VPN server with a web-based admin UI"; + homepage = "https://github.com/wg-easy/wg-easy"; + license = lib.licenses.agpl3Only; + platforms = lib.platforms.linux; + mainProgram = "wg-easy"; + }; +}) diff --git a/privatekey b/privatekey new file mode 100644 index 00000000..15ea1c5d --- /dev/null +++ b/privatekey @@ -0,0 +1 @@ +8PLg+fY2dl4JuoWZvsmmumA6rzFn8tvb5bg/juCuc0I= diff --git a/publickey b/publickey new file mode 100644 index 00000000..e89bbbb3 --- /dev/null +++ b/publickey @@ -0,0 +1 @@ +VjyJs31bpDkEqlSEERvjNCjIDj7fRY0Odc69Eo665Qo= diff --git a/src/app/composables/useSubmit.ts b/src/app/composables/useSubmit.ts index 805c2b66..db40e931 100644 --- a/src/app/composables/useSubmit.ts +++ b/src/app/composables/useSubmit.ts @@ -1,46 +1,42 @@ -import type { - NitroFetchRequest, - NitroFetchOptions, - TypedInternalResponse, - ExtractedRouteMethod, -} from 'nitropack/types'; import { FetchError } from 'ofetch'; -type RevertFn< - R extends NitroFetchRequest, - T = unknown, - O extends NitroFetchOptions = NitroFetchOptions, -> = ( +type SubmitResponse = { + status?: string; + type?: string; + uri?: string; + key?: string; +}; + +type SubmitOptions = { + method: 'post' | 'delete' | 'put' | 'patch'; +}; + +type SubmitFetch = ( + request: string, + options: SubmitOptions & { body: unknown } +) => Promise; + +type RevertFn = ( success: boolean, - data: - | TypedInternalResponse< - R, - T, - NitroFetchOptions extends O ? 'get' : ExtractedRouteMethod - > - | undefined + data: SubmitResponse | undefined ) => Promise; -type SubmitOpts< - R extends NitroFetchRequest, - T = unknown, - O extends NitroFetchOptions = NitroFetchOptions, -> = { - revert: RevertFn; +type SubmitOpts = { + revert: RevertFn; successMsg?: string; noSuccessToast?: boolean; }; -export function useSubmit< - R extends NitroFetchRequest, - O extends NitroFetchOptions & { body?: never }, - T = unknown, ->(url: R, options: O, opts: SubmitOpts) { +export function useSubmit( + url: string, + options: SubmitOptions, + opts: SubmitOpts +) { const toast = useToast(); return async (data: unknown) => { try { - const res = await $fetch(url, { + const res = await ($fetch as SubmitFetch)(url, { ...options, body: data, }); @@ -52,8 +48,7 @@ export function useSubmit< }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await opts.revert(true, res as any); + await opts.revert(true, res); } catch (e) { if (e instanceof FetchError) { toast.showToast({ diff --git a/src/app/pages/me.vue b/src/app/pages/me.vue index 65c68ae9..5c51fc2b 100644 --- a/src/app/pages/me.vue +++ b/src/app/pages/me.vue @@ -177,7 +177,7 @@ const _setup2fa = useSubmit( }, { revert: async (success, data) => { - if (success && data?.type === 'setup') { + if (success && data?.type === 'setup' && data.uri && data.key) { const qrcode = encodeQR(data.uri, 'svg', { ecc: 'high', scale: 4, diff --git a/src/app/stores/auth.ts b/src/app/stores/auth.ts index 64bac790..d7de8c09 100644 --- a/src/app/stores/auth.ts +++ b/src/app/stores/auth.ts @@ -1,11 +1,20 @@ import type { H3Event } from 'h3'; import type { SharedPublicUser } from '~~/shared/utils/permissions'; +type SessionFetch = ( + request: string, + options: { method: 'get' } +) => Promise; + export const useAuthStore = defineStore('Auth', () => { const userData = useState('user-data', () => null); async function getSession(event?: H3Event) { - const fetch = event?.$fetch || $fetch; + let fetch = $fetch as unknown as SessionFetch; + if (event) { + fetch = event.$fetch as unknown as SessionFetch; + } + try { const data = await fetch('/api/session', { method: 'get', diff --git a/src/cli/clients/qr.ts b/src/cli/clients/qr.ts index 9c76d9ad..96220160 100644 --- a/src/cli/clients/qr.ts +++ b/src/cli/clients/qr.ts @@ -6,6 +6,8 @@ import { wg } from '../../server/utils/wgHelper'; import { encodeQRCodeTerm } from '../../server/utils/qr'; import { db, schema } from '../db'; +const interfaceName = process.env.WG_INTERFACE_NAME?.trim() || 'wg0'; + export default defineCommand({ meta: { name: 'clients:qr', @@ -34,7 +36,7 @@ export default defineCommand({ consola.info('Generating QR code for client...'); const wgInterface = await db.query.wgInterface.findFirst({ - where: eq(schema.wgInterface.name, 'wg0'), + where: eq(schema.wgInterface.name, interfaceName), }); if (!wgInterface) { consola.error('WireGuard interface not found'); @@ -42,7 +44,7 @@ export default defineCommand({ } const userConfig = await db.query.userConfig.findFirst({ - where: eq(schema.userConfig.id, 'wg0'), + where: eq(schema.userConfig.id, interfaceName), }); if (!userConfig) { consola.error('User config not found'); diff --git a/src/cli/db.ts b/src/cli/db.ts index 70945378..cbcb060d 100644 --- a/src/cli/db.ts +++ b/src/cli/db.ts @@ -3,8 +3,8 @@ import { drizzle } from 'drizzle-orm/libsql'; import * as schema from '../server/database/schema'; -//const client = createClient({ url: 'file:../data/wg-easy.db' }); -const client = createClient({ url: 'file:/etc/wireguard/wg-easy.db' }); +const stateDir = process.env.WG_STATE_DIR?.trim() || '/etc/wireguard'; +const client = createClient({ url: `file:${stateDir}/wg-easy.db` }); export const db = drizzle({ client, schema }); export { schema }; diff --git a/src/server/database/migrations/0001_classy_the_stranger.sql b/src/server/database/migrations/0001_classy_the_stranger.sql index a4bdf0f1..32159e89 100644 --- a/src/server/database/migrations/0001_classy_the_stranger.sql +++ b/src/server/database/migrations/0001_classy_the_stranger.sql @@ -9,9 +9,9 @@ INSERT INTO `hooks_table` (`id`, `pre_up`, `post_up`, `pre_down`, `post_down`) VALUES ( 'wg0', '', - 'iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE; iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT;', + 'iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE; iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -A FORWARD -i {{name}} -j ACCEPT; iptables -A FORWARD -o {{name}} -j ACCEPT; ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i {{name}} -j ACCEPT; ip6tables -A FORWARD -o {{name}} -j ACCEPT;', '', - 'iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE; iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -D FORWARD -o wg0 -j ACCEPT;' + 'iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE; iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -D FORWARD -i {{name}} -j ACCEPT; iptables -D FORWARD -o {{name}} -j ACCEPT; ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i {{name}} -j ACCEPT; ip6tables -D FORWARD -o {{name}} -j ACCEPT;' ); --> statement-breakpoint INSERT INTO `user_configs_table` (`id`, `default_mtu`, `default_persistent_keepalive`, `default_dns`, `default_allowed_ips`, `host`, `port`) diff --git a/src/server/database/repositories/client/service.ts b/src/server/database/repositories/client/service.ts index 97953cda..cc4f34db 100644 --- a/src/server/database/repositories/client/service.ts +++ b/src/server/database/repositories/client/service.ts @@ -9,6 +9,31 @@ import type { import type { DBType } from '#db/sqlite'; import { wgInterface, userConfig } from '#db/schema'; +function nixManagedClientDefaults(fallback?: { + allowedIps?: string[]; + dns?: string[]; +}) { + return { + ...(WG_CLIENT_DEFAULTS.ALLOWED_IPS !== undefined + ? { allowedIps: WG_CLIENT_DEFAULTS.ALLOWED_IPS } + : fallback?.allowedIps !== undefined + ? { allowedIps: fallback.allowedIps } + : {}), + ...(WG_CLIENT_DEFAULTS.DNS !== undefined + ? { dns: WG_CLIENT_DEFAULTS.DNS } + : fallback?.dns !== undefined + ? { dns: fallback.dns } + : {}), + serverAllowedIps: WG_CLIENT_DEFAULTS.SERVER_ALLOWED_IPS ?? [], + ...(WG_CLIENT_DEFAULTS.FIREWALL_ALLOWED_IPS !== undefined + ? { firewallIps: WG_CLIENT_DEFAULTS.FIREWALL_ALLOWED_IPS } + : {}), + ...(WG_CLIENT_DEFAULTS.PERSISTENT_KEEPALIVE !== undefined + ? { persistentKeepalive: WG_CLIENT_DEFAULTS.PERSISTENT_KEEPALIVE } + : {}), + }; +} + function createPreparedStatement(db: DBType) { return { findAll: db.query.client @@ -177,7 +202,7 @@ export class ClientService { const clients = await tx.query.client.findMany().execute(); const clientInterface = await tx.query.wgInterface .findFirst({ - where: eq(wgInterface.name, 'wg0'), + where: eq(wgInterface.name, WG_ENV.INTERFACE_NAME), }) .execute(); @@ -206,7 +231,7 @@ export class ClientService { name, // TODO: properly assign user id userId: 1, - interfaceId: 'wg0', + interfaceId: WG_ENV.INTERFACE_NAME, expiresAt, privateKey, publicKey, @@ -223,7 +248,7 @@ export class ClientService { i4: clientConfig.defaultI4, i5: clientConfig.defaultI5, persistentKeepalive: clientConfig.defaultPersistentKeepalive, - serverAllowedIps: [], + ...nixManagedClientDefaults(), enabled: true, }) .returning({ clientId: client.id }) @@ -243,7 +268,7 @@ export class ClientService { return this.#db.transaction(async (tx) => { const clientInterface = await tx.query.wgInterface .findFirst({ - where: eq(wgInterface.name, 'wg0'), + where: eq(wgInterface.name, WG_ENV.INTERFACE_NAME), }) .execute(); @@ -279,7 +304,7 @@ export class ClientService { .values({ name, userId: 1, - interfaceId: 'wg0', + interfaceId: WG_ENV.INTERFACE_NAME, privateKey, publicKey, preSharedKey, @@ -296,7 +321,10 @@ export class ClientService { allowedIps: clientConfig.defaultAllowedIps, dns: clientConfig.defaultDns, persistentKeepalive: clientConfig.defaultPersistentKeepalive, - serverAllowedIps: [], + ...nixManagedClientDefaults({ + allowedIps: clientConfig.defaultAllowedIps, + dns: clientConfig.defaultDns, + }), enabled, }) .execute(); diff --git a/src/server/database/repositories/hooks/service.ts b/src/server/database/repositories/hooks/service.ts index b3ea4073..b3bc2a15 100644 --- a/src/server/database/repositories/hooks/service.ts +++ b/src/server/database/repositories/hooks/service.ts @@ -21,7 +21,9 @@ export class HooksService { } async get() { - const hooks = await this.#statements.get.execute({ interface: 'wg0' }); + const hooks = await this.#statements.get.execute({ + interface: WG_ENV.INTERFACE_NAME, + }); if (!hooks) { throw new Error('Hooks not found'); } @@ -32,7 +34,7 @@ export class HooksService { return this.#db .update(hooks) .set(data) - .where(eq(hooks.id, 'wg0')) + .where(eq(hooks.id, WG_ENV.INTERFACE_NAME)) .execute(); } } diff --git a/src/server/database/repositories/interface/service.ts b/src/server/database/repositories/interface/service.ts index 4c0d0b42..1e6febe1 100644 --- a/src/server/database/repositories/interface/service.ts +++ b/src/server/database/repositories/interface/service.ts @@ -39,7 +39,7 @@ export class InterfaceService { async get() { const wgInterface = await this.#statements.get.execute({ - interface: 'wg0', + interface: WG_ENV.INTERFACE_NAME, }); if (!wgInterface) { throw new Error('Interface not found'); @@ -49,7 +49,7 @@ export class InterfaceService { updateKeyPair(privateKey: string, publicKey: string) { return this.#statements.updateKeyPair.execute({ - interface: 'wg0', + interface: WG_ENV.INTERFACE_NAME, privateKey, publicKey, }); @@ -59,13 +59,13 @@ export class InterfaceService { return this.#db .update(wgInterface) .set(data) - .where(eq(wgInterface.name, 'wg0')) + .where(eq(wgInterface.name, WG_ENV.INTERFACE_NAME)) .execute(); } setFirewallEnabled(firewallEnabled: boolean) { return this.#statements.setFirewallEnabled.execute({ - interface: 'wg0', + interface: WG_ENV.INTERFACE_NAME, firewallEnabled, }); } @@ -74,7 +74,7 @@ export class InterfaceService { return this.#db.transaction(async (tx) => { const oldCidr = await tx.query.wgInterface .findFirst({ - where: eq(wgInterface.name, 'wg0'), + where: eq(wgInterface.name, WG_ENV.INTERFACE_NAME), columns: { ipv4Cidr: true, ipv6Cidr: true }, }) .execute(); @@ -86,7 +86,7 @@ export class InterfaceService { await tx .update(wgInterface) .set(data) - .where(eq(wgInterface.name, 'wg0')) + .where(eq(wgInterface.name, WG_ENV.INTERFACE_NAME)) .execute(); const clients = await tx.query.client.findMany().execute(); diff --git a/src/server/database/repositories/userConfig/service.ts b/src/server/database/repositories/userConfig/service.ts index ecc3c4cc..122c0a34 100644 --- a/src/server/database/repositories/userConfig/service.ts +++ b/src/server/database/repositories/userConfig/service.ts @@ -22,7 +22,9 @@ export class UserConfigService { } async get() { - const userConfig = await this.#statements.get.execute({ interface: 'wg0' }); + const userConfig = await this.#statements.get.execute({ + interface: WG_ENV.INTERFACE_NAME, + }); if (!userConfig) { throw new Error('User config not found'); @@ -43,13 +45,13 @@ export class UserConfigService { await tx .update(userConfig) .set({ host, port }) - .where(eq(userConfig.id, 'wg0')) + .where(eq(userConfig.id, WG_ENV.INTERFACE_NAME)) .execute(); await tx .update(wgInterface) .set({ port }) - .where(eq(wgInterface.name, 'wg0')) + .where(eq(wgInterface.name, WG_ENV.INTERFACE_NAME)) .execute(); }); } @@ -58,7 +60,7 @@ export class UserConfigService { return this.#db .update(userConfig) .set(data) - .where(eq(userConfig.id, 'wg0')) + .where(eq(userConfig.id, WG_ENV.INTERFACE_NAME)) .execute(); } } diff --git a/src/server/database/sqlite.ts b/src/server/database/sqlite.ts index 4b0f6723..0c58ab80 100644 --- a/src/server/database/sqlite.ts +++ b/src/server/database/sqlite.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs/promises'; import { drizzle } from 'drizzle-orm/libsql'; import { migrate as drizzleMigrate } from 'drizzle-orm/libsql/migrator'; import { createClient } from '@libsql/client'; @@ -15,17 +16,23 @@ import { OneTimeLinkService } from './repositories/oneTimeLink/service'; const DB_DEBUG = createDebug('Database'); -const client = createClient({ url: 'file:/etc/wireguard/wg-easy.db' }); +await fs.mkdir(WG_ENV.STATE_DIR, { recursive: true, mode: 0o700 }); + +const client = createClient({ url: `file:${WG_ENV.STATE_DIR}/wg-easy.db` }); const db = drizzle({ client, schema }); export async function connect() { await migrate(); + await normalizeInterfaceName(db); const dbService = new DBService(db); if (WG_INITIAL_ENV.ENABLED) { await initialSetup(dbService); } + await applyRuntimeInterfaceConfig(db); + await applyNixClientDefaults(db); + if (WG_ENV.DISABLE_IPV6) { DB_DEBUG('Warning: Disabling IPv6...'); await disableIpv6(db); @@ -120,16 +127,167 @@ async function initialSetup(db: DBServiceType) { } } +function normalizeHookTemplate(hook: string) { + return hook + .replace(/-i wg0/g, '-i {{name}}') + .replace(/-o wg0/g, '-o {{name}}'); +} + +async function normalizeInterfaceName(db: DBType) { + const interfaceName = WG_ENV.INTERFACE_NAME; + if (interfaceName === 'wg0') { + return; + } + + const configuredInterface = await db.query.wgInterface + .findFirst({ + where: eq(schema.wgInterface.name, interfaceName), + }) + .execute(); + + if (configuredInterface) { + return; + } + + const defaultInterface = await db.query.wgInterface + .findFirst({ + where: eq(schema.wgInterface.name, 'wg0'), + }) + .execute(); + + if (!defaultInterface) { + return; + } + + DB_DEBUG(`Renaming default interface wg0 to ${interfaceName}...`); + await db.transaction(async (tx) => { + await tx + .update(schema.wgInterface) + .set({ name: interfaceName }) + .where(eq(schema.wgInterface.name, 'wg0')) + .execute(); + + await tx + .update(schema.hooks) + .set({ id: interfaceName }) + .where(eq(schema.hooks.id, 'wg0')) + .execute(); + + await tx + .update(schema.userConfig) + .set({ id: interfaceName }) + .where(eq(schema.userConfig.id, 'wg0')) + .execute(); + + await tx + .update(schema.client) + .set({ interfaceId: interfaceName }) + .where(eq(schema.client.interfaceId, 'wg0')) + .execute(); + }); +} + +async function applyRuntimeInterfaceConfig(db: DBType) { + const interfaceName = WG_ENV.INTERFACE_NAME; + const interfaceConfig: { + port: number; + device?: string; + firewallEnabled?: boolean; + } = WG_ENV.WG_DEVICE + ? { port: WG_ENV.WG_PORT, device: WG_ENV.WG_DEVICE } + : { port: WG_ENV.WG_PORT }; + if (WG_ENV.FIREWALL_ENABLED !== undefined) { + interfaceConfig.firewallEnabled = WG_ENV.FIREWALL_ENABLED; + } + + await db.transaction(async (tx) => { + await tx + .update(schema.wgInterface) + .set(interfaceConfig) + .where(eq(schema.wgInterface.name, interfaceName)) + .execute(); + + await tx + .update(schema.userConfig) + .set({ port: WG_ENV.WG_PORT }) + .where(eq(schema.userConfig.id, interfaceName)) + .execute(); + + const hooks = await tx.query.hooks + .findFirst({ + where: eq(schema.hooks.id, interfaceName), + }) + .execute(); + + if (!hooks) { + return; + } + + const nextPostUp = normalizeHookTemplate(hooks.postUp); + const nextPostDown = normalizeHookTemplate(hooks.postDown); + + if (nextPostUp !== hooks.postUp || nextPostDown !== hooks.postDown) { + await tx + .update(schema.hooks) + .set({ + postUp: nextPostUp, + postDown: nextPostDown, + }) + .where(eq(schema.hooks.id, interfaceName)) + .execute(); + } + }); +} + +async function applyNixClientDefaults(db: DBType) { + if (!WG_CLIENT_DEFAULTS.FORCE_UPDATE_CLIENTS) { + return; + } + + const clientDefaults: Partial = {}; + + if (WG_CLIENT_DEFAULTS.DNS !== undefined) { + clientDefaults.dns = WG_CLIENT_DEFAULTS.DNS; + } + + if (WG_CLIENT_DEFAULTS.ALLOWED_IPS !== undefined) { + clientDefaults.allowedIps = WG_CLIENT_DEFAULTS.ALLOWED_IPS; + } + + if (WG_CLIENT_DEFAULTS.SERVER_ALLOWED_IPS !== undefined) { + clientDefaults.serverAllowedIps = WG_CLIENT_DEFAULTS.SERVER_ALLOWED_IPS; + } + + if (WG_CLIENT_DEFAULTS.FIREWALL_ALLOWED_IPS !== undefined) { + clientDefaults.firewallIps = WG_CLIENT_DEFAULTS.FIREWALL_ALLOWED_IPS; + } + + if (WG_CLIENT_DEFAULTS.PERSISTENT_KEEPALIVE !== undefined) { + clientDefaults.persistentKeepalive = WG_CLIENT_DEFAULTS.PERSISTENT_KEEPALIVE; + } + + if (Object.keys(clientDefaults).length === 0) { + return; + } + + DB_DEBUG('Force updating clients with Nix-managed defaults...'); + await db + .update(schema.client) + .set(clientDefaults) + .where(eq(schema.client.interfaceId, WG_ENV.INTERFACE_NAME)) + .execute(); +} + async function disableIpv6(db: DBType) { // This should match the initial value migration const postUpMatch = - ' ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT;'; + ' ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i {{name}} -j ACCEPT; ip6tables -A FORWARD -o {{name}} -j ACCEPT;'; const postDownMatch = - ' ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -D FORWARD -o wg0 -j ACCEPT;'; + ' ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i {{name}} -j ACCEPT; ip6tables -D FORWARD -o {{name}} -j ACCEPT;'; await db.transaction(async (tx) => { const hooks = await tx.query.hooks.findFirst({ - where: eq(schema.hooks.id, 'wg0'), + where: eq(schema.hooks.id, WG_ENV.INTERFACE_NAME), }); if (!hooks) { @@ -144,7 +302,7 @@ async function disableIpv6(db: DBType) { postUp: hooks.postUp.replace(postUpMatch, ''), postDown: hooks.postDown.replace(postDownMatch, ''), }) - .where(eq(schema.hooks.id, 'wg0')) + .where(eq(schema.hooks.id, WG_ENV.INTERFACE_NAME)) .execute(); } else { DB_DEBUG('IPv6 Post Up hooks already disabled, skipping...'); @@ -157,7 +315,7 @@ async function disableIpv6(db: DBType) { postUp: hooks.postUp.replace(postUpMatch, ''), postDown: hooks.postDown.replace(postDownMatch, ''), }) - .where(eq(schema.hooks.id, 'wg0')) + .where(eq(schema.hooks.id, WG_ENV.INTERFACE_NAME)) .execute(); } else { DB_DEBUG('IPv6 Post Down hooks already disabled, skipping...'); diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts index 9ffc8ccb..acdfd0ba 100644 --- a/src/server/utils/WireGuard.ts +++ b/src/server/utils/WireGuard.ts @@ -62,8 +62,9 @@ class WireGuard { result.push(''); WG_DEBUG('Saving Config...'); + await fs.mkdir(WG_ENV.STATE_DIR, { recursive: true, mode: 0o700 }); await fs.writeFile( - `/etc/wireguard/${wgInterface.name}.conf`, + `${WG_ENV.STATE_DIR}/${wgInterface.name}.conf`, result.join('\n\n'), { mode: 0o600, diff --git a/src/server/utils/config.ts b/src/server/utils/config.ts index 1c5b2081..079fb1be 100644 --- a/src/server/utils/config.ts +++ b/src/server/utils/config.ts @@ -5,6 +5,106 @@ export const RELEASE = 'v' + packageJson.version; export const SERVER_DEBUG = createDebug('Server'); +const DEFAULT_INTERFACE_NAME = 'wg0'; +const DEFAULT_UI_PORT = '51821'; +const DEFAULT_WG_PORT = 51820; +const DEFAULT_STATE_DIR = '/etc/wireguard'; + +const interfaceNamePattern = /^[A-Za-z0-9_.-]{1,15}$/; + +function parseInterfaceName(env: string, fallback = DEFAULT_INTERFACE_NAME) { + const value = process.env[env]?.trim() || fallback; + + if (!interfaceNamePattern.test(value)) { + throw new Error( + `${env} must be 1-15 characters and contain only letters, numbers, '.', '_', or '-'` + ); + } + + return value; +} + +function parsePortEnv(env: string, fallback: number) { + const raw = process.env[env]?.trim(); + if (!raw) { + return fallback; + } + + const port = Number.parseInt(raw, 10); + if ( + !Number.isInteger(port) || + port < 1 || + port > 65535 || + `${port}` !== raw + ) { + throw new Error(`${env} must be a TCP/UDP port between 1 and 65535`); + } + + return port; +} + +function parseOptionalIntegerEnv(env: string, min: number, max: number) { + const raw = process.env[env]; + if (raw === undefined) { + return undefined; + } + + const value = raw.trim(); + const integer = Number.parseInt(value, 10); + if ( + !Number.isInteger(integer) || + integer < min || + integer > max || + `${integer}` !== value + ) { + throw new Error(`${env} must be an integer between ${min} and ${max}`); + } + + return integer; +} + +function parseOptionalBooleanEnv(env: string) { + const raw = process.env[env]; + if (raw === undefined) { + return undefined; + } + + const value = raw.trim(); + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + + throw new Error(`${env} must be either true or false`); +} + +function parseStateDir() { + const value = process.env.WG_STATE_DIR?.trim() || DEFAULT_STATE_DIR; + if (!value.startsWith('/') || value.includes('\0')) { + throw new Error('WG_STATE_DIR must be an absolute path'); + } + + return value.replace(/\/+$/, '') || '/'; +} + +function parseOptionalListEnv(env: string) { + const raw = process.env[env]; + if (raw === undefined) { + return undefined; + } + + if (raw.trim() === '') { + return []; + } + + return raw + .split(',') + .map((value) => value.trim()) + .filter((value) => value.length > 0); +} + export const OLD_ENV = { /** @deprecated Only for migration purposes */ PASSWORD: process.env.PASSWORD, @@ -34,9 +134,21 @@ export const WG_ENV = { /** UI is hosted on HTTP instead of HTTPS */ INSECURE: process.env.INSECURE === 'true', /** Port the UI is listening on */ - PORT: assertEnv('PORT'), + PORT: process.env.PORT?.trim() || DEFAULT_UI_PORT, /** If IPv6 should be disabled */ DISABLE_IPV6: process.env.DISABLE_IPV6 === 'true', + /** WireGuard interface name */ + INTERFACE_NAME: parseInterfaceName('WG_INTERFACE_NAME'), + /** WireGuard listen port */ + WG_PORT: parsePortEnv('WG_PORT', DEFAULT_WG_PORT), + /** Optional external network device for NAT hooks */ + WG_DEVICE: process.env.WG_DEVICE + ? parseInterfaceName('WG_DEVICE', process.env.WG_DEVICE) + : null, + /** Optional Nix-managed per-client firewall setting */ + FIREWALL_ENABLED: parseOptionalBooleanEnv('WG_FIREWALL_ENABLED'), + /** Writable directory for database and WireGuard configs */ + STATE_DIR: parseStateDir(), WG_EXECUTABLE: await detectAwg(), }; @@ -54,12 +166,15 @@ export const WG_INITIAL_ENV = { : undefined, }; -function assertEnv(env: T) { - const val = process.env[env]; - - if (!val) { - throw new Error(`Missing environment variable: ${env}`); - } - - return val; -} +export const WG_CLIENT_DEFAULTS = { + DNS: parseOptionalListEnv('WG_DEFAULT_DNS'), + ALLOWED_IPS: parseOptionalListEnv('WG_DEFAULT_ALLOWED_IPS'), + SERVER_ALLOWED_IPS: parseOptionalListEnv('WG_DEFAULT_SERVER_ALLOWED_IPS'), + FIREWALL_ALLOWED_IPS: parseOptionalListEnv('WG_DEFAULT_FIREWALL_ALLOWED_IPS'), + PERSISTENT_KEEPALIVE: parseOptionalIntegerEnv( + 'WG_DEFAULT_PERSISTENT_KEEPALIVE', + 0, + 65535 + ), + FORCE_UPDATE_CLIENTS: process.env.WG_FORCE_UPDATE_CLIENTS === 'true', +}; diff --git a/src/server/utils/ip.ts b/src/server/utils/ip.ts index c3c097e1..5100d71c 100644 --- a/src/server/utils/ip.ts +++ b/src/server/utils/ip.ts @@ -93,7 +93,7 @@ function getPrivateInformation() { const obj: Record = {}; for (const name of interfaceNames) { - if (name === 'wg0') { + if (name === WG_ENV.INTERFACE_NAME) { continue; } diff --git a/src/server/utils/template.ts b/src/server/utils/template.ts index 504a15dc..87c6ad4f 100644 --- a/src/server/utils/template.ts +++ b/src/server/utils/template.ts @@ -19,6 +19,7 @@ export function removeNewlines(templ: string) { * Available keys: * - ipv4Cidr: IPv4 CIDR * - ipv6Cidr: IPv6 CIDR + * - name: WireGuard interface name * - device: Network device * - port: Port number * - uiPort: UI port number @@ -27,6 +28,7 @@ export function iptablesTemplate(templ: string, wgInterface: InterfaceType) { return template(removeNewlines(templ), { ipv4Cidr: wgInterface.ipv4Cidr, ipv6Cidr: wgInterface.ipv6Cidr, + name: wgInterface.name, device: wgInterface.device, port: wgInterface.port.toString(), uiPort: WG_ENV.PORT, diff --git a/src/server/utils/wgHelper.ts b/src/server/utils/wgHelper.ts index 0ad7ceff..aa85704a 100644 --- a/src/server/utils/wgHelper.ts +++ b/src/server/utils/wgHelper.ts @@ -16,6 +16,16 @@ type Options = { // needed to support cli const wgExecutable = typeof WG_ENV !== 'undefined' ? WG_ENV.WG_EXECUTABLE : 'dev'; +const wgStateDir = + typeof WG_ENV !== 'undefined' ? WG_ENV.STATE_DIR : '/etc/wireguard'; + +function shellArg(value: string) { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function wgQuickTarget(infName: string) { + return shellArg(`${wgStateDir}/${infName}.conf`); +} export const wg = { generateServerPeer: ( @@ -185,29 +195,33 @@ Endpoint = ${userConfig.host}:${userConfig.port}`; }, up: (infName: string) => { - return exec(`${wgExecutable}-quick up ${infName}`); + return exec(`${wgExecutable}-quick up ${wgQuickTarget(infName)}`); }, down: (infName: string) => { - return exec(`${wgExecutable}-quick down ${infName}`); + return exec(`${wgExecutable}-quick down ${wgQuickTarget(infName)}`); }, restart: (infName: string) => { + const target = wgQuickTarget(infName); return exec( - `${wgExecutable}-quick down ${infName}; ${wgExecutable}-quick up ${infName}` + `${wgExecutable}-quick down ${target}; ${wgExecutable}-quick up ${target}` ); }, sync: (infName: string) => { return exec( - `${wgExecutable} syncconf ${infName} <(${wgExecutable}-quick strip ${infName})` + `${wgExecutable} syncconf ${shellArg(infName)} <(${wgExecutable}-quick strip ${wgQuickTarget(infName)})` ); }, dump: async (infName: string) => { - const rawDump = await exec(`${wgExecutable} show ${infName} dump`, { - log: false, - }); + const rawDump = await exec( + `${wgExecutable} show ${shellArg(infName)} dump`, + { + log: false, + } + ); type wgDumpLine = [ string,