diff --git a/docs/content/advanced/config/optional-config.md b/docs/content/advanced/config/optional-config.md index 2f450b03..4d98f9d4 100644 --- a/docs/content/advanced/config/optional-config.md +++ b/docs/content/advanced/config/optional-config.md @@ -21,3 +21,74 @@ You will however still see a IPv6 address in the Web UI, but it won't be used. This option can be removed in the future, as more devices support IPv6. /// + +## Configuration Overrides + +These environment variables allow you to override settings that would normally be configured through the Admin Panel. When set, these values take precedence over database settings at runtime. + +### Interface Settings + +| Env | Example | Description | +| -------------- | ------------- | ------------------------- | +| `WG_PORT` | `51820` | WireGuard interface port | +| `WG_DEVICE` | `eth0` | Network device/interface | +| `WG_MTU` | `1420` | Maximum Transmission Unit | +| `WG_IPV4_CIDR` | `10.8.0.0/24` | IPv4 CIDR range | +| `WG_IPV6_CIDR` | `fdcc::/112` | IPv6 CIDR range | + +### Client Connection Settings + +| Env | Example | Description | +| --------------------------------- | ----------------- | ------------------------------- | +| `WG_HOST` | `vpn.example.com` | Host clients will connect to | +| `WG_CLIENT_PORT` | `51820` | Port clients will connect to | +| `WG_DEFAULT_DNS` | `1.1.1.1,8.8.8.8` | Default DNS servers for clients | +| `WG_DEFAULT_ALLOWED_IPS` | `0.0.0.0/0,::/0` | Default allowed IPs for clients | +| `WG_DEFAULT_MTU` | `1420` | Default MTU for clients | +| `WG_DEFAULT_PERSISTENT_KEEPALIVE` | `25` | Default persistent keepalive | + +### General Settings + +| Env | Example | Description | +| ----------------------- | ----------------- | ------------------------- | +| `WG_SESSION_TIMEOUT` | `3600` | Session timeout (seconds) | +| `WG_METRICS_PASSWORD` | `mypassword123` | Metrics endpoint password | +| `WG_METRICS_PROMETHEUS` | `true` or `false` | Enable Prometheus metrics | +| `WG_METRICS_JSON` | `true` or `false` | Enable JSON metrics | + +### Hooks + +| Env | Example | Description | +| -------------- | ------------------------- | --------------------- | +| `WG_PRE_UP` | `echo "Starting WG"` | PreUp hook command | +| `WG_POST_UP` | `iptables -A FORWARD ...` | PostUp hook command | +| `WG_PRE_DOWN` | `echo "Stopping WG"` | PreDown hook command | +| `WG_POST_DOWN` | `iptables -D FORWARD ...` | PostDown hook command | + +/// warning | Override Behavior + +When these override environment variables are set: + +- The specified values will be used at runtime instead of database settings +- You can still update these fields through the Web UI and they will be saved to the database +- However, the overridden values from environment variables will always take precedence at runtime +- The Web UI will display the database values with warning indicators showing which fields are overridden +- On first start, if no database values exist, some overridden values will be saved to the database + +Some overrides will not be applied to existing clients until they are manually edited. + +- `WG_DEFAULT_*` settings will only apply to new clients +- `WG_IPV4_CIDR` and `WG_IPV6_CIDR` changes will require clients to be manually edited to take effect + +/// + +/// note | Note on Port Variables + +- `WG_PORT` - The port WireGuard listens on (interface port) +- `WG_CLIENT_PORT` - The port clients connect to (endpoint port, uses `WG_PORT` if not set) +- `PORT` - The port the Web UI listens on (HTTP server port) + +In most cases you will only need to set `WG_PORT` to change the WireGuard port. +Keep in mind that you have to adjust both sides of the port publish option in your docker setup. + +/// diff --git a/docs/content/advanced/config/unattended-setup.md b/docs/content/advanced/config/unattended-setup.md index b0444d93..b70a2dc0 100644 --- a/docs/content/advanced/config/unattended-setup.md +++ b/docs/content/advanced/config/unattended-setup.md @@ -11,18 +11,20 @@ These will only be used during the first start of the container. After that, the | `INIT_ENABLED` | `true` | Enables the below env vars | 0 | | `INIT_USERNAME` | `admin` | Sets admin username | 1 | | `INIT_PASSWORD` | `Se!ureP%ssw` | Sets admin password | 1 | -| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 1 | -| `INIT_PORT` | `51820` | Port clients will connect to and wireguard will listen on | 1 | -| `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 2 | -| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 3 | -| `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 3 | -| `INIT_ALLOWED_IPS` | `10.8.0.0/24,2001:0DB8::/32` | Sets global Allowed IPs | 4 | +| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 2 | +| `INIT_PORT` | `51820` | Port clients will connect to and WireGuard will listen on | 2 | +| `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 3 | +| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 4 | +| `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 4 | +| `INIT_ALLOWED_IPS` | `10.8.0.0/24,2001:0DB8::/32` | Sets global Allowed IPs | 5 | /// warning | Variables have to be used together If variables are in the same group, you have to set all of them. For example, if you set `INIT_IPV4_CIDR`, you also have to set `INIT_IPV6_CIDR`. -If you want to skip the setup process, you have to configure group `1` +To skip the setup process, you must configure groups `1` and `2`. You can alternatively use `WG_HOST` and `WG_PORT` to set group `2` without using the `INIT_` variables. + +Avoid setting both `INIT_` and `WG_` variables for the same setting to prevent confusion. /// /// note | Security diff --git a/docs/content/advanced/migrate/from-14-to-15.md b/docs/content/advanced/migrate/from-14-to-15.md index 3ec3276e..aee21174 100644 --- a/docs/content/advanced/migrate/from-14-to-15.md +++ b/docs/content/advanced/migrate/from-14-to-15.md @@ -51,7 +51,9 @@ In the setup wizard, select that you already have a configuration file and uploa ### Environment Variables -v15 does not use the same environment variables as v14, most of them have been moved to the Admin Panel in the Web UI. +v15 does use some of the environment variables as v14. View [Configuration Overrides](../config/optional-config.md#configuration-overrides) to see which environment variables are supported in v15. + +If you want to be able to change settings through the Web UI, do not set the corresponding environment variables, as they will override the database settings. Instead, manually change the settings through the Web UI after the migration. ### Done diff --git a/src/app/components/Form/ArrayField.vue b/src/app/components/Form/ArrayField.vue index fd0c2e22..e1282092 100644 --- a/src/app/components/Form/ArrayField.vue +++ b/src/app/components/Form/ArrayField.vue @@ -1,5 +1,12 @@ diff --git a/src/app/components/Form/TextField.vue b/src/app/components/Form/TextField.vue index e62de448..4f99dba4 100644 --- a/src/app/components/Form/TextField.vue +++ b/src/app/components/Form/TextField.vue @@ -6,6 +6,12 @@ + + + (); const data = defineModel(); diff --git a/src/app/pages/admin/config.vue b/src/app/pages/admin/config.vue index 8f6f5767..1d29d887 100644 --- a/src/app/pages/admin/config.vue +++ b/src/app/pages/admin/config.vue @@ -9,12 +9,14 @@ :label="$t('general.host')" :description="$t('admin.config.hostDesc')" url="/api/admin/ip-info" + :overridden="overrides?.host" /> @@ -24,13 +26,18 @@ {{ $t('general.dns') }} - + {{ $t('form.sectionAdvanced') }} @@ -39,12 +46,14 @@ v-model="data.defaultMtu" :label="$t('general.mtu')" :description="$t('admin.config.mtuDesc')" + :overridden="overrides?.defaultMtu" /> @@ -118,6 +127,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/userconfig`, { method: 'get', }); +const { data: overridesData } = await useFetch(`/api/admin/overrides`, { + method: 'get', +}); + +const overrides = computed(() => overridesData.value?.userConfig); + const data = toRef(_data.value); const _submit = useSubmit( diff --git a/src/app/pages/admin/general.vue b/src/app/pages/admin/general.vue index 5950776f..bc8bb56c 100644 --- a/src/app/pages/admin/general.vue +++ b/src/app/pages/admin/general.vue @@ -7,6 +7,7 @@ v-model="data.sessionTimeout" :label="$t('admin.general.sessionTimeout')" :description="$t('admin.general.sessionTimeoutDesc')" + :overridden="overrides?.sessionTimeout" /> @@ -16,18 +17,21 @@ v-model="data.metricsPassword" :label="$t('admin.general.metricsPassword')" :description="$t('admin.general.metricsPasswordDesc')" + :overridden="overrides?.metricsPassword" /> @@ -43,6 +47,13 @@ const { data: _data, refresh } = await useFetch(`/api/admin/general`, { method: 'get', }); + +const { data: overridesData } = await useFetch(`/api/admin/overrides`, { + method: 'get', +}); + +const overrides = computed(() => overridesData.value?.general); + const data = toRef(_data.value); const _submit = useSubmit( diff --git a/src/app/pages/admin/hooks.vue b/src/app/pages/admin/hooks.vue index 047ac0b0..f619f765 100644 --- a/src/app/pages/admin/hooks.vue +++ b/src/app/pages/admin/hooks.vue @@ -6,21 +6,25 @@ id="PreUp" v-model="data.preUp" :label="$t('hooks.preUp')" + :overridden="overrides?.preUp" /> @@ -37,6 +41,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/hooks`, { method: 'get', }); +const { data: overridesData } = await useFetch(`/api/admin/overrides`, { + method: 'get', +}); + +const overrides = computed(() => overridesData.value?.hooks); + const data = toRef(_data.value); const _submit = useSubmit( diff --git a/src/app/pages/admin/interface.vue b/src/app/pages/admin/interface.vue index e75f164a..536401c1 100644 --- a/src/app/pages/admin/interface.vue +++ b/src/app/pages/admin/interface.vue @@ -7,18 +7,21 @@ v-model="data.mtu" :label="$t('general.mtu')" :description="$t('admin.interface.mtuDesc')" + :overridden="overrides?.mtu" /> @@ -173,6 +176,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/interface`, { method: 'get', }); +const { data: overridesData } = await useFetch(`/api/admin/overrides`, { + method: 'get', +}); + +const overrides = computed(() => overridesData.value?.interface); + const data = toRef(_data.value); const _submit = useSubmit( diff --git a/src/app/pages/setup/2.vue b/src/app/pages/setup/2.vue index bba23947..1b246068 100644 --- a/src/app/pages/setup/2.vue +++ b/src/app/pages/setup/2.vue @@ -56,9 +56,15 @@ const _submit = useSubmit( body: data, }), { - revert: async (success) => { + revert: async (success, data) => { if (success) { - await navigateTo('/setup/3'); + if (data?.setupDone) { + // Setup is complete, redirect to success page + await navigateTo('/setup/success'); + } else { + // Continue to step 3 + await navigateTo('/setup/3'); + } } }, noSuccessToast: true, diff --git a/src/server/api/admin/interface/cidr.post.ts b/src/server/api/admin/interface/cidr.post.ts index 95e239cf..a9abcdfc 100644 --- a/src/server/api/admin/interface/cidr.post.ts +++ b/src/server/api/admin/interface/cidr.post.ts @@ -8,7 +8,6 @@ export default definePermissionEventHandler( event, validateZod(InterfaceCidrUpdateSchema, event) ); - await Database.interfaces.updateCidr(data); await WireGuard.saveConfig(); return { success: true }; diff --git a/src/server/api/admin/overrides.get.ts b/src/server/api/admin/overrides.get.ts new file mode 100644 index 00000000..ad9a6d53 --- /dev/null +++ b/src/server/api/admin/overrides.get.ts @@ -0,0 +1,34 @@ +export default definePermissionEventHandler('admin', 'any', async () => { + return { + interface: { + port: WG_OVERRIDE_ENV.PORT !== undefined, + device: WG_OVERRIDE_ENV.DEVICE !== undefined, + mtu: WG_OVERRIDE_ENV.MTU !== undefined, + ipv4Cidr: WG_OVERRIDE_ENV.IPV4_CIDR !== undefined, + ipv6Cidr: WG_OVERRIDE_ENV.IPV6_CIDR !== undefined, + }, + userConfig: { + host: WG_CLIENT_OVERRIDE_ENV.HOST !== undefined, + port: WG_CLIENT_OVERRIDE_ENV.CLIENT_PORT !== undefined, + defaultDns: WG_CLIENT_OVERRIDE_ENV.DEFAULT_DNS !== undefined, + defaultAllowedIps: + WG_CLIENT_OVERRIDE_ENV.DEFAULT_ALLOWED_IPS !== undefined, + defaultMtu: WG_CLIENT_OVERRIDE_ENV.DEFAULT_MTU !== undefined, + defaultPersistentKeepalive: + WG_CLIENT_OVERRIDE_ENV.DEFAULT_PERSISTENT_KEEPALIVE !== undefined, + }, + general: { + sessionTimeout: WG_GENERAL_OVERRIDE_ENV.SESSION_TIMEOUT !== undefined, + metricsPassword: WG_GENERAL_OVERRIDE_ENV.METRICS_PASSWORD !== undefined, + metricsPrometheus: + WG_GENERAL_OVERRIDE_ENV.METRICS_PROMETHEUS !== undefined, + metricsJson: WG_GENERAL_OVERRIDE_ENV.METRICS_JSON !== undefined, + }, + hooks: { + preUp: WG_HOOKS_OVERRIDE_ENV.PRE_UP !== undefined, + postUp: WG_HOOKS_OVERRIDE_ENV.POST_UP !== undefined, + preDown: WG_HOOKS_OVERRIDE_ENV.PRE_DOWN !== undefined, + postDown: WG_HOOKS_OVERRIDE_ENV.POST_DOWN !== undefined, + }, + }; +}); diff --git a/src/server/api/setup/2.post.ts b/src/server/api/setup/2.post.ts index 29c0c769..129f0a06 100644 --- a/src/server/api/setup/2.post.ts +++ b/src/server/api/setup/2.post.ts @@ -8,6 +8,19 @@ export default defineSetupEventHandler(2, async ({ event }) => { await Database.users.create(username, password); - await Database.general.setSetupStep(3); - return { success: true }; + // If host and port are already set by environment variables, skip step 4 + const host = WG_INITIAL_ENV.HOST ?? WG_CLIENT_OVERRIDE_ENV.HOST; + const port = WG_INITIAL_ENV.PORT ?? WG_INTERFACE_OVERRIDE_ENV.PORT; + + const setupDone = host && port; + + if (setupDone) { + // Skip to done + await Database.general.setSetupStep(0); + } else { + // Proceed to step 3 (which leads to step 4) + await Database.general.setSetupStep(3); + } + + return { success: true, setupDone: setupDone }; }); diff --git a/src/server/database/repositories/client/service.ts b/src/server/database/repositories/client/service.ts index 97953cda..cff161b8 100644 --- a/src/server/database/repositories/client/service.ts +++ b/src/server/database/repositories/client/service.ts @@ -175,26 +175,30 @@ export class ClientService { return this.#db.transaction(async (tx) => { const clients = await tx.query.client.findMany().execute(); - const clientInterface = await tx.query.wgInterface + const _clientInterface = await tx.query.wgInterface .findFirst({ where: eq(wgInterface.name, 'wg0'), }) .execute(); - if (!clientInterface) { + if (!_clientInterface) { throw new Error('WireGuard interface not found'); } - const clientConfig = await tx.query.userConfig + const clientInterface = applyInterfaceOverrides(_clientInterface); + + const _clientConfig = await tx.query.userConfig .findFirst({ where: eq(userConfig.id, clientInterface.name), }) .execute(); - if (!clientConfig) { + if (!_clientConfig) { throw new Error('WireGuard interface configuration not found'); } + const clientConfig = applyUserConfigOverrides(_clientConfig); + const ipv4Cidr = parseCidr(clientInterface.ipv4Cidr); const ipv4Address = nextIP(4, ipv4Cidr, clients); const ipv6Cidr = parseCidr(clientInterface.ipv6Cidr); @@ -241,16 +245,18 @@ export class ClientService { update(id: ID, data: UpdateClientType) { return this.#db.transaction(async (tx) => { - const clientInterface = await tx.query.wgInterface + const _clientInterface = await tx.query.wgInterface .findFirst({ where: eq(wgInterface.name, 'wg0'), }) .execute(); - if (!clientInterface) { + if (!_clientInterface) { throw new Error('WireGuard interface not found'); } + const clientInterface = applyInterfaceOverrides(_clientInterface); + if (!containsCidr(clientInterface.ipv4Cidr, data.ipv4Address)) { throw new Error('IPv4 address is not within the CIDR range'); } @@ -272,7 +278,8 @@ export class ClientService { privateKey, publicKey, }: ClientCreateFromExistingType) { - const clientConfig = await Database.userConfigs.get(); + const _clientConfig = await Database.userConfigs.get(); + const clientConfig = applyUserConfigOverrides(_clientConfig); return this.#db .insert(client) diff --git a/src/server/database/sqlite.ts b/src/server/database/sqlite.ts index 4b0f6723..02798786 100644 --- a/src/server/database/sqlite.ts +++ b/src/server/database/sqlite.ts @@ -101,22 +101,26 @@ async function initialSetup(db: DBServiceType) { }); } - if ( - WG_INITIAL_ENV.USERNAME && - WG_INITIAL_ENV.PASSWORD && - WG_INITIAL_ENV.HOST && - WG_INITIAL_ENV.PORT - ) { + if (WG_INITIAL_ENV.USERNAME && WG_INITIAL_ENV.PASSWORD) { DB_DEBUG('Creating initial user...'); await db.users.create(WG_INITIAL_ENV.USERNAME, WG_INITIAL_ENV.PASSWORD); + await db.general.setSetupStep(3); + } + + // Use INIT vars or fall back to override vars for HOST and PORT + const host = WG_INITIAL_ENV.HOST ?? WG_CLIENT_OVERRIDE_ENV.HOST; + const port = WG_INITIAL_ENV.PORT ?? WG_INTERFACE_OVERRIDE_ENV.PORT; + + // HOST and PORT can come from either INIT vars or override vars + if (host && port) { DB_DEBUG('Setting initial host and port...'); - await db.userConfigs.updateHostPort( - WG_INITIAL_ENV.HOST, - WG_INITIAL_ENV.PORT - ); + await db.userConfigs.updateHostPort(host, port); - await db.general.setSetupStep(0); + // Setup completion requires USERNAME and PASSWORD (no overrides for these) + if (WG_INITIAL_ENV.USERNAME && WG_INITIAL_ENV.PASSWORD) { + await db.general.setSetupStep(0); + } } } diff --git a/src/server/middleware/setup.ts b/src/server/middleware/setup.ts index 374cad7b..1ad12139 100644 --- a/src/server/middleware/setup.ts +++ b/src/server/middleware/setup.ts @@ -9,12 +9,17 @@ export default defineEventHandler(async (event) => { const { step, done } = await Database.general.getSetupStep(); if (!done) { - const parsedSetup = url.pathname.match(/\/setup\/(\d)/); + const parsedSetup = url.pathname.match(/\/setup\/(\d|migrate|success)/); if (!parsedSetup) { return sendRedirect(event, `/setup/1`, 302); } const [_, currentSetup] = parsedSetup; + // Allow access to success page during setup + if (currentSetup === 'success') { + return; + } + if (step.toString() === currentSetup) { return; } diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts index 9ffc8ccb..818166b6 100644 --- a/src/server/utils/WireGuard.ts +++ b/src/server/utils/WireGuard.ts @@ -12,7 +12,10 @@ class WireGuard { * Save and sync config */ async saveConfig() { - const wgInterface = await Database.interfaces.get(); + const wgInterface = applyInterfaceOverrides( + await Database.interfaces.get() + ); + await this.#saveWireguardConfig(wgInterface); await this.#syncWireguardConfig(wgInterface); await this.#applyFirewallRules(wgInterface); @@ -39,7 +42,7 @@ class WireGuard { */ async #saveWireguardConfig(wgInterface: InterfaceType) { const clients = await Database.clients.getAll(); - const hooks = await Database.hooks.get(); + const hooks = applyHooksOverrides(await Database.hooks.get()); const result = []; result.push( @@ -164,8 +167,12 @@ class WireGuard { } async getClientConfiguration({ clientId }: { clientId: ID }) { - const wgInterface = await Database.interfaces.get(); - const userConfig = await Database.userConfigs.get(); + const wgInterface = applyInterfaceOverrides( + await Database.interfaces.get() + ); + const userConfig = applyUserConfigOverrides( + await Database.userConfigs.get() + ); const client = await Database.clients.get(clientId); @@ -227,25 +234,33 @@ class WireGuard { Database.interfaces.update(wgInterface); } - WG_DEBUG(`Starting Wireguard Interface ${wgInterface.name}...`); - await this.#saveWireguardConfig(wgInterface); - await wg.down(wgInterface.name).catch(() => {}); - await wg.up(wgInterface.name).catch((err) => { + const wgInterfaceWithOverrides = applyInterfaceOverrides(wgInterface); + + WG_DEBUG( + `Starting Wireguard Interface ${wgInterfaceWithOverrides.name}...` + ); + await this.#saveWireguardConfig(wgInterfaceWithOverrides); + await wg.down(wgInterfaceWithOverrides.name).catch(() => {}); + await wg.up(wgInterfaceWithOverrides.name).catch((err) => { if ( err && err.message && - err.message.includes(`Cannot find device "${wgInterface.name}"`) + err.message.includes( + `Cannot find device "${wgInterfaceWithOverrides.name}"` + ) ) { throw new Error( - `WireGuard exited with the error: Cannot find device "${wgInterface.name}"\nThis usually means that your host's kernel does not support WireGuard!`, + `WireGuard exited with the error: Cannot find device "${wgInterfaceWithOverrides.name}"\nThis usually means that your host's kernel does not support WireGuard!`, { cause: err.message } ); } throw err; }); - await this.#syncWireguardConfig(wgInterface); - WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`); + await this.#syncWireguardConfig(wgInterfaceWithOverrides); + WG_DEBUG( + `Wireguard Interface ${wgInterfaceWithOverrides.name} started successfully.` + ); // Check if firewall was enabled but iptables isn't available if (wgInterface.firewallEnabled) { @@ -282,12 +297,16 @@ class WireGuard { // Shutdown wireguard async Shutdown() { - const wgInterface = await Database.interfaces.get(); + const wgInterface = applyInterfaceOverrides( + await Database.interfaces.get() + ); await wg.down(wgInterface.name).catch(() => {}); } async Restart() { - const wgInterface = await Database.interfaces.get(); + const wgInterface = applyInterfaceOverrides( + await Database.interfaces.get() + ); await wg.restart(wgInterface.name); } diff --git a/src/server/utils/config.ts b/src/server/utils/config.ts index 4498442a..2ddc896e 100644 --- a/src/server/utils/config.ts +++ b/src/server/utils/config.ts @@ -55,6 +55,78 @@ export const WG_INITIAL_ENV = { : undefined, }; +export const WG_INTERFACE_OVERRIDE_ENV = { + /** Override the WireGuard interface port */ + PORT: process.env.WG_PORT + ? Number.parseInt(process.env.WG_PORT, 10) + : undefined, + /** Override the network device/interface */ + DEVICE: process.env.WG_DEVICE, + /** Override the MTU setting */ + MTU: process.env.WG_MTU ? Number.parseInt(process.env.WG_MTU, 10) : undefined, + /** Override the IPv4 CIDR */ + IPV4_CIDR: process.env.WG_IPV4_CIDR, + /** Override the IPv6 CIDR */ + IPV6_CIDR: process.env.WG_IPV6_CIDR, +}; + +export const WG_CLIENT_OVERRIDE_ENV = { + /** Override the client connection host */ + HOST: process.env.WG_HOST, + /** Override the client connection port (falls back to Interface Port) */ + CLIENT_PORT: process.env.WG_CLIENT_PORT + ? Number.parseInt(process.env.WG_CLIENT_PORT, 10) + : WG_INTERFACE_OVERRIDE_ENV.PORT, + /** Override default client DNS servers */ + DEFAULT_DNS: process.env.WG_DEFAULT_DNS?.split(',').map((x) => x.trim()), + /** Override default client allowed IPs */ + DEFAULT_ALLOWED_IPS: process.env.WG_DEFAULT_ALLOWED_IPS?.split(',').map((x) => + x.trim() + ), + /** Override default client MTU */ + DEFAULT_MTU: process.env.WG_DEFAULT_MTU + ? Number.parseInt(process.env.WG_DEFAULT_MTU, 10) + : undefined, + /** Override default client persistent keepalive */ + DEFAULT_PERSISTENT_KEEPALIVE: process.env.WG_DEFAULT_PERSISTENT_KEEPALIVE + ? Number.parseInt(process.env.WG_DEFAULT_PERSISTENT_KEEPALIVE, 10) + : undefined, +}; + +export const WG_GENERAL_OVERRIDE_ENV = { + /** Override session timeout */ + SESSION_TIMEOUT: process.env.WG_SESSION_TIMEOUT + ? Number.parseInt(process.env.WG_SESSION_TIMEOUT, 10) + : undefined, + /** Override metrics password */ + METRICS_PASSWORD: process.env.WG_METRICS_PASSWORD, + /** Override metrics Prometheus enabled status */ + METRICS_PROMETHEUS: + process.env.WG_METRICS_PROMETHEUS === 'true' + ? true + : process.env.WG_METRICS_PROMETHEUS === 'false' + ? false + : undefined, + /** Override metrics JSON enabled status */ + METRICS_JSON: + process.env.WG_METRICS_JSON === 'true' + ? true + : process.env.WG_METRICS_JSON === 'false' + ? false + : undefined, +}; + +export const WG_HOOKS_OVERRIDE_ENV = { + /** Override PreUp hook */ + PRE_UP: process.env.WG_PRE_UP, + /** Override PostUp hook */ + POST_UP: process.env.WG_POST_UP, + /** Override PreDown hook */ + PRE_DOWN: process.env.WG_PRE_DOWN, + /** Override PostDown hook */ + POST_DOWN: process.env.WG_POST_DOWN, +}; + function assertEnv(env: T) { const val = process.env[env]; @@ -64,3 +136,105 @@ function assertEnv(env: T) { return val; } + +/** + * Apply environment variable overrides to an interface object + */ +export function applyInterfaceOverrides< + T extends { + port: number; + device: string; + mtu: number; + ipv4Cidr: string; + ipv6Cidr: string; + }, +>(wgInterface: T): T { + return { + ...wgInterface, + port: WG_INTERFACE_OVERRIDE_ENV.PORT ?? wgInterface.port, + device: WG_INTERFACE_OVERRIDE_ENV.DEVICE ?? wgInterface.device, + mtu: WG_INTERFACE_OVERRIDE_ENV.MTU ?? wgInterface.mtu, + ipv4Cidr: WG_INTERFACE_OVERRIDE_ENV.IPV4_CIDR ?? wgInterface.ipv4Cidr, + ipv6Cidr: WG_INTERFACE_OVERRIDE_ENV.IPV6_CIDR ?? wgInterface.ipv6Cidr, + }; +} + +/** + * Apply environment variable overrides to a user config object + */ +export function applyUserConfigOverrides< + T extends { + host: string; + port: number; + defaultDns: string[]; + defaultAllowedIps: string[]; + defaultMtu: number; + defaultPersistentKeepalive: number; + }, +>(userConfig: T): T { + return { + ...userConfig, + host: WG_CLIENT_OVERRIDE_ENV.HOST ?? userConfig.host, + port: WG_CLIENT_OVERRIDE_ENV.CLIENT_PORT ?? userConfig.port, + defaultDns: WG_CLIENT_OVERRIDE_ENV.DEFAULT_DNS ?? userConfig.defaultDns, + defaultAllowedIps: + WG_CLIENT_OVERRIDE_ENV.DEFAULT_ALLOWED_IPS ?? + userConfig.defaultAllowedIps, + defaultMtu: WG_CLIENT_OVERRIDE_ENV.DEFAULT_MTU ?? userConfig.defaultMtu, + defaultPersistentKeepalive: + WG_CLIENT_OVERRIDE_ENV.DEFAULT_PERSISTENT_KEEPALIVE ?? + userConfig.defaultPersistentKeepalive, + }; +} + +/** + * Apply environment variable overrides to a general config object + */ +export function applySessionOverrides< + T extends { + sessionTimeout: number; + }, +>(generalConfig: T): T { + return { + ...generalConfig, + sessionTimeout: + WG_GENERAL_OVERRIDE_ENV.SESSION_TIMEOUT ?? generalConfig.sessionTimeout, + }; +} + +export function applyMetricsOverrides< + T extends { + password: string | null; + prometheus: boolean; + json: boolean; + }, +>(metricsConfig: T): T { + return { + ...metricsConfig, + password: + WG_GENERAL_OVERRIDE_ENV.METRICS_PASSWORD ?? metricsConfig.password, + prometheus: + WG_GENERAL_OVERRIDE_ENV.METRICS_PROMETHEUS ?? metricsConfig.prometheus, + json: WG_GENERAL_OVERRIDE_ENV.METRICS_JSON ?? metricsConfig.json, + }; +} + +/** + * Apply environment variable overrides to a hooks object + */ +export function applyHooksOverrides< + T extends { + preUp: string; + postUp: string; + preDown: string; + postDown: string; + }, +>(hooks: T): T { + return { + ...hooks, + preUp: WG_HOOKS_OVERRIDE_ENV.PRE_UP ?? hooks.preUp, + postUp: WG_HOOKS_OVERRIDE_ENV.POST_UP ?? hooks.postUp, + preDown: WG_HOOKS_OVERRIDE_ENV.PRE_DOWN ?? hooks.preDown, + postDown: WG_HOOKS_OVERRIDE_ENV.POST_DOWN ?? hooks.postDown, + }; +} diff --git a/src/server/utils/handler.ts b/src/server/utils/handler.ts index 5f0baa08..ca26b69a 100644 --- a/src/server/utils/handler.ts +++ b/src/server/utils/handler.ts @@ -138,7 +138,9 @@ export const defineMetricsHandler = < handler: MetricsHandler ) => { return defineEventHandler(async (event) => { - const metricsConfig = await Database.general.getMetricsConfig(); + const metricsConfig = applyMetricsOverrides( + await Database.general.getMetricsConfig() + ); if (metricsConfig.password) { const auth = getHeader(event, 'Authorization'); diff --git a/src/server/utils/session.ts b/src/server/utils/session.ts index 1a144cea..c690d038 100644 --- a/src/server/utils/session.ts +++ b/src/server/utils/session.ts @@ -8,7 +8,10 @@ export type WGSession = Partial<{ const name = 'wg-easy'; export async function useWGSession(event: H3Event, rememberMe = false) { - const sessionConfig = await Database.general.getSessionConfig(); + const sessionConfig = applySessionOverrides( + await Database.general.getSessionConfig() + ); + return useSession(event, { password: sessionConfig.sessionPassword, name, @@ -22,7 +25,10 @@ export async function useWGSession(event: H3Event, rememberMe = false) { } export async function getWGSession(event: H3Event) { - const sessionConfig = await Database.general.getSessionConfig(); + const sessionConfig = applySessionOverrides( + await Database.general.getSessionConfig() + ); + return getSession(event, { password: sessionConfig.sessionPassword, name,