fix(awg2): mirror server obfuscation params in client config (fixes no-connect)
Clients were created successfully but could not connect: the AmneziaWG
handshake requires the client's obfuscation params (Jc/Jmin/Jmax/S1-S4/
H1-H4/I1-I5) to EXACTLY match the server's, and they did not.
Two causes, both fixed:
- syncServerKeysFromContainer() read params from `wg show` first and only
accepted H1-H4 in the AWG-2.0 "a-b" range format, dropping the single-value
H1-H4 used by classic AmneziaWG servers (the official Amnezia image). It
also skipped the complete wg0.conf read once `wg show` returned partial
data. Now the server config file (awg0.conf/wg0.conf) is the primary,
format-agnostic source; `wg show` is a fallback that accepts single values
and ranges.
- create() filled any param missing from the (incomplete) sync with awg2
defaults — injecting H1-H4 ranges, S3/S4 and I1 onto a classic server that
uses none of them. Now client params mirror the server's synced params
verbatim; defaults are used only when nothing was synced at all. Empty
AWG lines (params the server does not use) are stripped from the rendered
config so the client carries exactly the server's set.
Verified end-to-end on a live server: a real amneziawg-go client built from
the generated config completes the handshake
("latest handshake: 14 seconds ago", bidirectional transfer) — params
(jc/s1/s2/h1-h4) match the server exactly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+49
-32
@@ -199,12 +199,22 @@ class VpnClient
|
|||||||
|
|
||||||
$defaultAwgParams = self::getAwgParamDefaults($slug);
|
$defaultAwgParams = self::getAwgParamDefaults($slug);
|
||||||
|
|
||||||
// Add AWG parameters (use UPPERCASE keys internal logic)
|
// AmneziaWG requires the client's obfuscation params to EXACTLY match
|
||||||
foreach (array_keys($defaultAwgParams) as $key) {
|
// the server's. When the server's params are known (synced from its
|
||||||
if (isset($cleanAwgParams[$key])) {
|
// config), mirror them verbatim and leave anything the server does not
|
||||||
$vars[$key] = $cleanAwgParams[$key];
|
// use empty (those lines are stripped from the rendered config below).
|
||||||
} else {
|
// Only fall back to protocol defaults when NO params were synced at all
|
||||||
$vars[$key] = $defaultAwgParams[$key];
|
// (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);
|
self::addClientToServer($serverData, $keys['public'], $clientIP);
|
||||||
$qrCode = self::generateQRCode($config, $slug);
|
$qrCode = self::generateQRCode($config, $slug);
|
||||||
$priv = $keys['private'];
|
$priv = $keys['private'];
|
||||||
@@ -1064,37 +1086,32 @@ class VpnClient
|
|||||||
$dns = '1.1.1.1, 1.0.0.1';
|
$dns = '1.1.1.1, 1.0.0.1';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract AWG parameters.
|
// Extract AWG obfuscation parameters. The server config file is the
|
||||||
// NOTE: amnezia-awg does not expose these via `wg show` in many builds,
|
// source of truth: it holds the EXACT params awg-quick applied, in the
|
||||||
// so we primarily read them from /opt/amnezia/awg/wg0.conf.
|
// 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 = [];
|
$awgParams = [];
|
||||||
|
foreach (['/opt/amnezia/awg/awg0.conf', '/opt/amnezia/awg/wg0.conf', '/etc/wireguard/wg0.conf'] as $confPath) {
|
||||||
// Legacy attempt: some builds print jc/jmin/... in `wg show` output.
|
$awgParams = self::extractAwgParamsFromWg0Conf($server, $containerName, $confPath);
|
||||||
$wgShowCmd = "docker exec $containerName wg show wg0 2>/dev/null";
|
if (!empty($awgParams)) {
|
||||||
$wgOutput = (string) $server->executeCommand($wgShowCmd, true);
|
break;
|
||||||
$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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)) {
|
if (empty($awgParams)) {
|
||||||
$awgParams = self::extractAwgParamsFromWg0Conf($server, $containerName, $primaryConfigDir . '/wg0.conf');
|
$wgShowCmd = "docker exec $containerName wg show wg0 2>/dev/null";
|
||||||
if (empty($awgParams) && $primaryConfigDir !== '/opt/amnezia/awg') {
|
$wgOutput = (string) $server->executeCommand($wgShowCmd, true);
|
||||||
$awgParams = self::extractAwgParamsFromWg0Conf($server, $containerName, '/opt/amnezia/awg/wg0.conf');
|
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)) {
|
||||||
if (empty($awgParams)) {
|
$val = $matches[1];
|
||||||
$awgParams = self::extractAwgParamsFromWg0Conf($server, $containerName, '/etc/wireguard/wg0.conf');
|
$awgParams[strtoupper($param)] = ctype_digit($val) ? (int) $val : $val;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user