Browse Source

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

https://github.com/wg-easy/wg-easy/issues/2417
pull/2423/head^2
ne0x 6 months ago
parent
commit
5392e8e12b
  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>
<template #description> <template #description>
<div class="bg-white"> <div class="bg-white">
<img :src="qrCode" /> <img :src="useAmnezia ? `${qrCode}?type=amnezia-vpn` : qrCode" />
</div> </div>
</template> </template>
<template #actions> <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> <DialogClose as-child>
<BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton> <BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton>
</DialogClose> </DialogClose>
@ -18,4 +29,6 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ qrCode: string }>(); defineProps<{ qrCode: string }>();
const useAmnezia = ref(false);
</script> </script>

5
src/i18n/locales/bn.json

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

5
src/i18n/locales/de.json

@ -120,7 +120,10 @@
"endpointDesc": "IP-Adresse des Clients, von dem aus die WireGuard-Verbindung hergestellt wird", "endpointDesc": "IP-Adresse des Clients, von dem aus die WireGuard-Verbindung hergestellt wird",
"search": "Suche Clients...", "search": "Suche Clients...",
"config": "Konfiguration", "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": { "dialog": {
"change": "Ändern", "change": "Ändern",

5
src/i18n/locales/en.json

@ -120,7 +120,10 @@
"endpointDesc": "IP of the client from which the WireGuard connection is established", "endpointDesc": "IP of the client from which the WireGuard connection is established",
"search": "Search clients...", "search": "Search clients...",
"config": "Configuration", "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": { "dialog": {
"change": "Change", "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)", "dnsDesc": "Servidor DNS que usarán los clientes (anula la configuración global)",
"notConnected": "Cliente no conectado", "notConnected": "Cliente no conectado",
"endpoint": "Punto de conexión", "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": { "dialog": {
"change": "Cambiar", "change": "Cambiar",

5
src/i18n/locales/fr.json

@ -120,7 +120,10 @@
"search": "Rechercher des clients...", "search": "Rechercher des clients...",
"config": "Configuration", "config": "Configuration",
"viewConfig": "Voir la 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": { "dialog": {
"change": "Modifier", "change": "Modifier",

5
src/i18n/locales/id.json

@ -116,7 +116,10 @@
"dnsDesc": "Server DNS yang akan digunakan klien (menggantikan konfigurasi global)", "dnsDesc": "Server DNS yang akan digunakan klien (menggantikan konfigurasi global)",
"notConnected": "Klien tidak terhubung", "notConnected": "Klien tidak terhubung",
"endpoint": "Titik Akhir (Endpoint)", "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": { "dialog": {
"change": "Ubah", "change": "Ubah",

5
src/i18n/locales/it.json

@ -116,7 +116,10 @@
"dnsDesc": "Server DNS che i client useranno (sovrascrive la config globale)", "dnsDesc": "Server DNS che i client useranno (sovrascrive la config globale)",
"notConnected": "Client non connesso", "notConnected": "Client non connesso",
"endpoint": "Endpoint", "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": { "dialog": {
"change": "Modifica", "change": "Modifica",

5
src/i18n/locales/ko.json

@ -116,7 +116,10 @@
"dnsDesc": "클라이언트가 사용할 DNS 서버(전역 구성 무시)", "dnsDesc": "클라이언트가 사용할 DNS 서버(전역 구성 무시)",
"notConnected": "클라이언트가 연결되지 않음", "notConnected": "클라이언트가 연결되지 않음",
"endpoint": "엔드포인트", "endpoint": "엔드포인트",
"endpointDesc": "WireGuard에 연결된 클라이언트의 IP 주소" "endpointDesc": "WireGuard에 연결된 클라이언트의 IP 주소",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "AmneziaVPN 형식의 QR 코드 표시",
"showWireGuardQR": "WireGuard 형식의 QR 코드 표시"
}, },
"dialog": { "dialog": {
"change": "변경", "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", "endpointDesc": "IP van de cliënt vanaf welke de WireGuard-verbinding tot stand wordt gebracht",
"search": "Cliënten zoeken...", "search": "Cliënten zoeken...",
"config": "Configuratie", "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": { "dialog": {
"change": "Wijzigen", "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ą)", "dnsDesc": "Serwer DNS, którego będą używać klienci (nadpisuje konfigurację globalną)",
"notConnected": "Klient nie jest połączony", "notConnected": "Klient nie jest połączony",
"endpoint": "Punkt końcowy", "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": { "dialog": {
"change": "Zmień", "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)", "dnsDesc": "Os clientes do servidor DNS usarão (substitui a configuração global)",
"notConnected": "Client não conectado", "notConnected": "Client não conectado",
"endpoint": "Endpoint", "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": { "dialog": {
"change": "Mudar", "change": "Mudar",

5
src/i18n/locales/ru.json

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

5
src/i18n/locales/tr.json

@ -116,7 +116,10 @@
"dnsDesc": "İstemcilerin kullanacağı DNS sunucusu (genel yapılandırmayı geçersiz kılar)", "dnsDesc": "İstemcilerin kullanacağı DNS sunucusu (genel yapılandırmayı geçersiz kılar)",
"notConnected": "İstemci bağlı değil", "notConnected": "İstemci bağlı değil",
"endpoint": "Uç Nokta", "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": { "dialog": {
"change": "Değiştir", "change": "Değiştir",

5
src/i18n/locales/uk.json

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

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

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

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

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

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

@ -120,7 +120,10 @@
"endpointDesc": "用戶端建立 WireGuard 連線的來源 IP", "endpointDesc": "用戶端建立 WireGuard 連線的來源 IP",
"search": "搜尋用戶端...", "search": "搜尋用戶端...",
"config": "組態設定", "config": "組態設定",
"viewConfig": "檢視組態設定" "viewConfig": "檢視組態設定",
"amneziaVpn": "AmneziaVPN",
"showAmneziaVpnQR": "以 AmneziaVPN 格式顯示 QR 碼",
"showWireGuardQR": "以 WireGuard 格式顯示 QR 碼"
}, },
"dialog": { "dialog": {
"change": "變更", "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( export default definePermissionEventHandler(
'clients', 'clients',
@ -8,11 +8,15 @@ export default definePermissionEventHandler(
event, event,
validateZod(ClientGetSchema, event) validateZod(ClientGetSchema, event)
); );
const { type } = await getValidatedQuery(
event,
validateZod(ClientQrSchema, event)
);
const client = await Database.clients.get(clientId); const client = await Database.clients.get(clientId);
checkPermissions(client); checkPermissions(client);
const svg = await WireGuard.getClientQRCodeSVG({ clientId }); const svg = await WireGuard.getClientQRCodeSVG({ clientId, type });
setHeader(event, 'Content-Type', 'image/svg+xml'); setHeader(event, 'Content-Type', 'image/svg+xml');
return svg; return svg;
} }

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

@ -103,3 +103,12 @@ export type ClientCreateFromExistingType = Pick<
| 'publicKey' | 'publicKey'
| 'enabled' | '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; return clients;
} }
async getClientConfiguration({ clientId }: { clientId: ID }) { async getClientConfiguration({ clientId, type }: { clientId: ID, type?: 'amnezia-vpn' }) {
const wgInterface = await Database.interfaces.get(); const wgInterface = await Database.interfaces.get();
const userConfig = await Database.userConfigs.get(); const userConfig = await Database.userConfigs.get();
@ -159,13 +159,22 @@ class WireGuard {
throw new Error('Client not found'); throw new Error('Client not found');
} }
return wg.generateClientConfig(wgInterface, userConfig, client, { const configText = wg.generateClientConfig(wgInterface, userConfig, client, {
enableIpv6: !WG_ENV.DISABLE_IPV6, 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 }) { async getClientQRCodeSVG({ clientId, type }: { clientId: ID, type?: 'amnezia-vpn' }) {
const config = await this.getClientConfiguration({ clientId }); const config = await this.getClientConfiguration({ clientId, type });
const ECMode = ['high', 'quartile', 'medium', 'low'] as const; const ECMode = ['high', 'quartile', 'medium', 'low'] as const;
for (const ecc of ECMode) { for (const ecc of ECMode) {
try { try {

107
src/server/utils/wgHelper.ts

@ -1,3 +1,4 @@
import { deflateSync as zlibDeflateSync } from 'node:zlib';
import { parseCidr } from 'cidr-tools'; import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint'; import { stringifyIp } from 'ip-bigint';
import type { ClientType } from '#db/repositories/client/types'; import type { ClientType } from '#db/repositories/client/types';
@ -164,6 +165,112 @@ PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${userConfig.host}:${userConfig.port}`; 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: () => { generatePrivateKey: () => {
return exec(`${wgExecutable} genkey`); return exec(`${wgExecutable} genkey`);
}, },

Loading…
Cancel
Save