+
+
+ {{ $t('client.amneziaVpn') }}
+
+
+
{{ $t('dialog.cancel') }}
@@ -18,4 +29,6 @@
diff --git a/src/i18n/locales/bn.json b/src/i18n/locales/bn.json
index e8868342..90bb9b66 100644
--- a/src/i18n/locales/bn.json
+++ b/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": "পরিবর্তন করুন",
diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json
index 47adc8a2..3f39721d 100644
--- a/src/i18n/locales/de.json
+++ b/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",
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 17fc3f7f..45238bef 100644
--- a/src/i18n/locales/en.json
+++ b/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",
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
index 074eaf89..5e2d5afa 100644
--- a/src/i18n/locales/es.json
+++ b/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",
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index 18a0310c..d567746a 100644
--- a/src/i18n/locales/fr.json
+++ b/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",
diff --git a/src/i18n/locales/id.json b/src/i18n/locales/id.json
index bda9ad64..5e0f5fa3 100644
--- a/src/i18n/locales/id.json
+++ b/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",
diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json
index bf70a590..7bf9b512 100644
--- a/src/i18n/locales/it.json
+++ b/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",
diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json
index e712aa3c..a27663bc 100644
--- a/src/i18n/locales/ko.json
+++ b/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": "변경",
diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json
index a1e591d4..f92226a9 100644
--- a/src/i18n/locales/nl.json
+++ b/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",
diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json
index 85f11627..fe7317dd 100644
--- a/src/i18n/locales/pl.json
+++ b/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ń",
diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json
index d99a4e29..91709835 100644
--- a/src/i18n/locales/pt-BR.json
+++ b/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",
diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json
index be94543d..bb906307 100644
--- a/src/i18n/locales/ru.json
+++ b/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": "Изменить",
diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json
index 866ad487..d9852a6a 100644
--- a/src/i18n/locales/tr.json
+++ b/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",
diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json
index d47c645b..a3654f24 100644
--- a/src/i18n/locales/uk.json
+++ b/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": "Змінити",
diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json
index b602284f..2093fa9b 100644
--- a/src/i18n/locales/zh-CN.json
+++ b/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": "确认修改",
diff --git a/src/i18n/locales/zh-HK.json b/src/i18n/locales/zh-HK.json
index 41e7001b..2615c1e5 100644
--- a/src/i18n/locales/zh-HK.json
+++ b/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": "更改",
diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json
index 6253473c..afae945c 100644
--- a/src/i18n/locales/zh-TW.json
+++ b/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": "變更",
diff --git a/src/server/api/client/[clientId]/qrcode.svg.get.ts b/src/server/api/client/[clientId]/qrcode.svg.get.ts
index 6d25d577..e4c687a3 100644
--- a/src/server/api/client/[clientId]/qrcode.svg.get.ts
+++ b/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;
}
diff --git a/src/server/database/repositories/client/types.ts b/src/server/database/repositories/client/types.ts
index ae2ba852..266af0f2 100644
--- a/src/server/database/repositories/client/types.ts
+++ b/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,
+});
diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts
index 8b4ade14..e0644844 100644
--- a/src/server/utils/WireGuard.ts
+++ b/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 {
diff --git a/src/server/utils/wgHelper.ts b/src/server/utils/wgHelper.ts
index 607a0c51..22dc9a1d 100644
--- a/src/server/utils/wgHelper.ts
+++ b/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 = {};
+ 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`);
},