diff --git a/inc/VpnClient.php b/inc/VpnClient.php index 043eca6..d90ea2b 100644 --- a/inc/VpnClient.php +++ b/inc/VpnClient.php @@ -199,12 +199,22 @@ class VpnClient $defaultAwgParams = self::getAwgParamDefaults($slug); - // Add AWG parameters (use UPPERCASE keys internal logic) - foreach (array_keys($defaultAwgParams) as $key) { - if (isset($cleanAwgParams[$key])) { - $vars[$key] = $cleanAwgParams[$key]; - } else { - $vars[$key] = $defaultAwgParams[$key]; + // AmneziaWG requires the client's obfuscation params to EXACTLY match + // the server's. When the server's params are known (synced from its + // config), mirror them verbatim and leave anything the server does not + // use empty (those lines are stripped from the rendered config below). + // Only fall back to protocol defaults when NO params were synced at all + // (best effort) — injecting AWG 2.0 defaults (H1-H4 ranges, S3/S4, I1) + // onto a classic AmneziaWG server silently breaks the handshake. + // (issue #50) + $awgKeys = ['JC', 'JMIN', 'JMAX', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']; + if (!empty($cleanAwgParams)) { + foreach ($awgKeys as $key) { + $vars[$key] = array_key_exists($key, $cleanAwgParams) ? $cleanAwgParams[$key] : ''; + } + } else { + foreach ($awgKeys as $key) { + $vars[$key] = $defaultAwgParams[$key] ?? ''; } } @@ -243,6 +253,18 @@ class VpnClient ); } + // Drop AWG obfuscation lines that ended up empty (params the server + // does not use, e.g. S3/S4/I1-I5 on a classic AmneziaWG server). An + // empty "S3 =" / "I1 =" line is invalid, and any param the client + // carries but the server lacks breaks the handshake. (issue #50) + if ($isWireguard) { + $config = preg_replace( + '/^[ \t]*(?:Jc|Jmin|Jmax|S1|S2|S3|S4|H1|H2|H3|H4|I1|I2|I3|I4|I5)[ \t]*=[ \t]*\r?\n/mi', + '', + $config + ); + } + self::addClientToServer($serverData, $keys['public'], $clientIP); $qrCode = self::generateQRCode($config, $slug); $priv = $keys['private']; @@ -1064,37 +1086,32 @@ class VpnClient $dns = '1.1.1.1, 1.0.0.1'; } - // Extract AWG parameters. - // NOTE: amnezia-awg does not expose these via `wg show` in many builds, - // so we primarily read them from /opt/amnezia/awg/wg0.conf. + // Extract AWG obfuscation parameters. The server config file is the + // source of truth: it holds the EXACT params awg-quick applied, in the + // server's own format (single-value H1-H4 for classic AmneziaWG, or + // "a-b" ranges for AWG 2.0) and only the params the server actually + // uses. Client configs must mirror these exactly or the AmneziaWG + // handshake silently fails (issue #50). Inside the container the + // config always lives under /opt/amnezia/awg/. $awgParams = []; - - // Legacy attempt: some builds print jc/jmin/... in `wg show` output. - $wgShowCmd = "docker exec $containerName wg show wg0 2>/dev/null"; - $wgOutput = (string) $server->executeCommand($wgShowCmd, true); - $paramNames = ['jc', 'jmin', 'jmax', 's1', 's2', 's3', 's4', 'h1', 'h2', 'h3', 'h4', 'i1', 'i2', 'i3', 'i4', 'i5']; - foreach ($paramNames as $param) { - // For H1-H4 parameters, expect format like "1443912531-1981073285" (two values with dash) - // For other parameters, expect single integer value - if (in_array($param, ['h1', 'h2', 'h3', 'h4'], true)) { - if (preg_match('/^\s*' . preg_quote($param, '/') . ':\s*(\d+-\d+)/mi', $wgOutput, $matches)) { - $awgParams[strtoupper($param)] = $matches[1]; - } - } else { - if (preg_match('/^\s*' . preg_quote($param, '/') . ':\s*(\d+)/mi', $wgOutput, $matches)) { - $awgParams[strtoupper($param)] = (int) $matches[1]; - } + foreach (['/opt/amnezia/awg/awg0.conf', '/opt/amnezia/awg/wg0.conf', '/etc/wireguard/wg0.conf'] as $confPath) { + $awgParams = self::extractAwgParamsFromWg0Conf($server, $containerName, $confPath); + if (!empty($awgParams)) { + break; } } - // Primary source: wg0.conf + // Fallback: some builds only expose params via `wg show` (no readable + // config file). Accept both single integers and "a-b" ranges so + // classic AmneziaWG H1-H4 values are not dropped. if (empty($awgParams)) { - $awgParams = self::extractAwgParamsFromWg0Conf($server, $containerName, $primaryConfigDir . '/wg0.conf'); - if (empty($awgParams) && $primaryConfigDir !== '/opt/amnezia/awg') { - $awgParams = self::extractAwgParamsFromWg0Conf($server, $containerName, '/opt/amnezia/awg/wg0.conf'); - } - if (empty($awgParams)) { - $awgParams = self::extractAwgParamsFromWg0Conf($server, $containerName, '/etc/wireguard/wg0.conf'); + $wgShowCmd = "docker exec $containerName wg show wg0 2>/dev/null"; + $wgOutput = (string) $server->executeCommand($wgShowCmd, true); + foreach (['jc', 'jmin', 'jmax', 's1', 's2', 's3', 's4', 'h1', 'h2', 'h3', 'h4', 'i1', 'i2', 'i3', 'i4', 'i5'] as $param) { + if (preg_match('/^\s*' . preg_quote($param, '/') . ':\s*(\d+(?:-\d+)?)/mi', $wgOutput, $matches)) { + $val = $matches[1]; + $awgParams[strtoupper($param)] = ctype_digit($val) ? (int) $val : $val; + } } }