From 092fc0a1975cdc968dabe2915dda7175b4f5cea3 Mon Sep 17 00:00:00 2001 From: Carlo Alberto Aprigliano <31673755+CarloApri@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:09:44 +0200 Subject: [PATCH] feat: make the WireGuard interface name configurable (#2826) * add WG_INTERFACE env var * Scope firewall chains per interface --- .../advanced/config/optional-config.md | 10 + docs/content/examples/tutorials/adguard.md | 4 +- docs/content/examples/tutorials/podman-nft.md | 4 +- docs/content/examples/tutorials/routed.md | 8 +- src/cli/clients/qr.ts | 5 +- src/server/api/admin/ip-info.get.ts | 6 +- src/server/api/setup/4.get.ts | 6 +- .../migrations/0009_dusty_prodigy.sql | 5 + .../migrations/meta/0009_snapshot.json | 1144 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + .../database/repositories/client/service.ts | 9 +- .../database/repositories/hooks/service.ts | 7 +- .../repositories/interface/service.ts | 13 +- .../repositories/userConfig/service.ts | 11 +- src/server/database/sqlite.ts | 33 +- src/server/utils/config.ts | 3 + src/server/utils/firewall.ts | 71 +- src/server/utils/interfaceName.ts | 21 + src/server/utils/ip.ts | 14 +- src/server/utils/template.ts | 2 + src/test/unit/firewall.spec.ts | 42 +- src/test/unit/interfaceName.spec.ts | 25 + 22 files changed, 1381 insertions(+), 69 deletions(-) create mode 100644 src/server/database/migrations/0009_dusty_prodigy.sql create mode 100644 src/server/database/migrations/meta/0009_snapshot.json create mode 100644 src/server/utils/interfaceName.ts create mode 100644 src/test/unit/interfaceName.spec.ts diff --git a/docs/content/advanced/config/optional-config.md b/docs/content/advanced/config/optional-config.md index 6728d187..dba5be54 100644 --- a/docs/content/advanced/config/optional-config.md +++ b/docs/content/advanced/config/optional-config.md @@ -12,6 +12,7 @@ You can set these environment variables to configure the container. They are not | `DISABLE_IPV6` | `false` | `true` | If IPv6 support should be disabled | | `DISABLE_VERSION_CHECK` | `false` | `true` | If wg-easy should check for new updates | | `TRUSTED_PROXIES` | | `172.18.0.2,fd00:1234::/64` | Proxy IP addresses or CIDRs allowed to forward request metadata | +| `WG_INTERFACE` | `wg0` | `wg1` | Name of the WireGuard interface | ## Trusted Proxies @@ -32,6 +33,15 @@ request protocol remains controlled by `INSECURE`. Invalid addresses prevent wg-easy from starting so that configuration errors are not silently ignored. Restart the container after changing this setting. +## WireGuard Interface + +Set `WG_INTERFACE` to specify a custom interface name, useful if you are running multiple wg-easy instances on the same host with `network_mode: host`: + +```yaml +environment: + - WG_INTERFACE=wg1 +``` + /// note | IPv6 Caveats Disabling IPv6 will disable the creation of the default IPv6 firewall rules and won't add a IPv6 address to the interface and clients. diff --git a/docs/content/examples/tutorials/adguard.md b/docs/content/examples/tutorials/adguard.md index e65054ae..1e8906b3 100644 --- a/docs/content/examples/tutorials/adguard.md +++ b/docs/content/examples/tutorials/adguard.md @@ -127,13 +127,13 @@ networks: **_PostUp_** ```shell - iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -t nat -A PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.42.42.43; iptables -t nat -A PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43; ip6tables -t nat -A PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; ip6tables -t nat -A PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -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; ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; + iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -t nat -A PREROUTING -i {{interface}} -p udp --dport 53 -j DNAT --to-destination 10.42.42.43; iptables -t nat -A PREROUTING -i {{interface}} -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43; ip6tables -t nat -A PREROUTING -i {{interface}} -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; ip6tables -t nat -A PREROUTING -i {{interface}} -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; iptables -A FORWARD -i {{interface}} -j ACCEPT; iptables -A FORWARD -o {{interface}} -j ACCEPT; ip6tables -A FORWARD -i {{interface}} -j ACCEPT; ip6tables -A FORWARD -o {{interface}} -j ACCEPT; iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE; ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ``` **_PostDown_** ```shell - iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; iptables -t nat -D PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; iptables -t nat -D PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; ip6tables -t nat -D PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; ip6tables -t nat -D PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; iptables -D FORWARD -i wg0 -j ACCEPT || true; iptables -D FORWARD -o wg0 -j ACCEPT || true; ip6tables -D FORWARD -i wg0 -j ACCEPT || true; ip6tables -D FORWARD -o wg0 -j ACCEPT || true; iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE || true; ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE || true; + iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; iptables -t nat -D PREROUTING -i {{interface}} -p udp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; iptables -t nat -D PREROUTING -i {{interface}} -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; ip6tables -t nat -D PREROUTING -i {{interface}} -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; ip6tables -t nat -D PREROUTING -i {{interface}} -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; iptables -D FORWARD -i {{interface}} -j ACCEPT || true; iptables -D FORWARD -o {{interface}} -j ACCEPT || true; ip6tables -D FORWARD -i {{interface}} -j ACCEPT || true; ip6tables -D FORWARD -o {{interface}} -j ACCEPT || true; iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE || true; ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE || true; ``` 3. Restart `wg-easy` to apply changes: diff --git a/docs/content/examples/tutorials/podman-nft.md b/docs/content/examples/tutorials/podman-nft.md index 31141a61..071f47f5 100644 --- a/docs/content/examples/tutorials/podman-nft.md +++ b/docs/content/examples/tutorials/podman-nft.md @@ -88,13 +88,13 @@ In the Admin Panel of your WireGuard server, go to the `Hooks` tab and add the f 1. PostUp ```shell - nft add table inet wg_table; nft add chain inet wg_table prerouting { type nat hook prerouting priority 100 \; }; nft add chain inet wg_table postrouting { type nat hook postrouting priority 100 \; }; nft add rule inet wg_table postrouting ip saddr {{ipv4Cidr}} oifname {{device}} masquerade; nft add rule inet wg_table postrouting ip6 saddr {{ipv6Cidr}} oifname {{device}} masquerade; nft add chain inet wg_table input { type filter hook input priority 0 \; policy accept \; }; nft add rule inet wg_table input udp dport {{port}} accept; nft add rule inet wg_table input tcp dport {{uiPort}} accept; nft add chain inet wg_table forward { type filter hook forward priority 0 \; policy accept \; }; nft add rule inet wg_table forward iifname "wg0" accept; nft add rule inet wg_table forward oifname "wg0" accept; + nft add table inet wg_table_{{interface}}; nft add chain inet wg_table_{{interface}} prerouting { type nat hook prerouting priority 100 \; }; nft add chain inet wg_table_{{interface}} postrouting { type nat hook postrouting priority 100 \; }; nft add rule inet wg_table_{{interface}} postrouting ip saddr {{ipv4Cidr}} oifname {{device}} masquerade; nft add rule inet wg_table_{{interface}} postrouting ip6 saddr {{ipv6Cidr}} oifname {{device}} masquerade; nft add chain inet wg_table_{{interface}} input { type filter hook input priority 0 \; policy accept \; }; nft add rule inet wg_table_{{interface}} input udp dport {{port}} accept; nft add rule inet wg_table_{{interface}} input tcp dport {{uiPort}} accept; nft add chain inet wg_table_{{interface}} forward { type filter hook forward priority 0 \; policy accept \; }; nft add rule inet wg_table_{{interface}} forward iifname "{{interface}}" accept; nft add rule inet wg_table_{{interface}} forward oifname "{{interface}}" accept; ``` 2. PostDown ```shell - nft delete table inet wg_table + nft delete table inet wg_table_{{interface}} ``` If you don't have iptables loaded on your server, you could see many errors in the logs or in the UI. You can ignore them. diff --git a/docs/content/examples/tutorials/routed.md b/docs/content/examples/tutorials/routed.md index 2afdd9c9..5c406577 100644 --- a/docs/content/examples/tutorials/routed.md +++ b/docs/content/examples/tutorials/routed.md @@ -86,13 +86,13 @@ Then go to Admin → Hooks and add: PostUp ```shell -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 -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 -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -A FORWARD -i {{interface}} -j ACCEPT; iptables -A FORWARD -o {{interface}} -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i {{interface}} -j ACCEPT; ip6tables -A FORWARD -o {{interface}} -j ACCEPT ``` PostDown ```shell -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 -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 -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -D FORWARD -i {{interface}} -j ACCEPT; iptables -D FORWARD -o {{interface}} -j ACCEPT; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i {{interface}} -j ACCEPT; ip6tables -D FORWARD -o {{interface}} -j ACCEPT ``` /// warning | Important: When using nftables use the following hooks instead. @@ -100,13 +100,13 @@ iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -D FORWARD PostUp ```shell -nft add chain ip filter WG_EASY; nft add rule ip filter DOCKER-USER jump WG_EASY; nft add rule ip filter WG_EASY iifname {{device}} accept; nft add rule ip filter WG_EASY oifname {{device}} accept; nft add chain ip6 filter WG_EASY; nft add rule ip6 filter DOCKER-USER jump WG_EASY; nft add rule ip6 filter WG_EASY iifname {{device}} accept; nft add rule ip6 filter WG_EASY oifname {{device}} accept; +nft add chain ip filter WG_EASY_{{interface}}; nft add rule ip filter DOCKER-USER jump WG_EASY_{{interface}}; nft add rule ip filter WG_EASY_{{interface}} iifname {{device}} accept; nft add rule ip filter WG_EASY_{{interface}} oifname {{device}} accept; nft add chain ip6 filter WG_EASY_{{interface}}; nft add rule ip6 filter DOCKER-USER jump WG_EASY_{{interface}}; nft add rule ip6 filter WG_EASY_{{interface}} iifname {{device}} accept; nft add rule ip6 filter WG_EASY_{{interface}} oifname {{device}} accept; ``` PostDown ```shell -nft delete rule ip filter DOCKER-USER handle $(nft -a list chain ip filter DOCKER-USER | awk '/jump WG_EASY/ {print $NF}'); nft flush chain ip filter WG_EASY; nft delete chain ip filter WG_EASY; nft delete rule ip6 filter DOCKER-USER handle $(nft -a list chain ip6 filter DOCKER-USER | awk '/jump WG_EASY/ {print $NF}'); nft flush chain ip6 filter WG_EASY; nft delete chain ip6 filter WG_EASY +nft delete rule ip filter DOCKER-USER handle $(nft -a list chain ip filter DOCKER-USER | awk '/jump WG_EASY_{{interface}}/ {print $NF}'); nft flush chain ip filter WG_EASY_{{interface}}; nft delete chain ip filter WG_EASY_{{interface}}; nft delete rule ip6 filter DOCKER-USER handle $(nft -a list chain ip6 filter DOCKER-USER | awk '/jump WG_EASY_{{interface}}/ {print $NF}'); nft flush chain ip6 filter WG_EASY_{{interface}}; nft delete chain ip6 filter WG_EASY_{{interface}} ``` /// diff --git a/src/cli/clients/qr.ts b/src/cli/clients/qr.ts index f0b7a33d..eb00fa92 100644 --- a/src/cli/clients/qr.ts +++ b/src/cli/clients/qr.ts @@ -3,6 +3,7 @@ import { consola } from 'consola'; import { eq } from 'drizzle-orm'; import { db, schema } from '#cli/db'; +import { WG_ENV } from '#server/utils/config'; import { wg } from '#server/utils/wgHelper'; import { encodeQRCodeTerm } from '#server/utils/qr'; @@ -34,7 +35,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, WG_ENV.WG_INTERFACE), }); if (!wgInterface) { consola.error('WireGuard interface not found'); @@ -42,7 +43,7 @@ export default defineCommand({ } const userConfig = await db.query.userConfig.findFirst({ - where: eq(schema.userConfig.id, 'wg0'), + where: eq(schema.userConfig.id, wgInterface.name), }); if (!userConfig) { consola.error('User config not found'); diff --git a/src/server/api/admin/ip-info.get.ts b/src/server/api/admin/ip-info.get.ts index b558c6f4..95cc7bfb 100644 --- a/src/server/api/admin/ip-info.get.ts +++ b/src/server/api/admin/ip-info.get.ts @@ -1,7 +1,9 @@ +import Database from '#server/utils/Database'; import { definePermissionEventHandler } from '#server/utils/handler'; -import { cachedGetIpInformation } from '#server/utils/ip'; +import { getIpInformation } from '#server/utils/ip'; export default definePermissionEventHandler('admin', 'any', async () => { - const result = await cachedGetIpInformation(); + const wgInterface = await Database.interfaces.get(); + const result = await getIpInformation(wgInterface.name); return result; }); diff --git a/src/server/api/setup/4.get.ts b/src/server/api/setup/4.get.ts index 154c059c..e63a0405 100644 --- a/src/server/api/setup/4.get.ts +++ b/src/server/api/setup/4.get.ts @@ -1,7 +1,9 @@ +import Database from '#server/utils/Database'; import { defineSetupEventHandler } from '#server/utils/handler'; -import { cachedGetIpInformation } from '#server/utils/ip'; +import { getIpInformation } from '#server/utils/ip'; export default defineSetupEventHandler(4, async () => { - const result = await cachedGetIpInformation(); + const wgInterface = await Database.interfaces.get(); + const result = await getIpInformation(wgInterface.name); return result; }); diff --git a/src/server/database/migrations/0009_dusty_prodigy.sql b/src/server/database/migrations/0009_dusty_prodigy.sql new file mode 100644 index 00000000..c6d1f8b4 --- /dev/null +++ b/src/server/database/migrations/0009_dusty_prodigy.sql @@ -0,0 +1,5 @@ +UPDATE `hooks_table` SET + `pre_up` = replace(`pre_up`, 'wg0', '{{interface}}'), + `post_up` = replace(`post_up`, 'wg0', '{{interface}}'), + `pre_down` = replace(`pre_down`, 'wg0', '{{interface}}'), + `post_down` = replace(`post_down`, 'wg0', '{{interface}}'); diff --git a/src/server/database/migrations/meta/0009_snapshot.json b/src/server/database/migrations/meta/0009_snapshot.json new file mode 100644 index 00000000..b34488c5 --- /dev/null +++ b/src/server/database/migrations/meta/0009_snapshot.json @@ -0,0 +1,1144 @@ +{ + "id": "209191ca-75c3-4821-93f5-255119b9a351", + "prevId": "885a870f-9dc1-4d97-941e-d3d718b02d74", + "version": "6", + "dialect": "sqlite", + "tables": { + "clients_table": { + "name": "clients_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interface_id": { + "name": "interface_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ipv4_address": { + "name": "ipv4_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ipv6_address": { + "name": "ipv6_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pre_up": { + "name": "pre_up", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "post_up": { + "name": "post_up", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pre_down": { + "name": "pre_down", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "post_down": { + "name": "post_down", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pre_shared_key": { + "name": "pre_shared_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_ips": { + "name": "allowed_ips", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_allowed_ips": { + "name": "server_allowed_ips", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "firewall_ips": { + "name": "firewall_ips", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "persistent_keepalive": { + "name": "persistent_keepalive", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mtu": { + "name": "mtu", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "j_c": { + "name": "j_c", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "j_min": { + "name": "j_min", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "j_max": { + "name": "j_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i1": { + "name": "i1", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i2": { + "name": "i2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i3": { + "name": "i3", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i4": { + "name": "i4", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i5": { + "name": "i5", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_padding_addition": { + "name": "content_padding_addition", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rekey_after_time": { + "name": "rekey_after_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rekey_timeout": { + "name": "rekey_timeout", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reject_after_time": { + "name": "reject_after_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keepalive_timeout": { + "name": "keepalive_timeout", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_handshake_attempts": { + "name": "max_handshake_attempts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_cookies": { + "name": "disable_cookies", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dns": { + "name": "dns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_endpoint": { + "name": "server_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "clients_table_ipv4_address_unique": { + "name": "clients_table_ipv4_address_unique", + "columns": [ + "ipv4_address" + ], + "isUnique": true + }, + "clients_table_ipv6_address_unique": { + "name": "clients_table_ipv6_address_unique", + "columns": [ + "ipv6_address" + ], + "isUnique": true + }, + "public_key_interface_unique": { + "name": "public_key_interface_unique", + "columns": [ + "public_key", + "interface_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "clients_table_user_id_users_table_id_fk": { + "name": "clients_table_user_id_users_table_id_fk", + "tableFrom": "clients_table", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users_table", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "restrict" + }, + "clients_table_interface_id_interfaces_table_name_fk": { + "name": "clients_table_interface_id_interfaces_table_name_fk", + "tableFrom": "clients_table", + "columnsFrom": [ + "interface_id" + ], + "tableTo": "interfaces_table", + "columnsTo": [ + "name" + ], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "general_table": { + "name": "general_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "setup_step": { + "name": "setup_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_password": { + "name": "session_password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metrics_prometheus": { + "name": "metrics_prometheus", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metrics_json": { + "name": "metrics_json", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metrics_password": { + "name": "metrics_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hooks_table": { + "name": "hooks_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "pre_up": { + "name": "pre_up", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_up": { + "name": "post_up", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pre_down": { + "name": "pre_down", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_down": { + "name": "post_down", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "hooks_table_id_interfaces_table_name_fk": { + "name": "hooks_table_id_interfaces_table_name_fk", + "tableFrom": "hooks_table", + "columnsFrom": [ + "id" + ], + "tableTo": "interfaces_table", + "columnsTo": [ + "name" + ], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "interfaces_table": { + "name": "interfaces_table", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ipv4_cidr": { + "name": "ipv4_cidr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ipv6_cidr": { + "name": "ipv6_cidr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mtu": { + "name": "mtu", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "routing_table": { + "name": "routing_table", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "j_c": { + "name": "j_c", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 7 + }, + "j_min": { + "name": "j_min", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 10 + }, + "j_max": { + "name": "j_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1000 + }, + "s1": { + "name": "s1", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 128 + }, + "s2": { + "name": "s2", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 56 + }, + "s3": { + "name": "s3", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "s4": { + "name": "s4", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "h1": { + "name": "h1", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "h2": { + "name": "h2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "h3": { + "name": "h3", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "h4": { + "name": "h4", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i1": { + "name": "i1", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i2": { + "name": "i2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i3": { + "name": "i3", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i4": { + "name": "i4", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "i5": { + "name": "i5", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "header_protection_key": { + "name": "header_protection_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_padding_addition": { + "name": "content_padding_addition", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rekey_after_time": { + "name": "rekey_after_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rekey_timeout": { + "name": "rekey_timeout", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reject_after_time": { + "name": "reject_after_time", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keepalive_timeout": { + "name": "keepalive_timeout", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_handshake_attempts": { + "name": "max_handshake_attempts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "random_trailers": { + "name": "random_trailers", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_cookies": { + "name": "disable_cookies", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "firewall_enabled": { + "name": "firewall_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "interfaces_table_port_unique": { + "name": "interfaces_table_port_unique", + "columns": [ + "port" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "one_time_links_table": { + "name": "one_time_links_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "one_time_link": { + "name": "one_time_link", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "one_time_links_table_one_time_link_unique": { + "name": "one_time_links_table_one_time_link_unique", + "columns": [ + "one_time_link" + ], + "isUnique": true + } + }, + "foreignKeys": { + "one_time_links_table_id_clients_table_id_fk": { + "name": "one_time_links_table_id_clients_table_id_fk", + "tableFrom": "one_time_links_table", + "columnsFrom": [ + "id" + ], + "tableTo": "clients_table", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totp_key": { + "name": "totp_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_verified": { + "name": "totp_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "users_table_email_unique": { + "name": "users_table_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "oauth_provider_id_unique": { + "name": "oauth_provider_id_unique", + "columns": [ + "oauth_provider", + "oauth_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_configs_table": { + "name": "user_configs_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "default_mtu": { + "name": "default_mtu", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_persistent_keepalive": { + "name": "default_persistent_keepalive", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_dns": { + "name": "default_dns", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_allowed_ips": { + "name": "default_allowed_ips", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_j_c": { + "name": "default_j_c", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 7 + }, + "default_j_min": { + "name": "default_j_min", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 10 + }, + "default_j_max": { + "name": "default_j_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1000 + }, + "default_i1": { + "name": "default_i1", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_i2": { + "name": "default_i2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_i3": { + "name": "default_i3", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_i4": { + "name": "default_i4", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_i5": { + "name": "default_i5", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_configs_table_id_interfaces_table_name_fk": { + "name": "user_configs_table_id_interfaces_table_name_fk", + "tableFrom": "user_configs_table", + "columnsFrom": [ + "id" + ], + "tableTo": "interfaces_table", + "columnsTo": [ + "name" + ], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/server/database/migrations/meta/_journal.json b/src/server/database/migrations/meta/_journal.json index 6d23e252..fd5a5501 100644 --- a/src/server/database/migrations/meta/_journal.json +++ b/src/server/database/migrations/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1788760134302, "tag": "0008_thin_patch", "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1790082534310, + "tag": "0009_dusty_prodigy", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/database/repositories/client/service.ts b/src/server/database/repositories/client/service.ts index 14b664b1..3afb81d4 100644 --- a/src/server/database/repositories/client/service.ts +++ b/src/server/database/repositories/client/service.ts @@ -9,6 +9,7 @@ import type { UpdateClientType, } from './types'; +import { WG_ENV } from '#server/utils/config'; import Database from '#server/utils/Database'; import { nextIP } from '#server/utils/ip'; import type { ID } from '#server/utils/types'; @@ -162,7 +163,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.WG_INTERFACE), }) .execute(); @@ -191,7 +192,7 @@ export class ClientService { name, // TODO: properly assign user id userId: 1, - interfaceId: 'wg0', + interfaceId: WG_ENV.WG_INTERFACE, expiresAt, privateKey, publicKey, @@ -228,7 +229,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.WG_INTERFACE), }) .execute(); @@ -264,7 +265,7 @@ export class ClientService { .values({ name, userId: 1, - interfaceId: 'wg0', + interfaceId: WG_ENV.WG_INTERFACE, privateKey, publicKey, preSharedKey, diff --git a/src/server/database/repositories/hooks/service.ts b/src/server/database/repositories/hooks/service.ts index 41af78b8..6a48f464 100644 --- a/src/server/database/repositories/hooks/service.ts +++ b/src/server/database/repositories/hooks/service.ts @@ -3,6 +3,7 @@ import { eq, sql } from 'drizzle-orm'; import { hooks } from './schema'; import type { HooksUpdateType } from './types'; +import { WG_ENV } from '#server/utils/config'; import type { DBType } from '#db/sqlite'; function createPreparedStatement(db: DBType) { @@ -23,7 +24,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.WG_INTERFACE, + }); if (!hooks) { throw new Error('Hooks not found'); } @@ -34,7 +37,7 @@ export class HooksService { return this.#db .update(hooks) .set(data) - .where(eq(hooks.id, 'wg0')) + .where(eq(hooks.id, WG_ENV.WG_INTERFACE)) .execute(); } } diff --git a/src/server/database/repositories/interface/service.ts b/src/server/database/repositories/interface/service.ts index 7100835d..5dcb79e9 100644 --- a/src/server/database/repositories/interface/service.ts +++ b/src/server/database/repositories/interface/service.ts @@ -4,6 +4,7 @@ import { parseCidr } from 'cidr-tools'; import { wgInterface } from './schema'; import type { InterfaceCidrUpdateType, InterfaceUpdateType } from './types'; +import { WG_ENV } from '#server/utils/config'; import { nextIPFromUsedAddresses } from '#server/utils/ip'; import { client as clientSchema } from '#db/schema'; import type { DBType } from '#db/sqlite'; @@ -42,7 +43,7 @@ export class InterfaceService { async get() { const wgInterface = await this.#statements.get.execute({ - interface: 'wg0', + interface: WG_ENV.WG_INTERFACE, }); if (!wgInterface) { throw new Error('Interface not found'); @@ -52,7 +53,7 @@ export class InterfaceService { updateKeyPair(privateKey: string, publicKey: string) { return this.#statements.updateKeyPair.execute({ - interface: 'wg0', + interface: WG_ENV.WG_INTERFACE, privateKey, publicKey, }); @@ -62,13 +63,13 @@ export class InterfaceService { return this.#db .update(wgInterface) .set(data) - .where(eq(wgInterface.name, 'wg0')) + .where(eq(wgInterface.name, WG_ENV.WG_INTERFACE)) .execute(); } setFirewallEnabled(firewallEnabled: boolean) { return this.#statements.setFirewallEnabled.execute({ - interface: 'wg0', + interface: WG_ENV.WG_INTERFACE, firewallEnabled, }); } @@ -77,7 +78,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.WG_INTERFACE), columns: { ipv4Cidr: true, ipv6Cidr: true }, }) .execute(); @@ -89,7 +90,7 @@ export class InterfaceService { await tx .update(wgInterface) .set(data) - .where(eq(wgInterface.name, 'wg0')) + .where(eq(wgInterface.name, WG_ENV.WG_INTERFACE)) .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 db18c8d0..7437ca04 100644 --- a/src/server/database/repositories/userConfig/service.ts +++ b/src/server/database/repositories/userConfig/service.ts @@ -3,6 +3,7 @@ import { eq, sql } from 'drizzle-orm'; import { userConfig } from './schema'; import type { UserConfigUpdateType } from './types'; +import { WG_ENV } from '#server/utils/config'; import { wgInterface } from '#db/schema'; import type { DBType } from '#db/sqlite'; @@ -24,7 +25,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.WG_INTERFACE, + }); if (!userConfig) { throw new Error('User config not found'); @@ -45,13 +48,13 @@ export class UserConfigService { await tx .update(userConfig) .set({ host, port }) - .where(eq(userConfig.id, 'wg0')) + .where(eq(userConfig.id, WG_ENV.WG_INTERFACE)) .execute(); await tx .update(wgInterface) .set({ port }) - .where(eq(wgInterface.name, 'wg0')) + .where(eq(wgInterface.name, WG_ENV.WG_INTERFACE)) .execute(); }); } @@ -60,7 +63,7 @@ export class UserConfigService { return this.#db .update(userConfig) .set(data) - .where(eq(userConfig.id, 'wg0')) + .where(eq(userConfig.id, WG_ENV.WG_INTERFACE)) .execute(); } } diff --git a/src/server/database/sqlite.ts b/src/server/database/sqlite.ts index 59469ccc..f5a01e54 100644 --- a/src/server/database/sqlite.ts +++ b/src/server/database/sqlite.ts @@ -21,6 +21,7 @@ const db = drizzle({ client, schema }); export async function connect() { await migrate(); + await renameInterface(db); const dbService = new DBService(db); if (WG_INITIAL_ENV.ENABLED) { @@ -122,16 +123,38 @@ async function initialSetup(db: DBServiceType) { } } +// Renames the interface to the one specified in `WG_INTERFACE` then hooks, users, and clients via `ON UPDATE CASCADE` +async function renameInterface(db: DBType) { + const wgInterface = await db.query.wgInterface.findFirst(); + + if (!wgInterface) { + throw new Error('Interface not found'); + } + + if (wgInterface.name === WG_ENV.WG_INTERFACE) { + return; + } + + DB_DEBUG( + `Renaming Interface ${wgInterface.name} to ${WG_ENV.WG_INTERFACE}...` + ); + await db + .update(schema.wgInterface) + .set({ name: WG_ENV.WG_INTERFACE }) + .where(eq(schema.wgInterface.name, wgInterface.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 {{interface}} -j ACCEPT; ip6tables -A FORWARD -o {{interface}} -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 {{interface}} -j ACCEPT; ip6tables -D FORWARD -o {{interface}} -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.WG_INTERFACE), }); if (!hooks) { @@ -146,7 +169,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.WG_INTERFACE)) .execute(); } else { DB_DEBUG('IPv6 Post Up hooks already disabled, skipping...'); @@ -159,7 +182,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.WG_INTERFACE)) .execute(); } else { DB_DEBUG('IPv6 Post Down hooks already disabled, skipping...'); diff --git a/src/server/utils/config.ts b/src/server/utils/config.ts index 94f2f280..dfd8c043 100644 --- a/src/server/utils/config.ts +++ b/src/server/utils/config.ts @@ -2,6 +2,7 @@ import { createDebug } from 'obug'; import packageJson from '@@/package.json'; import { exec } from '#server/utils/cmd'; +import { parseInterfaceName } from '#server/utils/interfaceName'; import { parseTrustedProxies } from '#server/utils/trustedProxy'; import { OAUTH_PROVIDERS, @@ -50,6 +51,8 @@ export const WG_ENV = { PORT: assertEnv('PORT'), /** If IPv6 should be disabled */ DISABLE_IPV6: process.env.DISABLE_IPV6 === 'true', + /** Name of the WireGuard interface */ + WG_INTERFACE: parseInterfaceName(process.env.WG_INTERFACE), WG_EXECUTABLE: await detectAwg(), DISABLE_VERSION_CHECK: process.env.DISABLE_VERSION_CHECK === 'true', /** List of enabled and configured OAuth providers */ diff --git a/src/server/utils/firewall.ts b/src/server/utils/firewall.ts index ef7998c6..26c94343 100644 --- a/src/server/utils/firewall.ts +++ b/src/server/utils/firewall.ts @@ -9,6 +9,14 @@ import type { UserConfigType } from '#db/repositories/userConfig/types'; const FW_DEBUG = createDebug('Firewall'); const CHAIN_NAME = 'WG_CLIENTS'; +/** + * Tie chains to a specific interface + * Prevents instances in the same network namespace from wiping each other's rules + */ +function chainName(interfaceName: string) { + return `${CHAIN_NAME}_${interfaceName}`; +} + // Mutex to prevent concurrent rule rebuilds let rebuildInProgress = false; let rebuildQueued = false; @@ -131,6 +139,7 @@ function parseFirewallEntry(entry: string): ParsedEntry { * Generate iptables rule arguments for a single firewall entry */ function generateRuleArgs( + interfaceName: string, clientIp: string, entry: ParsedEntry, comment?: string, @@ -138,7 +147,7 @@ function generateRuleArgs( ): string[] { const rules: string[] = []; const commentArg = comment ? ` -m comment --comment "${comment}"` : ''; - const baseArgs = `-${action} ${CHAIN_NAME} -s ${clientIp} -d ${entry.ip}`; + const baseArgs = `-${action} ${chainName(interfaceName)} -s ${clientIp} -d ${entry.ip}`; if (entry.port) { // Port-specific rules @@ -165,24 +174,25 @@ export const firewall = { * Initialize the custom chain if it doesn't exist */ async initChain(interfaceName: string, enableIpv6: boolean): Promise { + const chain = chainName(interfaceName); FW_DEBUG( - `Initializing firewall chain ${CHAIN_NAME} for interface ${interfaceName}` + `Initializing firewall chain ${chain} for interface ${interfaceName}` ); // Create chain if not exists (iptables returns error if exists, so we ignore) - await exec(`iptables -N ${CHAIN_NAME} 2>/dev/null || true`); + await exec(`iptables -N ${chain} 2>/dev/null || true`); if (enableIpv6) { - await exec(`ip6tables -N ${CHAIN_NAME} 2>/dev/null || true`); + await exec(`ip6tables -N ${chain} 2>/dev/null || true`); } // Ensure chain is referenced from FORWARD (if not already) // Insert at position 1 to process before generic ACCEPT rules await exec( - `iptables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || iptables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}` + `iptables -C FORWARD -i ${interfaceName} -j ${chain} 2>/dev/null || iptables -I FORWARD 1 -i ${interfaceName} -j ${chain}` ); if (enableIpv6) { await exec( - `ip6tables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || ip6tables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}` + `ip6tables -C FORWARD -i ${interfaceName} -j ${chain} 2>/dev/null || ip6tables -I FORWARD 1 -i ${interfaceName} -j ${chain}` ); } }, @@ -190,11 +200,12 @@ export const firewall = { /** * Flush all rules in the custom chain */ - async flushChain(enableIpv6: boolean): Promise { - FW_DEBUG(`Flushing firewall chain ${CHAIN_NAME}`); - await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`); + async flushChain(interfaceName: string, enableIpv6: boolean): Promise { + const chain = chainName(interfaceName); + FW_DEBUG(`Flushing firewall chain ${chain}`); + await exec(`iptables -F ${chain} 2>/dev/null || true`); if (enableIpv6) { - await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`); + await exec(`ip6tables -F ${chain} 2>/dev/null || true`); } }, @@ -202,6 +213,7 @@ export const firewall = { * Apply firewall rules for a single client */ async applyClientRules( + interfaceName: string, client: FirewallClient, defaultAllowedIps: string[], enableIpv6: boolean @@ -226,13 +238,23 @@ export const firewall = { if (destIsIpv6) { if (enableIpv6) { - const rules = generateRuleArgs(client.ipv6Address, parsed, comment); + const rules = generateRuleArgs( + interfaceName, + client.ipv6Address, + parsed, + comment + ); for (const rule of rules) { await exec(`ip6tables ${rule}`); } } } else { - const rules = generateRuleArgs(client.ipv4Address, parsed, comment); + const rules = generateRuleArgs( + interfaceName, + client.ipv4Address, + parsed, + comment + ); for (const rule of rules) { await exec(`iptables ${rule}`); } @@ -271,12 +293,13 @@ export const firewall = { await this.initChain(wgInterface.name, enableIpv6); // Flush existing rules - await this.flushChain(enableIpv6); + await this.flushChain(wgInterface.name, enableIpv6); // Apply rules for each enabled client for (const client of clients) { if (!client.enabled) continue; await this.applyClientRules( + wgInterface.name, client, userConfig.defaultAllowedIps, enableIpv6 @@ -284,9 +307,10 @@ export const firewall = { } // Add final DROP for any traffic not explicitly allowed - await exec(`iptables -A ${CHAIN_NAME} -j DROP`); + const chain = chainName(wgInterface.name); + await exec(`iptables -A ${chain} -j DROP`); if (enableIpv6) { - await exec(`ip6tables -A ${CHAIN_NAME} -j DROP`); + await exec(`ip6tables -A ${chain} -j DROP`); } FW_DEBUG('Firewall rules rebuilt successfully'); @@ -309,24 +333,33 @@ export const firewall = { interfaceName: string, enableIpv6: boolean ): Promise { + const chain = chainName(interfaceName); FW_DEBUG(`Removing firewall filtering for interface ${interfaceName}`); // Remove jump rules from FORWARD chain + // This disconnects the interface from the old shared chain, but it won't delete the chain itself. + // Other wg-easy instances on the same host may still rely on it. + await exec( + `iptables -D FORWARD -i ${interfaceName} -j ${chain} 2>/dev/null || true` + ); await exec( `iptables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true` ); if (enableIpv6) { + await exec( + `ip6tables -D FORWARD -i ${interfaceName} -j ${chain} 2>/dev/null || true` + ); await exec( `ip6tables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true` ); } // Flush and delete the chain - await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`); - await exec(`iptables -X ${CHAIN_NAME} 2>/dev/null || true`); + await exec(`iptables -F ${chain} 2>/dev/null || true`); + await exec(`iptables -X ${chain} 2>/dev/null || true`); if (enableIpv6) { - await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`); - await exec(`ip6tables -X ${CHAIN_NAME} 2>/dev/null || true`); + await exec(`ip6tables -F ${chain} 2>/dev/null || true`); + await exec(`ip6tables -X ${chain} 2>/dev/null || true`); } }, diff --git a/src/server/utils/interfaceName.ts b/src/server/utils/interfaceName.ts new file mode 100644 index 00000000..2f1739f3 --- /dev/null +++ b/src/server/utils/interfaceName.ts @@ -0,0 +1,21 @@ +/** + * Default interface name + */ +const DEFAULT_INTERFACE_NAME = 'wg0'; + +/** + * `wg-quick` compliant interface naming + */ +const INTERFACE_NAME_REGEX = /^[a-zA-Z0-9_=+.-]{1,15}$/; + +export function parseInterfaceName(value: string | undefined): string { + if (!value) { + return DEFAULT_INTERFACE_NAME; + } + + if (!INTERFACE_NAME_REGEX.test(value)) { + throw new Error(`Invalid interface name: ${value}`); + } + + return value; +} diff --git a/src/server/utils/ip.ts b/src/server/utils/ip.ts index 480519a8..11bfefff 100644 --- a/src/server/utils/ip.ts +++ b/src/server/utils/ip.ts @@ -96,7 +96,7 @@ async function getReverseDns(ip: string) { } } -function getPrivateInformation() { +function getPrivateInformation(interfaceName: string) { const interfaces = networkInterfaces(); const interfaceNames = Object.keys(interfaces); @@ -104,7 +104,7 @@ function getPrivateInformation() { const obj: Record = {}; for (const name of interfaceNames) { - if (name === 'wg0') { + if (name === interfaceName) { continue; } @@ -132,10 +132,10 @@ function getPrivateInformation() { return obj; } -async function getIpInformation() { +export async function getIpInformation(interfaceName: string) { const results = []; - const publicInfo = await getPublicInformation(); + const publicInfo = await cachedGetPublicInformation(); if (publicInfo.ipv4) { results.push({ value: publicInfo.ipv4, @@ -155,7 +155,7 @@ async function getIpInformation() { }); } - const privateInfo = getPrivateInformation(); + const privateInfo = getPrivateInformation(interfaceName); for (const [name, { ipv4, ipv6 }] of Object.entries(privateInfo)) { for (const ip of ipv4) { results.push({ @@ -175,9 +175,9 @@ async function getIpInformation() { } /** - * Fetch IP Information + * Fetch public IP Information * @cache Response is cached for 15 min */ -export const cachedGetIpInformation = cacheFunction(getIpInformation, { +const cachedGetPublicInformation = cacheFunction(getPublicInformation, { expiry: 15 * 60 * 1000, }); diff --git a/src/server/utils/template.ts b/src/server/utils/template.ts index c3f1b0d9..22679737 100644 --- a/src/server/utils/template.ts +++ b/src/server/utils/template.ts @@ -19,6 +19,7 @@ export function removeNewlines(templ: string) { * - ipv4Cidr: IPv4 CIDR * - ipv6Cidr: IPv6 CIDR * - device: Network device + * - interface: WireGuard interface * - port: Port number * - uiPort: UI port number */ @@ -27,6 +28,7 @@ export function iptablesTemplate(templ: string, wgInterface: InterfaceType) { ipv4Cidr: wgInterface.ipv4Cidr, ipv6Cidr: wgInterface.ipv6Cidr, device: wgInterface.device, + interface: wgInterface.name, port: wgInterface.port.toString(), uiPort: WG_ENV.PORT, }); diff --git a/src/test/unit/firewall.spec.ts b/src/test/unit/firewall.spec.ts index 53710112..0dea0567 100644 --- a/src/test/unit/firewall.spec.ts +++ b/src/test/unit/firewall.spec.ts @@ -18,7 +18,7 @@ describe('firewall', () => { describe('IPv4-only chain management', () => { test('does not invoke ip6tables when initializing and flushing', async () => { await firewall.initChain('wg0', false); - await firewall.flushChain(false); + await firewall.flushChain('wg0', false); expect(execMock).toHaveBeenCalledWith( expect.stringContaining('iptables -C FORWARD -i wg0') @@ -40,6 +40,30 @@ describe('firewall', () => { }); }); + describe('interface scoped chains', () => { + test('uses one chain per interface', async () => { + await firewall.initChain('wg1', false); + + expect(execMock).toHaveBeenCalledWith( + expect.stringContaining('iptables -N WG_CLIENTS_wg1') + ); + expect(execMock).not.toHaveBeenCalledWith( + expect.stringContaining('iptables -N WG_CLIENTS ') + ); + }); + + test('stops referencing the legacy chain when removing filtering', async () => { + await firewall.removeFiltering('wg1', false); + + expect(execMock).toHaveBeenCalledWith( + expect.stringContaining('iptables -D FORWARD -i wg1 -j WG_CLIENTS ') + ); + expect(execMock).toHaveBeenCalledWith( + expect.stringContaining('iptables -X WG_CLIENTS_wg1') + ); + }); + }); + describe('isValidFirewallEntry', () => { test('invalid ips', () => { expect(() => typesTestExports.FirewallIpEntrySchema.parse('')).toThrow(); @@ -290,41 +314,43 @@ describe('firewall', () => { describe('generateRuleArgs', () => { test('includes comment when provided', () => { const rules = firewallTestExports.generateRuleArgs( + 'wg0', '10.8.0.2', { ip: '10.0.0.1' }, 'client 1: test' ); expect(rules).toEqual([ - '-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -m comment --comment "client 1: test" -j ACCEPT', + '-A WG_CLIENTS_wg0 -s 10.8.0.2 -d 10.0.0.1 -m comment --comment "client 1: test" -j ACCEPT', ]); }); test('omits comment when not provided', () => { - const rulesTcp = firewallTestExports.generateRuleArgs('10.8.0.2', { + const rulesTcp = firewallTestExports.generateRuleArgs('wg0', '10.8.0.2', { ip: '10.0.0.1', port: 80, proto: 'tcp', }); expect(rulesTcp).toEqual([ - '-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p tcp --dport 80 -j ACCEPT', + '-A WG_CLIENTS_wg0 -s 10.8.0.2 -d 10.0.0.1 -p tcp --dport 80 -j ACCEPT', ]); - const rulesUdp = firewallTestExports.generateRuleArgs('10.8.0.2', { + const rulesUdp = firewallTestExports.generateRuleArgs('wg0', '10.8.0.2', { ip: '10.0.0.1', port: 80, proto: 'udp', }); expect(rulesUdp).toEqual([ - '-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p udp --dport 80 -j ACCEPT', + '-A WG_CLIENTS_wg0 -s 10.8.0.2 -d 10.0.0.1 -p udp --dport 80 -j ACCEPT', ]); }); test('comment with port generates two rules for both proto', () => { const rules = firewallTestExports.generateRuleArgs( + 'wg0', '10.8.0.2', { ip: '10.0.0.1', port: 443, proto: 'both' }, 'client 2: phone' ); expect(rules).toEqual([ - '-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p tcp --dport 443 -m comment --comment "client 2: phone" -j ACCEPT', - '-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p udp --dport 443 -m comment --comment "client 2: phone" -j ACCEPT', + '-A WG_CLIENTS_wg0 -s 10.8.0.2 -d 10.0.0.1 -p tcp --dport 443 -m comment --comment "client 2: phone" -j ACCEPT', + '-A WG_CLIENTS_wg0 -s 10.8.0.2 -d 10.0.0.1 -p udp --dport 443 -m comment --comment "client 2: phone" -j ACCEPT', ]); }); }); diff --git a/src/test/unit/interfaceName.spec.ts b/src/test/unit/interfaceName.spec.ts new file mode 100644 index 00000000..0c1a0765 --- /dev/null +++ b/src/test/unit/interfaceName.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from 'vitest'; + +import { parseInterfaceName } from '#server/utils/interfaceName'; + +describe('parseInterfaceName', () => { + test('returns the default interface when a custom name is not specified', () => { + expect(parseInterfaceName(undefined)).toBe('wg0'); + expect(parseInterfaceName('')).toBe('wg0'); + }); + + test('accepts names compliant with wg-quick', () => { + expect(parseInterfaceName('wg1')).toBe('wg1'); + expect(parseInterfaceName('wg-home')).toBe('wg-home'); + expect(parseInterfaceName('a'.repeat(15))).toBe('a'.repeat(15)); + }); + + test('rejects invalid names', () => { + expect(() => parseInterfaceName('wg 0')).toThrow( + 'Invalid interface name: wg 0' + ); + expect(() => parseInterfaceName('wg0; id')).toThrow(); + expect(() => parseInterfaceName('wg/0')).toThrow(); + expect(() => parseInterfaceName('a'.repeat(16))).toThrow(); + }); +});