Browse Source

[Feat]: QR code generation for AmneziaVPN app #2417

https://github.com/wg-easy/wg-easy/issues/2417
pull/2423/head
ne0x 6 months ago
parent
commit
63d8255568
  1. 15
      src/app/components/Clients/QRCodeDialog.vue
  2. 5
      src/i18n/locales/bn.json
  3. 5
      src/i18n/locales/de.json
  4. 5
      src/i18n/locales/en.json
  5. 5
      src/i18n/locales/es.json
  6. 5
      src/i18n/locales/fr.json
  7. 5
      src/i18n/locales/id.json
  8. 5
      src/i18n/locales/it.json
  9. 5
      src/i18n/locales/ko.json
  10. 5
      src/i18n/locales/nl.json
  11. 5
      src/i18n/locales/pl.json
  12. 5
      src/i18n/locales/pt-BR.json
  13. 5
      src/i18n/locales/ru.json
  14. 5
      src/i18n/locales/tr.json
  15. 5
      src/i18n/locales/uk.json
  16. 5
      src/i18n/locales/zh-CN.json
  17. 5
      src/i18n/locales/zh-HK.json
  18. 5
      src/i18n/locales/zh-TW.json
  19. 8
      src/server/api/client/[clientId]/qrcode.svg.get.ts
  20. 9
      src/server/database/repositories/client/types.ts
  21. 17
      src/server/utils/WireGuard.ts
  22. 107
      src/server/utils/wgHelper.ts

15
src/app/components/Clients/QRCodeDialog.vue

@ -5,10 +5,21 @@
</template>
<template #description>
<div class="bg-white">
<img :src="qrCode" />
<img :src="useAmnezia ? `${qrCode}?type=amnezia-vpn` : qrCode" />
</div>
</template>
<template #actions>
<div class="flex items-center justify-left gap-3 mr-auto">
<span class="text-sm dark:text-neutral-200">
{{ $t('client.amneziaVpn') }}
</span>
<BaseSwitch
v-model="useAmnezia"
:title="
useAmnezia ? $t('client.showWireGuardQR') : $t('client.showAmneziaVpnQR')
"
/>
</div>
<DialogClose as-child>
<BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton>
</DialogClose>
@ -18,4 +29,6 @@
<script setup lang="ts">
defineProps<{ qrCode: string }>();
const useAmnezia = ref(false);
</script>

5
src/i18n/locales/bn.json

@ -116,7 +116,10 @@
"dnsDesc": "DNS সার্ভার ক্লায়েন্টরা ব্যবহার করবে (গ্লোবাল কনফিগ ওভাররাইড করবে)",
"notConnected": "ক্লায়েন্ট সংযুক্ত নয়",
"endpoint": "এন্ডপয়েন্ট",
"endpointDesc": "ক্লায়েন্টের IP যেখান থেকে WireGuard সংযোগ স্থাপন করা হয়েছে"
"endpointDesc": "ক্লায়েন্টের IP যেখান থেকে WireGuard সংযোগ স্থাপন করা হয়েছে",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "AmneziaVPN ফরম্যাটে QR কোড দেখান",
"showWireGuardQR": "WireGuard ফরম্যাটে QR কোড দেখান"
},
"dialog": {
"change": "পরিবর্তন করুন",

5
src/i18n/locales/de.json

@ -120,7 +120,10 @@
"endpointDesc": "IP-Adresse des Clients, von dem aus die WireGuard-Verbindung hergestellt wird",
"search": "Suche Clients...",
"config": "Konfiguration",
"viewConfig": "Konfiguration anzeigen"
"viewConfig": "Konfiguration anzeigen",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "QR-Code im AmneziaVPN-Format anzeigen",
"showWireGuardQR": "QR-Code im WireGuard-Format anzeigen"
},
"dialog": {
"change": "Ändern",

5
src/i18n/locales/en.json

@ -120,7 +120,10 @@
"endpointDesc": "IP of the client from which the WireGuard connection is established",
"search": "Search clients...",
"config": "Configuration",
"viewConfig": "View Configuration"
"viewConfig": "View Configuration",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Show QR Code in AmneziaVPN format",
"showWireGuardQR": "Show QR Code in WireGuard format"
},
"dialog": {
"change": "Change",

5
src/i18n/locales/es.json

@ -116,7 +116,10 @@
"dnsDesc": "Servidor DNS que usarán los clientes (anula la configuración global)",
"notConnected": "Cliente no conectado",
"endpoint": "Punto de conexión",
"endpointDesc": "IP del cliente desde donde se establece la conexión WireGuard"
"endpointDesc": "IP del cliente desde donde se establece la conexión WireGuard",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Mostrar código QR en formato AmneziaVPN",
"showWireGuardQR": "Mostrar código QR en formato WireGuard"
},
"dialog": {
"change": "Cambiar",

5
src/i18n/locales/fr.json

@ -120,7 +120,10 @@
"search": "Rechercher des clients...",
"config": "Configuration",
"viewConfig": "Voir la configuration",
"delete": "Supprimer"
"delete": "Supprimer",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Afficher le code QR au format AmneziaVPN",
"showWireGuardQR": "Afficher le code QR au format WireGuard"
},
"dialog": {
"change": "Modifier",

5
src/i18n/locales/id.json

@ -116,7 +116,10 @@
"dnsDesc": "Server DNS yang akan digunakan klien (menggantikan konfigurasi global)",
"notConnected": "Klien tidak terhubung",
"endpoint": "Titik Akhir (Endpoint)",
"endpointDesc": "IP klien dari mana koneksi WireGuard dibuat"
"endpointDesc": "IP klien dari mana koneksi WireGuard dibuat",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Tampilkan Kode QR dalam format AmneziaVPN",
"showWireGuardQR": "Tampilkan Kode QR dalam format WireGuard"
},
"dialog": {
"change": "Ubah",

5
src/i18n/locales/it.json

@ -116,7 +116,10 @@
"dnsDesc": "Server DNS che i client useranno (sovrascrive la config globale)",
"notConnected": "Client non connesso",
"endpoint": "Endpoint",
"endpointDesc": "IP del client da cui viene stabilita la connessione WireGuard"
"endpointDesc": "IP del client da cui viene stabilita la connessione WireGuard",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Mostra codice QR in formato AmneziaVPN",
"showWireGuardQR": "Mostra codice QR in formato WireGuard"
},
"dialog": {
"change": "Modifica",

5
src/i18n/locales/ko.json

@ -116,7 +116,10 @@
"dnsDesc": "클라이언트가 사용할 DNS 서버(전역 구성 무시)",
"notConnected": "클라이언트가 연결되지 않음",
"endpoint": "엔드포인트",
"endpointDesc": "WireGuard에 연결된 클라이언트의 IP 주소"
"endpointDesc": "WireGuard에 연결된 클라이언트의 IP 주소",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "AmneziaVPN 형식의 QR 코드 표시",
"showWireGuardQR": "WireGuard 형식의 QR 코드 표시"
},
"dialog": {
"change": "변경",

5
src/i18n/locales/nl.json

@ -120,7 +120,10 @@
"endpointDesc": "IP van de cliënt vanaf welke de WireGuard-verbinding tot stand wordt gebracht",
"search": "Cliënten zoeken...",
"config": "Configuratie",
"viewConfig": "Configuratie weergeven"
"viewConfig": "Configuratie weergeven",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "QR-code in AmneziaVPN-formaat tonen",
"showWireGuardQR": "QR-code in WireGuard-formaat tonen"
},
"dialog": {
"change": "Wijzigen",

5
src/i18n/locales/pl.json

@ -116,7 +116,10 @@
"dnsDesc": "Serwer DNS, którego będą używać klienci (nadpisuje konfigurację globalną)",
"notConnected": "Klient nie jest połączony",
"endpoint": "Punkt końcowy",
"endpointDesc": "Adres IP klienta, z którego ustanawiane jest połączenie WireGuard"
"endpointDesc": "Adres IP klienta, z którego ustanawiane jest połączenie WireGuard",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Pokaż kod QR w formacie AmneziaVPN",
"showWireGuardQR": "Pokaż kod QR w formacie WireGuard"
},
"dialog": {
"change": "Zmień",

5
src/i18n/locales/pt-BR.json

@ -116,7 +116,10 @@
"dnsDesc": "Os clientes do servidor DNS usarão (substitui a configuração global)",
"notConnected": "Client não conectado",
"endpoint": "Endpoint",
"endpointDesc": "IP do cliente a partir do qual a conexão WireGuard é estabelecida"
"endpointDesc": "IP do cliente a partir do qual a conexão WireGuard é estabelecida",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Mostrar QR Code no formato AmneziaVPN",
"showWireGuardQR": "Mostrar QR Code no formato WireGuard"
},
"dialog": {
"change": "Mudar",

5
src/i18n/locales/ru.json

@ -120,7 +120,10 @@
"endpointDesc": "IP‑адрес клиента, с которого установлено соединение WireGuard",
"search": "Поиск клиентов...",
"config": "Конфигурация",
"viewConfig": "Просмотреть конфигурацию"
"viewConfig": "Просмотреть конфигурацию",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Показать QR-код в формате AmneziaVPN",
"showWireGuardQR": "Показать QR-код в формате WireGuard"
},
"dialog": {
"change": "Изменить",

5
src/i18n/locales/tr.json

@ -116,7 +116,10 @@
"dnsDesc": "İstemcilerin kullanacağı DNS sunucusu (genel yapılandırmayı geçersiz kılar)",
"notConnected": "İstemci bağlı değil",
"endpoint": "Uç Nokta",
"endpointDesc": "WireGuard bağlantısının kurulduğu istemcinin IP'si"
"endpointDesc": "WireGuard bağlantısının kurulduğu istemcinin IP'si",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "AmneziaVPN formatında QR kodu göster",
"showWireGuardQR": "WireGuard formatında QR kodu göster"
},
"dialog": {
"change": "Değiştir",

5
src/i18n/locales/uk.json

@ -120,7 +120,10 @@
"endpointDesc": "IP-адреса клієнта, з якої встановлюється з’єднання WireGuard",
"search": "Пошук клієнтів...",
"config": "Конфігурація",
"viewConfig": "Переглянути конфігурацію"
"viewConfig": "Переглянути конфігурацію",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "Показати QR-код у форматі AmneziaVPN",
"showWireGuardQR": "Показати QR-код у форматі WireGuard"
},
"dialog": {
"change": "Змінити",

5
src/i18n/locales/zh-CN.json

@ -120,7 +120,10 @@
"endpointDesc": "建立 WireGuard 连接时客户端的 IP 地址",
"search": "搜索客户端...",
"config": "配置",
"viewConfig": "查看配置文本"
"viewConfig": "查看配置文本",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "以 AmneziaVPN 格式显示二维码",
"showWireGuardQR": "以 WireGuard 格式显示二维码"
},
"dialog": {
"change": "确认修改",

5
src/i18n/locales/zh-HK.json

@ -114,7 +114,10 @@
"hooksDescription": "掛鉤僅適用於wg-quick",
"hooksLeaveEmpty": "僅適用於wg-quick,否則請留空",
"dnsDesc": "客戶端使用的域名系統伺服器(取代全局配置)",
"search": "搜尋客戶端..."
"search": "搜尋客戶端...",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "以 AmneziaVPN 格式顯示 QR 碼",
"showWireGuardQR": "以 WireGuard 格式顯示 QR 碼"
},
"dialog": {
"change": "更改",

5
src/i18n/locales/zh-TW.json

@ -120,7 +120,10 @@
"endpointDesc": "用戶端建立 WireGuard 連線的來源 IP",
"search": "搜尋用戶端...",
"config": "組態設定",
"viewConfig": "檢視組態設定"
"viewConfig": "檢視組態設定",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "以 AmneziaVPN 格式顯示 QR 碼",
"showWireGuardQR": "以 WireGuard 格式顯示 QR 碼"
},
"dialog": {
"change": "變更",

8
src/server/api/client/[clientId]/qrcode.svg.get.ts

@ -1,4 +1,4 @@
import { ClientGetSchema } from '#db/repositories/client/types';
import { ClientGetSchema, ClientQrSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler(
'clients',
@ -8,11 +8,15 @@ export default definePermissionEventHandler(
event,
validateZod(ClientGetSchema, event)
);
const { type } = await getValidatedQuery(
event,
validateZod(ClientQrSchema, event)
);
const client = await Database.clients.get(clientId);
checkPermissions(client);
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
const svg = await WireGuard.getClientQRCodeSVG({ clientId, type });
setHeader(event, 'Content-Type', 'image/svg+xml');
return svg;
}

9
src/server/database/repositories/client/types.ts

@ -103,3 +103,12 @@ export type ClientCreateFromExistingType = Pick<
| 'publicKey'
| 'enabled'
>;
const qrType = z.preprocess(
(val) => (val === 'amnezia-vpn' ? val : undefined),
z.enum(['amnezia-vpn']).optional()
);
export const ClientQrSchema = z.object({
type: qrType,
});

17
src/server/utils/WireGuard.ts

@ -149,7 +149,7 @@ class WireGuard {
return clients;
}
async getClientConfiguration({ clientId }: { clientId: ID }) {
async getClientConfiguration({ clientId, type }: { clientId: ID, type?: 'amnezia-vpn' }) {
const wgInterface = await Database.interfaces.get();
const userConfig = await Database.userConfigs.get();
@ -159,13 +159,22 @@ class WireGuard {
throw new Error('Client not found');
}
return wg.generateClientConfig(wgInterface, userConfig, client, {
const configText = wg.generateClientConfig(wgInterface, userConfig, client, {
enableIpv6: !WG_ENV.DISABLE_IPV6,
});
if (type === 'amnezia-vpn') {
const amneziaVPNClientConfig = wg.generateAmneziaVPNClientConfig(wgInterface, userConfig, client, configText, {
enableIpv6: !WG_ENV.DISABLE_IPV6,
});
return wg.buildAmneziaQrPack(JSON.stringify(amneziaVPNClientConfig));
}
return configText;
}
async getClientQRCodeSVG({ clientId }: { clientId: ID }) {
const config = await this.getClientConfiguration({ clientId });
async getClientQRCodeSVG({ clientId, type }: { clientId: ID, type?: 'amnezia-vpn' }) {
const config = await this.getClientConfiguration({ clientId, type });
const ECMode = ['high', 'quartile', 'medium', 'low'] as const;
for (const ecc of ECMode) {
try {

107
src/server/utils/wgHelper.ts

@ -1,3 +1,4 @@
import { deflateSync as zlibDeflateSync } from 'node:zlib';
import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
import type { ClientType } from '#db/repositories/client/types';
@ -164,6 +165,112 @@ PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${userConfig.host}:${userConfig.port}`;
},
generateAmneziaVPNClientConfig: (
wgInterface: InterfaceType,
userConfig: UserConfigType,
client: ClientType,
configText: string,
options: Options = {}
) => {
const { enableIpv6 = true } = options;
const address =
`${client.ipv4Address}/32` +
(enableIpv6 ? `, ${client.ipv6Address}/128` : '');
// ip6 DNS servers are not supported by AmneziaVPN app
const [dns1 = '', dns2 = ''] = (client.dns ?? userConfig.defaultDns ?? []).filter(
(value) => /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value)
);
let awgExtras: Record<string, string> = {};
let containerType = 'wireguard';
if (wgExecutable === 'awg') {
containerType = 'awg';
// S3, S4, i1, i2, i3, i4, i5 are not supported by AmneziaVPN app for now
const awgParams = {
Jc: client.jC,
Jmin: client.jMin,
Jmax: client.jMax,
S1: wgInterface.s1,
S2: wgInterface.s2,
// S3: wgInterface.s3,
// S4: wgInterface.s4,
// i1: client.i1,
// i2: client.i2,
// i3: client.i3,
// i4: client.i4,
// i5: client.i5,
H1: wgInterface.h1,
H2: wgInterface.h2,
H3: wgInterface.h3,
H4: wgInterface.h4,
} as const;
awgExtras = Object.fromEntries(
Object.entries(awgParams)
.filter(
([_, value]) => value !== null && value !== undefined && value !== ''
)
.map(([key, value]) => [key, `${value}`])
);
}
const lastConfigObj = {
...awgExtras,
allowed_ips: client.allowedIps ?? userConfig.defaultAllowedIps ?? [],
client_ip: address,
client_priv_key: client.privateKey,
config: configText,
hostName: userConfig.host,
mtu: `${client.mtu}`,
persistent_keep_alive: `${client.persistentKeepalive}`,
port: +userConfig.port,
psk_key: client.preSharedKey,
server_pub_key: wgInterface.publicKey,
};
return {
containers: [
{
[containerType]: {
isThirdPartyConfig: true,
last_config: JSON.stringify(lastConfigObj),
port: `${userConfig.port}`,
transport_proto: 'udp',
},
container: `amnezia-${containerType}`,
},
],
defaultContainer: `amnezia-${containerType}`,
description: client.name,
dns1: dns1,
dns2: dns2,
hostName: `${userConfig.host}`,
};
},
buildAmneziaQrPack: (amneziaConfigJSON: string) => {
// Observed working QR wrapper:
// [0..3] magic/version (0x07C00100)
// [4..7] zlib_len + 4
// [8..11] uncompressed_len
// [12..] zlib(deflate) bytes (starts with 78 DA typically)
const plain = Buffer.from(amneziaConfigJSON, 'utf8');
const z = zlibDeflateSync(plain);
const MAGIC = 0x07c00100;
const header = Buffer.allocUnsafe(12);
header.writeUInt32BE(MAGIC, 0);
header.writeUInt32BE(z.length + 4, 4);
header.writeUInt32BE(plain.length, 8);
const packed = Buffer.concat([header, z]);
return packed.toString('base64url');
},
generatePrivateKey: () => {
return exec(`${wgExecutable} genkey`);
},

Loading…
Cancel
Save