Browse Source

Feat: Server AllowedIPs, MTU (#1356)

* add wireguard helpers

* improve wireguard helpers

* add server mtu

* fix wg0.conf formatting

* add ipv6 support to docker compose and readme
pull/1648/head
Bernd Storath 7 months ago
committed by Bernd Storath
parent
commit
7e500e0a5c
  1. 30
      README.md
  2. 14
      docker-compose.yml
  3. 135
      src/server/utils/WireGuard.ts
  4. 131
      src/server/utils/wgHelper.ts
  5. 10
      src/services/database/migrations/1.ts
  6. 1
      src/services/database/repositories/client.ts
  7. 1
      src/services/database/repositories/system.ts

30
README.md

@ -65,25 +65,39 @@ And log in again.
### 2. Run WireGuard Easy
To setup the IPv6 Network, simply run once:
```bash
docker network create \
-d bridge --ipv6 \
-d default \
--subnet 10.42.42.0/24 \
--subnet fdcc:ad94:bacf:61a3::/64 wg \
```
To automatically install & run wg-easy, simply run:
```bash
docker run -d \
--name=wg-easy \
--net wg \
-e PORT=51821 \
--name wg-easy \
--ip6 fdcc:ad94:bacf:61a3::2a \
--ip 10.42.42.42 \
-v ~/.wg-easy:/etc/wireguard \
-p 51820:51820/udp \
-p 51821:51821/tcp \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
--cap-add NET_ADMIN \
--cap-add SYS_MODULE \
--sysctl net.ipv4.ip_forward=1 \
--sysctl net.ipv4.conf.all.src_valid_mark=1 \
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
--sysctl net.ipv6.conf.all.forwarding=1 \
--sysctl net.ipv6.conf.default.forwarding=1 \
--restart unless-stopped \
ghcr.io/wg-easy/wg-easy
```
> 💡 Replace `<🚨YOUR_SERVER_IP>` with your WAN IP, or a Dynamic DNS hostname.
The Web UI will now be available on `http://0.0.0.0:51821`.
The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/)
@ -92,7 +106,7 @@ The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`.
WireGuard Easy can be launched with Docker Compose as well - just download
[`docker-compose.yml`](docker-compose.yml), make necessary adjustments and
execute `docker compose up --detach`.
execute `docker compose up -d`.
### 3. Sponsor

14
docker-compose.yml

@ -11,6 +11,10 @@ services:
image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy
networks:
wg:
ipv4_address: 10.42.42.42
ipv6_address: fdcc:ad94:bacf:61a3::2a
volumes:
- etc_wireguard:/etc/wireguard
ports:
@ -27,3 +31,13 @@ services:
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
networks:
wg:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: 10.42.42.0/24
- subnet: fdcc:ad94:bacf:61a3::/64

135
src/server/utils/WireGuard.ts

@ -21,37 +21,18 @@ class WireGuard {
async #saveWireguardConfig() {
const system = await Database.getSystem();
const clients = await Database.getClients();
const cidr4Block = parseCidr(system.userConfig.address4Range).prefix;
const cidr6Block = parseCidr(system.userConfig.address6Range).prefix;
let result = `
# Note: Do not edit this file directly.
# Your changes will be overwritten!
# Server
[Interface]
PrivateKey = ${system.interface.privateKey}
Address = ${system.interface.address4}/${cidr4Block}, ${system.interface.address6}/${cidr6Block}
ListenPort = ${system.wgPort}
PreUp = ${system.iptables.PreUp}
PostUp = ${system.iptables.PostUp}
PreDown = ${system.iptables.PreDown}
PostDown = ${system.iptables.PostDown}
`;
for (const [clientId, client] of Object.entries(clients)) {
if (!client.enabled) continue;
result += `
# Client: ${client.name} (${clientId})
[Peer]
PublicKey = ${client.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${client.address4}/32, ${client.address6}/128`;
const result = [];
result.push(wg.generateServerInterface(system));
for (const client of Object.values(clients)) {
if (!client.enabled) {
continue;
}
result.push(wg.generateServerPeer(client));
}
DEBUG('Config saving...');
await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, {
await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result.join('\n\n'), {
mode: 0o600,
});
DEBUG('Config saved.');
@ -59,7 +40,7 @@ AllowedIPs = ${client.address4}/32, ${client.address6}/128`;
async #syncWireguardConfig() {
DEBUG('Config syncing...');
await exec('wg syncconf wg0 <(wg-quick strip wg0)');
await wg.sync();
DEBUG('Config synced.');
}
@ -86,37 +67,28 @@ AllowedIPs = ${client.address4}/32, ${client.address6}/128`;
}));
// Loop WireGuard status
const dump = await exec('wg show wg0 dump', {
log: false,
});
dump
.trim()
.split('\n')
.slice(1)
.forEach((line) => {
const [
publicKey,
_preSharedKey,
endpoint,
_allowedIps,
latestHandshakeAt,
transferRx,
transferTx,
persistentKeepalive,
] = line.split('\t');
const dump = await wg.dump();
dump.forEach(
({
publicKey,
latestHandshakeAt,
endpoint,
transferRx,
transferTx,
persistentKeepalive,
}) => {
const client = clients.find((client) => client.publicKey === publicKey);
if (!client) return;
client.latestHandshakeAt =
latestHandshakeAt === '0'
? null
: new Date(Number(`${latestHandshakeAt}000`));
client.endpoint = endpoint === '(none)' ? null : (endpoint ?? null);
client.transferRx = Number(transferRx);
client.transferTx = Number(transferTx);
client.persistentKeepalive = persistentKeepalive ?? null;
});
if (!client) {
return;
}
client.latestHandshakeAt = latestHandshakeAt;
client.endpoint = endpoint;
client.transferRx = transferRx;
client.transferTx = transferTx;
client.persistentKeepalive = persistentKeepalive;
}
);
return clients;
}
@ -136,22 +108,8 @@ AllowedIPs = ${client.address4}/32, ${client.address6}/128`;
async getClientConfiguration({ clientId }: { clientId: string }) {
const system = await Database.getSystem();
const client = await this.getClient({ clientId });
const cidr4Block = parseCidr(system.userConfig.address4Range).prefix;
const cidr6Block = parseCidr(system.userConfig.address6Range).prefix;
return `
[Interface]
PrivateKey = ${client.privateKey}
Address = ${client.address4}/${cidr4Block}, ${client.address6}/${cidr6Block}
DNS = ${system.userConfig.defaultDns.join(', ')}
MTU = ${system.userConfig.mtu}
[Peer]
PublicKey = ${system.interface.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${client.allowedIPs.join(', ')}
PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
return wg.generateClientConfig(system, client);
}
async getClientQRCodeSVG({ clientId }: { clientId: string }) {
@ -172,11 +130,9 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
const system = await Database.getSystem();
const clients = await Database.getClients();
const privateKey = await exec('wg genkey');
const publicKey = await exec(`echo ${privateKey} | wg pubkey`, {
log: 'echo ***hidden*** | wg pubkey',
});
const preSharedKey = await exec('wg genpsk');
const privateKey = await wg.generatePrivateKey();
const publicKey = await wg.getPublicKey(privateKey);
const preSharedKey = await wg.generatePresharedKey();
// Calculate next IP
const cidr4 = parseCidr(system.userConfig.address4Range);
@ -239,6 +195,7 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
expiresAt: null,
enabled: true,
allowedIPs: system.userConfig.allowedIps,
serverAllowedIPs: null,
persistentKeepalive: system.userConfig.persistentKeepalive,
};
@ -374,8 +331,8 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
});
DEBUG('Starting Wireguard');
await this.#saveWireguardConfig();
await exec('wg-quick down wg0').catch(() => {});
await exec('wg-quick up wg0').catch((err) => {
await wg.down().catch(() => {});
await wg.up().catch((err) => {
if (
err &&
err.message &&
@ -407,7 +364,7 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
// Shutdown wireguard
async Shutdown() {
await exec('wg-quick down wg0').catch(() => {});
await wg.down().catch(() => {});
}
async cronJob() {
@ -465,15 +422,15 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
returnText += '\n# HELP wireguard_configured_peers\n';
returnText += '# TYPE wireguard_configured_peers gauge\n';
returnText += `wireguard_configured_peers{interface="wg0"} ${Number(wireguardPeerCount)}\n`;
returnText += `wireguard_configured_peers{interface="wg0"} ${wireguardPeerCount}\n`;
returnText += '\n# HELP wireguard_enabled_peers\n';
returnText += '# TYPE wireguard_enabled_peers gauge\n';
returnText += `wireguard_enabled_peers{interface="wg0"} ${Number(wireguardEnabledPeersCount)}\n`;
returnText += `wireguard_enabled_peers{interface="wg0"} ${wireguardEnabledPeersCount}\n`;
returnText += '\n# HELP wireguard_connected_peers\n';
returnText += '# TYPE wireguard_connected_peers gauge\n';
returnText += `wireguard_connected_peers{interface="wg0"} ${Number(wireguardConnectedPeersCount)}\n`;
returnText += `wireguard_connected_peers{interface="wg0"} ${wireguardConnectedPeersCount}\n`;
returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n';
returnText += '# TYPE wireguard_sent_bytes counter\n';
@ -507,9 +464,9 @@ Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
}
}
return {
wireguard_configured_peers: Number(wireguardPeerCount),
wireguard_enabled_peers: Number(wireguardEnabledPeersCount),
wireguard_connected_peers: Number(wireguardConnectedPeersCount),
wireguard_configured_peers: wireguardPeerCount,
wireguard_enabled_peers: wireguardEnabledPeersCount,
wireguard_connected_peers: wireguardConnectedPeersCount,
};
}
}

131
src/server/utils/wgHelper.ts

@ -0,0 +1,131 @@
import { parseCidr } from 'cidr-tools';
import type { Client } from '~~/services/database/repositories/client';
import type { System } from '~~/services/database/repositories/system';
export const wg = {
generateServerPeer: (client: Client) => {
const allowedIps = [
`${client.address4}/32`,
`${client.address6}/128`,
...(client.serverAllowedIPs ?? []),
];
return `# Client: ${client.name} (${client.id})
[Peer]
PublicKey = ${client.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${allowedIps.join(', ')}`;
},
generateServerInterface: (system: System) => {
const cidr4Block = parseCidr(system.userConfig.address4Range).prefix;
const cidr6Block = parseCidr(system.userConfig.address6Range).prefix;
return `# Note: Do not edit this file directly.
# Your changes will be overwritten!
# Server
[Interface]
PrivateKey = ${system.interface.privateKey}
Address = ${system.interface.address4}/${cidr4Block}, ${system.interface.address6}/${cidr6Block}
ListenPort = ${system.wgPort}
MTU = ${system.userConfig.serverMtu}
PreUp = ${system.iptables.PreUp}
PostUp = ${system.iptables.PostUp}
PreDown = ${system.iptables.PreDown}
PostDown = ${system.iptables.PostDown}`;
},
generateClientConfig: (system: System, client: Client) => {
const cidr4Block = parseCidr(system.userConfig.address4Range).prefix;
const cidr6Block = parseCidr(system.userConfig.address6Range).prefix;
return `[Interface]
PrivateKey = ${client.privateKey}
Address = ${client.address4}/${cidr4Block}, ${client.address6}/${cidr6Block}
DNS = ${system.userConfig.defaultDns.join(', ')}
MTU = ${system.userConfig.mtu}
[Peer]
PublicKey = ${system.interface.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${client.allowedIPs.join(', ')}
PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
},
generatePrivateKey: () => {
return exec('wg genkey');
},
getPublicKey: (privateKey: string) => {
return exec(`echo ${privateKey} | wg pubkey`, {
log: 'echo ***hidden*** | wg pubkey',
});
},
generatePresharedKey: () => {
return exec('wg genpsk');
},
up: () => {
return exec('wg-quick up wg0');
},
down: () => {
return exec('wg-quick down wg0');
},
sync: () => {
return exec('wg syncconf wg0 <(wg-quick strip wg0)');
},
dump: async () => {
const rawDump = await exec('wg show wg0 dump', {
log: false,
});
type wgDumpLine = [
string,
string,
string,
string,
string,
string,
string,
string,
];
return rawDump
.trim()
.split('\n')
.slice(1)
.map((line) => {
const splitLines = line.split('\t');
const [
publicKey,
preSharedKey,
endpoint,
allowedIPs,
latestHandshakeAt,
transferRx,
transferTx,
persistentKeepalive,
] = splitLines as wgDumpLine;
return {
publicKey,
preSharedKey,
endpoint: endpoint === '(none)' ? null : endpoint,
allowedIPs,
latestHandshakeAt:
latestHandshakeAt === '0'
? null
: new Date(Number.parseInt(`${latestHandshakeAt}000`)),
transferRx: Number.parseInt(transferRx),
transferTx: Number.parseInt(transferTx),
persistentKeepalive: persistentKeepalive,
};
});
},
};

10
src/services/database/migrations/1.ts

@ -5,14 +5,14 @@ import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
export async function run1(db: Low<Database>) {
const privateKey = await exec('wg genkey');
const publicKey = await exec(`echo ${privateKey} | wg pubkey`, {
log: 'echo ***hidden*** | wg pubkey',
});
const privateKey = await wg.generatePrivateKey();
const publicKey = await wg.getPublicKey(privateKey);
const address4Range = '10.8.0.0/24';
const address6Range = 'fdcc:ad94:bacf:61a4::cafe:0/112';
const cidr4 = parseCidr(address4Range);
const cidr6 = parseCidr(address6Range);
const database: Database = {
migrations: [],
system: {
@ -26,6 +26,7 @@ export async function run1(db: Low<Database>) {
lang: 'en',
userConfig: {
mtu: 1420,
serverMtu: 1420,
persistentKeepalive: 0,
address4Range: address4Range,
address6Range: address6Range,
@ -70,7 +71,6 @@ export async function run1(db: Low<Database>) {
clients: {},
};
// TODO: use variables inside up/down script
// TODO: properly check if ipv6 support
database.system.iptables.PostUp = `
iptables -t nat -A POSTROUTING -s ${database.system.userConfig.address4Range} -o ${database.system.wgDevice} -j MASQUERADE;

1
src/services/database/repositories/client.ts

@ -16,6 +16,7 @@ export type Client = {
expiresAt: string | null;
endpoint: string | null;
allowedIPs: string[];
serverAllowedIPs: string[] | null;
oneTimeLink: OneTimeLink | null;
/** ISO String */
createdAt: string;

1
src/services/database/repositories/system.ts

@ -18,6 +18,7 @@ export type WGInterface = {
export type WGConfig = {
mtu: number;
serverMtu: number;
persistentKeepalive: number;
address4Range: string;
address6Range: string;

Loading…
Cancel
Save