mirror of
https://github.com/wg-easy/wg-easy.git
synced 2026-10-21 06:11:16 +00:00
feat: make the WireGuard interface name configurable (#2826)
* add WG_INTERFACE env var * Scope firewall chains per interface
This commit is contained in:
parent
eb748ed8fd
commit
092fc0a197
22 changed files with 1381 additions and 69 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
```
|
||||
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
5
src/server/database/migrations/0009_dusty_prodigy.sql
Normal file
5
src/server/database/migrations/0009_dusty_prodigy.sql
Normal file
|
|
@ -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}}');
|
||||
1144
src/server/database/migrations/meta/0009_snapshot.json
Normal file
1144
src/server/database/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -64,6 +64,13 @@
|
|||
"when": 1788760134302,
|
||||
"tag": "0008_thin_patch",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1790082534310,
|
||||
"tag": "0009_dusty_prodigy",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...');
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
FW_DEBUG(`Flushing firewall chain ${CHAIN_NAME}`);
|
||||
await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`);
|
||||
async flushChain(interfaceName: string, enableIpv6: boolean): Promise<void> {
|
||||
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<void> {
|
||||
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`);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
21
src/server/utils/interfaceName.ts
Normal file
21
src/server/utils/interfaceName.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<string, { ipv4: string[]; ipv6: string[] }> = {};
|
||||
|
||||
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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
25
src/test/unit/interfaceName.spec.ts
Normal file
25
src/test/unit/interfaceName.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue