Compare commits
5 Commits
b819eb35b0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| baa3ef5f76 | |||
| 24a6cb276f | |||
| 222953049d | |||
| d771af866c | |||
| 0d72579edd |
+116
-46
@@ -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];
|
||||
// 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 {
|
||||
$vars[$key] = $defaultAwgParams[$key];
|
||||
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'];
|
||||
@@ -797,19 +819,20 @@ class VpnClient
|
||||
private static function generateClientKeys(array $serverData, string $clientName): array
|
||||
{
|
||||
$containerName = $serverData['container_name'];
|
||||
$protocolSlug = (string) ($serverData['install_protocol'] ?? '');
|
||||
$isAwg2 = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2');
|
||||
// The amneziawg-go image ships `awg` and a `wg -> awg` symlink, so either
|
||||
// tool works there. Use `awg` for AWG2 and `wg` otherwise.
|
||||
$wgTool = $isAwg2 ? 'awg' : 'wg';
|
||||
|
||||
// Inner script that runs inside the container shell. Generates a private
|
||||
// key, derives the public key and prints them separated by a "---" marker.
|
||||
$script = sprintf(
|
||||
'set -e; umask 077; priv=$(%s genkey | tr -d "\r\n"); [ -n "$priv" ] || { echo empty_private_key; exit 1; }; pub=$(printf "%%s\n" "$priv" | %s pubkey | tr -d "\r\n"); [ -n "$pub" ] || { echo empty_public_key; exit 1; }; printf "%%s\n---\n%%s\n" "$priv" "$pub"',
|
||||
$wgTool,
|
||||
$wgTool
|
||||
);
|
||||
// Detect the WireGuard userspace tool INSIDE the container instead of
|
||||
// hardcoding it. Different AWG2 images expose it under different names:
|
||||
// the official Amnezia image ships only `wg` (a patched AmneziaWG binary),
|
||||
// while amneziawg-go provides `awg` (with `wg` symlinked to it). Hardcoding
|
||||
// `awg` made `awg genkey` fail with "awg: not found" on the Amnezia image,
|
||||
// which is the actual cause of the "Failed to generate client keys" error
|
||||
// in issue #50. Prefer `awg`, fall back to `wg`.
|
||||
$script = 'set -e; umask 077; '
|
||||
. 'tool=$(command -v awg 2>/dev/null || command -v wg 2>/dev/null); '
|
||||
. '[ -n "$tool" ] || { echo no_wg_tool; exit 1; }; '
|
||||
. 'priv=$("$tool" genkey | tr -d "\r\n"); [ -n "$priv" ] || { echo empty_private_key; exit 1; }; '
|
||||
. 'pub=$(printf "%s\n" "$priv" | "$tool" pubkey | tr -d "\r\n"); [ -n "$pub" ] || { echo empty_public_key; exit 1; }; '
|
||||
. 'printf "%s\n---\n%s\n" "$priv" "$pub"';
|
||||
|
||||
$cmd = sprintf(
|
||||
'docker exec -i %s sh -lc %s',
|
||||
@@ -1063,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 = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy attempt: some builds print jc/jmin/... in `wg show` output.
|
||||
// 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)) {
|
||||
$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];
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
if (preg_match('/^\s*' . preg_quote($param, '/') . ':\s*(\d+)/mi', $wgOutput, $matches)) {
|
||||
$awgParams[strtoupper($param)] = (int) $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Primary source: wg0.conf
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,6 +1182,10 @@ class VpnClient
|
||||
$config .= "Address = {$clientIP}/32\n";
|
||||
$config .= "DNS = 1.1.1.1, 1.0.0.1\n";
|
||||
$config .= "PrivateKey = {$privateKey}\n";
|
||||
// AmneziaWG obfuscation adds per-packet overhead; without a reduced MTU
|
||||
// the tunnel connects but large packets are dropped (no usable traffic).
|
||||
// 1280 matches the official Amnezia app default. (issue #50)
|
||||
$config .= "MTU = 1280\n";
|
||||
|
||||
// Add AWG parameters (in the order used by Amnezia app)
|
||||
// For awg2 include I1-I5, S3, S4; for regular awg only H1-H4, Jc, Jmin, Jmax, S1, S2
|
||||
@@ -1223,9 +1245,12 @@ class VpnClient
|
||||
throw new Exception('Refusing to add client with empty public key');
|
||||
}
|
||||
|
||||
// Determine correct tool names (awg for AWG2, wg for standard)
|
||||
$wgTool = $isAwg2 ? 'awg' : 'wg';
|
||||
$wgQuickTool = $isAwg2 ? 'awg-quick' : 'wg-quick';
|
||||
// Determine correct tool names by probing the container. The official
|
||||
// Amnezia image exposes only `wg`/`wg-quick`; amneziawg-go provides
|
||||
// `awg`/`awg-quick`. Hardcoding `awg` broke peer setup on the Amnezia
|
||||
// image (issue #50). Prefer `awg`, fall back to `wg`.
|
||||
$wgTool = $isAwg2 ? self::resolveWgTool($serverData, $containerName) : 'wg';
|
||||
$wgQuickTool = $wgTool . '-quick';
|
||||
|
||||
// 1. Create temp file for PSK (to avoid shell escaping issues)
|
||||
$pskFile = '/tmp/' . bin2hex(random_bytes(8)) . '.psk';
|
||||
@@ -1265,6 +1290,22 @@ class VpnClient
|
||||
// Without this, the interface uses standard WireGuard without Jc/S1/S2/H1-H4
|
||||
$cmd5 = sprintf("docker exec -i %s sh -c 'ip link del %s 2>/dev/null || true; %s up %s/%s 2>&1'", $containerName, $ifaceName, $wgQuickTool, $configDir, $configFile);
|
||||
self::executeServerCommand($serverData, $cmd5, true);
|
||||
|
||||
// 7. CRITICAL: Clamp TCP MSS so download-direction packets fit the reduced
|
||||
// AmneziaWG tunnel MTU (clients use MTU 1280 -> MSS 1240). Without this the
|
||||
// handshake succeeds and small packets flow, but large packets (web pages,
|
||||
// TLS responses) exceed the tunnel and are silently dropped — the classic
|
||||
// "connected but no traffic" symptom (issue #50). Idempotent (-C then -A),
|
||||
// and ip_forward is ensured for good measure. Re-applied on every client
|
||||
// creation so it survives container restarts/reinstalls.
|
||||
$mssRule = "-p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1240";
|
||||
$cmd6 = sprintf(
|
||||
"docker exec -i %s sh -c 'sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true; iptables -t mangle -C FORWARD %s 2>/dev/null || iptables -t mangle -A FORWARD %s'",
|
||||
$containerName,
|
||||
$mssRule,
|
||||
$mssRule
|
||||
);
|
||||
self::executeServerCommand($serverData, $cmd6, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1302,11 +1343,40 @@ class VpnClient
|
||||
self::executeServerCommand($serverData, $updateCmd, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the WireGuard userspace tool name available inside a container.
|
||||
* Returns 'awg' when present, otherwise 'wg'. Used so AWG2 works on both the
|
||||
* official Amnezia image (ships `wg`) and amneziawg-go (ships `awg`).
|
||||
*/
|
||||
private static function resolveWgTool(array $serverData, string $containerName): string
|
||||
{
|
||||
$probe = sprintf(
|
||||
"docker exec -i %s sh -lc 'command -v awg >/dev/null 2>&1 && echo awg || echo wg'",
|
||||
escapeshellarg($containerName)
|
||||
);
|
||||
$tool = trim(self::executeServerCommand($serverData, $probe, true));
|
||||
return $tool === 'awg' ? 'awg' : 'wg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute command on server
|
||||
*/
|
||||
private static function executeServerCommand(array $serverData, string $command, bool $sudo = false): string
|
||||
{
|
||||
// Delegate to VpnServer::executeCommand so SSH key authentication, docker
|
||||
// sudo auto-detection and retry logic are shared with the rest of the
|
||||
// panel. The previous inline implementation was password-only and failed
|
||||
// on key-based servers (contributing to issue #50).
|
||||
if (!empty($serverData['id'])) {
|
||||
try {
|
||||
$server = new VpnServer((int) $serverData['id']);
|
||||
return $server->executeCommand($command, $sudo ? null : false);
|
||||
} catch (Exception $e) {
|
||||
error_log('executeServerCommand: delegate failed, using legacy path: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback (no server id in $serverData): password-only SSH.
|
||||
$needsSudo = $sudo && strtolower((string) ($serverData['username'] ?? '')) !== 'root';
|
||||
$baseCommand = $command;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ SET output_template = '[Interface]
|
||||
Address = {{client_ip}}/32
|
||||
DNS = {{dns_servers}}
|
||||
PrivateKey = {{private_key}}
|
||||
MTU = 1280
|
||||
Jc = {{Jc}}
|
||||
Jmin = {{Jmin}}
|
||||
Jmax = {{Jmax}}
|
||||
@@ -144,8 +145,8 @@ echo "H2 = $H2_VAL"
|
||||
echo "H3 = $H3_VAL"
|
||||
echo "H4 = $H4_VAL"
|
||||
echo "I1 = $I1_VAL"
|
||||
echo "PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE"
|
||||
echo "PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE"
|
||||
echo "PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1240"
|
||||
echo "PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; iptables -t mangle -D FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1240"
|
||||
} > /opt/amnezia/awg2/awg0.conf
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg2/wireguard_server_private_key.key
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- =====================================================================
|
||||
-- Migration 071: Restore client MTU for AmneziaWG 2.0 (awg2)
|
||||
--
|
||||
-- Issue #50: clients connect (handshake succeeds) but no traffic flows.
|
||||
-- Root cause: the awg2 client output_template lost its "MTU = 1280" line
|
||||
-- when migration 064 rewrote it (migration 058 had it). With no explicit
|
||||
-- MTU the client defaults to 1420, which is too large once AmneziaWG
|
||||
-- obfuscation overhead (Jc junk packets, S1/S2 padding) is added on top of
|
||||
-- WireGuard's own overhead: the handshake (small packets) succeeds, but
|
||||
-- larger packets (TLS, web pages) exceed the path and are dropped — so the
|
||||
-- tunnel is "connected" yet carries no usable traffic. 1280 is the value the
|
||||
-- official Amnezia app uses for AmneziaWG clients.
|
||||
-- =====================================================================
|
||||
|
||||
UPDATE protocols
|
||||
SET output_template = REPLACE(
|
||||
output_template,
|
||||
'PrivateKey = {{private_key}}\n',
|
||||
'PrivateKey = {{private_key}}\nMTU = 1280\n'
|
||||
)
|
||||
WHERE slug = 'awg2'
|
||||
AND output_template LIKE '%PrivateKey = {{private_key}}%'
|
||||
AND output_template NOT LIKE '%MTU%';
|
||||
@@ -0,0 +1,25 @@
|
||||
-- =====================================================================
|
||||
-- Migration 072: TCP MSS clamping for AmneziaWG 2.0 (awg2)
|
||||
--
|
||||
-- Issue #50: clients connect (handshake OK) but no traffic flows. With the
|
||||
-- reduced tunnel MTU (clients use 1280), TCP must also negotiate a small
|
||||
-- enough MSS, otherwise full-size download packets (web pages, TLS responses)
|
||||
-- exceed the tunnel and are dropped — the handshake and small packets work,
|
||||
-- but browsing stalls. Clamping MSS to 1240 (1280 - 40) on the server's
|
||||
-- FORWARD path fixes the download direction.
|
||||
--
|
||||
-- This appends the clamp to the awg2 install script's PostUp so panel-installed
|
||||
-- servers get it on every interface bring-up. (Adopted native containers are
|
||||
-- handled at runtime by VpnClient::addClientToServer(), which applies the same
|
||||
-- rule idempotently on each client creation.)
|
||||
-- =====================================================================
|
||||
|
||||
UPDATE protocols
|
||||
SET install_script = REPLACE(
|
||||
install_script,
|
||||
'iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE',
|
||||
'iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1240'
|
||||
)
|
||||
WHERE slug = 'awg2'
|
||||
AND install_script LIKE '%-A POSTROUTING -o eth0 -j MASQUERADE%'
|
||||
AND install_script NOT LIKE '%TCPMSS%';
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
# =====================================================================
|
||||
# Downgrade an AmneziaWG server's obfuscation to a "classic" (AmneziaWG 1.0)
|
||||
# set that older router AmneziaWG implementations accept.
|
||||
#
|
||||
# Keeps : Jc, Jmin, Jmax, S1, S2 (widely supported AWG 1.0 junk params)
|
||||
# Converts: H1-H4 from "a-b" ranges -> single value "a"
|
||||
# Drops : S3, S4 and I1-I5 (AWG 1.5/2.0-only padding & magic packets)
|
||||
#
|
||||
# After running this you MUST regenerate every client config in the panel
|
||||
# (create new clients / re-export) and re-import them on phones too — the old
|
||||
# AWG 2.0 client configs no longer match the server and will stop connecting.
|
||||
#
|
||||
# Usage (on the VPS host that runs the container):
|
||||
# sh awg_downgrade_obfuscation.sh [container_name]
|
||||
# Defaults to container "amnezia-awg2".
|
||||
# =====================================================================
|
||||
set -e
|
||||
|
||||
CONTAINER="${1:-amnezia-awg2}"
|
||||
|
||||
if ! docker inspect "$CONTAINER" >/dev/null 2>&1; then
|
||||
echo "Container '$CONTAINER' not found. Pass the correct name as the 1st arg." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Locate the config inside the container (awg0.conf for AWG2, wg0.conf legacy).
|
||||
CONF=""
|
||||
for f in /opt/amnezia/awg/awg0.conf /opt/amnezia/awg/wg0.conf /etc/wireguard/wg0.conf; do
|
||||
if docker exec "$CONTAINER" test -f "$f" 2>/dev/null; then CONF="$f"; break; fi
|
||||
done
|
||||
[ -n "$CONF" ] || { echo "WireGuard config not found inside $CONTAINER" >&2; exit 1; }
|
||||
|
||||
echo "Container : $CONTAINER"
|
||||
echo "Config : $CONF"
|
||||
echo "Before:"
|
||||
docker exec "$CONTAINER" sh -c "grep -E '^(Jc|Jmin|Jmax|S[0-9]|H[0-9]|I[0-9])[[:space:]]*=' '$CONF' || true"
|
||||
|
||||
# Rewrite the [Interface] obfuscation params, then reload the interface using
|
||||
# whichever tool the image provides (awg on amneziawg-go, wg on the Amnezia image).
|
||||
docker exec "$CONTAINER" sh -c '
|
||||
set -e
|
||||
CONF="'"$CONF"'"
|
||||
IFACE="$(basename "$CONF" .conf)"
|
||||
cp "$CONF" "${CONF}.bak" 2>/dev/null || true
|
||||
|
||||
# H1-H4: "a-b" -> "a"
|
||||
sed -i -E "s/^([[:space:]]*H[1-4][[:space:]]*=[[:space:]]*[0-9]+)-[0-9]+/\1/" "$CONF"
|
||||
# Drop S3, S4 and I1-I5 lines entirely
|
||||
sed -i -E "/^[[:space:]]*(S3|S4|I[1-5])[[:space:]]*=/d" "$CONF"
|
||||
|
||||
QUICK="$(command -v awg-quick || command -v wg-quick)"
|
||||
"$QUICK" down "$CONF" 2>/dev/null || "$QUICK" down "$IFACE" 2>/dev/null || true
|
||||
"$QUICK" up "$CONF"
|
||||
'
|
||||
|
||||
echo "After:"
|
||||
docker exec "$CONTAINER" sh -c "grep -E '^(Jc|Jmin|Jmax|S[0-9]|H[0-9]|I[0-9])[[:space:]]*=' '$CONF' || true"
|
||||
echo "Done. Now regenerate all client configs in the panel and re-import them."
|
||||
Reference in New Issue
Block a user