fix: WireGuard deploy — RCI API с правильным парсингом auth и nwg50+ нумерацией

- Парсим realm+challenge из JSON-тела GET /auth (не из HTTP-заголовков)
- Вычисляем хеш: MD5("admin:realm:pass") → MD5(hash+challenge)
- Нумерация интерфейсов начинается с 50 (nwg50, nwg51...) чтобы не затирать существующие
- Fallback: ip link add nwgN + wg set + init.d S50hydravpn при неудаче RCI
This commit is contained in:
Андрей Бобырев
2026-04-28 16:10:25 +03:00
parent c00830f61a
commit 31c3883ebd

View File

@@ -710,11 +710,10 @@ async def wg_deploy(name: str, x_admin_password: str = Header("")):
port = srv.get("port", 51820) port = srv.get("port", 51820)
client_ip = peer["ip"] client_ip = peer["ip"]
# Build RCI payloads in Python (safe JSON, no shell escaping issues) # RCI template JSON — WireguardIDX is a placeholder replaced by sed at runtime
def _rci_iface(idx: int) -> str: template_rci = _json.dumps({
return _json.dumps({
"interface": { "interface": {
f"Wireguard{idx}": { "WireguardIDX": {
"description": "HydraVPN", "description": "HydraVPN",
"up": True, "up": True,
"address": [{"ip": client_ip, "mask": "255.255.255.255"}], "address": [{"ip": client_ip, "mask": "255.255.255.255"}],
@@ -730,16 +729,9 @@ async def wg_deploy(name: str, x_admin_password: str = Header("")):
} }
} }
}) })
rci_jsons = {i: _rci_iface(i) for i in range(6)}
rci_save = '{"system":{"configuration":{"save":true}}}' rci_save = '{"system":{"configuration":{"save":true}}}'
router_pass = (rcfg.get("password") or config.SSH_PASS).replace("'", r"'\''") router_pass = (rcfg.get("password") or config.SSH_PASS).replace("'", r"'\''")
# Embed JSON payloads into the script as heredoc files
rci_files = ""
for i, payload in rci_jsons.items():
rci_files += f"cat > /tmp/wg_rci_{i}.json << 'RCI{i}EOF'\n{payload}\nRCI{i}EOF\n"
script = f"""\ script = f"""\
echo '[1/3] Установка wireguard-tools...' echo '[1/3] Установка wireguard-tools...'
opkg update 2>/dev/null || true opkg update 2>/dev/null || true
@@ -754,31 +746,21 @@ chmod 600 /opt/etc/wireguard/wg0.conf
printf '%s\\n' '{priv_key}' > /opt/etc/wireguard/wg0.key printf '%s\\n' '{priv_key}' > /opt/etc/wireguard/wg0.key
chmod 600 /opt/etc/wireguard/wg0.key chmod 600 /opt/etc/wireguard/wg0.key
cat > /opt/etc/init.d/S50wg0 << 'INITEOF' # RCI шаблон (WireguardIDX заменяется sed'ом ниже)
#!/bin/sh cat > /tmp/wg_tmpl.json << 'TMPLEOF'
case "$1" in {template_rci}
start) TMPLEOF
ip link show wg0 >/dev/null 2>&1 || ip link add wg0 type wireguard 2>/dev/null || exit 0
wg set wg0 private-key /opt/etc/wireguard/wg0.key \\
peer '{pub_key}' allowed-ips 0.0.0.0/0 \\
endpoint '{vps_host}:{port}' persistent-keepalive 25
ip addr add '{client_ip}/32' dev wg0 2>/dev/null || true
ip link set up dev wg0
;;
stop) ip link set down dev wg0 2>/dev/null; ip link delete wg0 2>/dev/null ;;
restart) $0 stop; sleep 1; $0 start ;;
esac
INITEOF
chmod +x /opt/etc/init.d/S50wg0
echo '[3/3] Настройка WireGuard...' echo '[3/3] Настройка WireGuard...'
DONE=0 DONE=0
# === Вариант 1: Keenetic RCI HTTP API (нативно → появится в Другие подключения) === # === Keenetic RCI API (нативная интеграция) ===
{rci_files} # /auth возвращает JSON с realm и challenge в теле ответа
AUTH_HDR=$(curl -si http://localhost/auth 2>/dev/null) AUTH=$(curl -s http://localhost/auth 2>/dev/null)
REALM=$(printf '%s' "$AUTH_HDR" | grep -i 'x-ndm-realm' | tail -1 | tr -d '\\r\\n' | awk -F': ' '{{print $2}}') echo "Auth: $AUTH"
CHAL=$(printf '%s' "$AUTH_HDR" | grep -i 'x-ndm-challenge' | tail -1 | tr -d '\\r\\n' | awk -F': ' '{{print $2}}') REALM=$(printf '%s' "$AUTH" | grep -o '"realm":"[^"]*"' | cut -d'"' -f4)
CHAL=$(printf '%s' "$AUTH" | grep -o '"challenge":"[^"]*"' | cut -d'"' -f4)
echo "Realm=[$REALM] Challenge=[$CHAL]"
if [ -n "$REALM" ] && [ -n "$CHAL" ]; then if [ -n "$REALM" ] && [ -n "$CHAL" ]; then
HASH=$(printf 'admin:%s:{router_pass}' "$REALM" | md5sum | cut -c1-32) HASH=$(printf 'admin:%s:{router_pass}' "$REALM" | md5sum | cut -c1-32)
@@ -786,51 +768,71 @@ if [ -n "$REALM" ] && [ -n "$CHAL" ]; then
LOGIN=$(curl -s -c /tmp/rci.jar http://localhost/auth \\ LOGIN=$(curl -s -c /tmp/rci.jar http://localhost/auth \\
-H 'Content-Type: application/json' \\ -H 'Content-Type: application/json' \\
-d '{{"login":"admin","password":"'"$RESP"'"}}') -d '{{"login":"admin","password":"'"$RESP"'"}}')
echo "RCI auth: $LOGIN" echo "Login: $LOGIN"
if printf '%s' "$LOGIN" | grep -qi 'success\\|true'; then # Найти свободный слот: проверяем nwg50, nwg51... (nwgN = WireguardN)
for WG_IDX in 2 3 4 5 0 1; do WG_IDX=50
IFACE=$(curl -s -b /tmp/rci.jar \\ while wg show nwg${{WG_IDX}} >/dev/null 2>&1; do
"http://localhost/rci/show/interface/Wireguard${{WG_IDX}}/" 2>/dev/null) echo "nwg${{WG_IDX}} занят, пробуем nwg$((WG_IDX+1))..."
if printf '%s' "$IFACE" | grep -q '"description"\\|"type"'; then WG_IDX=$((WG_IDX+1))
echo "Wireguard${{WG_IDX}} занят, пробуем следующий..." done
continue echo "Используем Wireguard${{WG_IDX}} (nwg${{WG_IDX}})"
fi
sed "s/WireguardIDX/Wireguard${{WG_IDX}}/" /tmp/wg_tmpl.json > /tmp/wg_rci.json
CFG=$(curl -s -b /tmp/rci.jar -X POST http://localhost/rci/ \\ CFG=$(curl -s -b /tmp/rci.jar -X POST http://localhost/rci/ \\
-H 'Content-Type: application/json' \\ -H 'Content-Type: application/json' \\
--data-binary @/tmp/wg_rci_${{WG_IDX}}.json) --data-binary @/tmp/wg_rci.json)
echo "RCI config Wireguard${{WG_IDX}}: $CFG" echo "Config: $CFG"
if ! printf '%s' "$CFG" | grep -qi '"error"\\|"code":-'; then
curl -s -b /tmp/rci.jar -X POST http://localhost/rci/ \\ curl -s -b /tmp/rci.jar -X POST http://localhost/rci/ \\
-H 'Content-Type: application/json' \\ -H 'Content-Type: application/json' \\
-d '{rci_save}' >/dev/null -d '{rci_save}' >/dev/null
DONE=1 DONE=1
echo "OK: Wireguard${{WG_IDX}} создан в Keenetic (проверь Другие подключения)" echo "OK: Wireguard${{WG_IDX}} (nwg${{WG_IDX}}) создан в Keenetic"
break else
done echo "RCI вернул ошибку"
fi fi
fi fi
rm -f /tmp/rci.jar /tmp/wg_rci_*.json 2>/dev/null rm -f /tmp/rci.jar /tmp/wg_rci.json /tmp/wg_tmpl.json 2>/dev/null
# === Вариант 2: raw wg + ip (Entware wg0, без интеграции в UI) === # === Fallback: wg + ip (nwg50+, без UI интеграции) ===
if [ "$DONE" = "0" ]; then if [ "$DONE" = "0" ]; then
echo 'RCI не сработал, используем wg + ip...' echo 'RCI не сработал wg + ip...'
ip link show wg0 >/dev/null 2>&1 || ip link add wg0 type wireguard 2>/dev/null || true WG_IDX=50
wg set wg0 private-key /opt/etc/wireguard/wg0.key \\ while wg show nwg${{WG_IDX}} >/dev/null 2>&1; do
WG_IDX=$((WG_IDX+1))
done
ip link add nwg${{WG_IDX}} type wireguard 2>/dev/null || true
wg set nwg${{WG_IDX}} private-key /opt/etc/wireguard/wg0.key \\
peer '{pub_key}' allowed-ips 0.0.0.0/0 \\ peer '{pub_key}' allowed-ips 0.0.0.0/0 \\
endpoint '{vps_host}:{port}' persistent-keepalive 25 && DONE=1 || true endpoint '{vps_host}:{port}' persistent-keepalive 25 && DONE=1 || true
ip addr add '{client_ip}/32' dev wg0 2>/dev/null || true ip addr add '{client_ip}/32' dev nwg${{WG_IDX}} 2>/dev/null || true
ip link set up dev wg0 2>/dev/null || true ip link set up dev nwg${{WG_IDX}} 2>/dev/null || true
if [ "$DONE" = "1" ]; then
echo 'OK: wg0 поднят (VPN работает, но не виден в Keenetic UI)' cat > /opt/etc/init.d/S50hydravpn << INITEOF
echo 'Чтобы добавить в UI вручную: Другие подключения → Загрузить из файла' #!/bin/sh
echo 'Файл конфига: /opt/etc/wireguard/wg0.conf' case "\\$1" in
fi start)
ip link show nwg${{WG_IDX}} >/dev/null 2>&1 || ip link add nwg${{WG_IDX}} type wireguard 2>/dev/null || exit 0
wg set nwg${{WG_IDX}} private-key /opt/etc/wireguard/wg0.key \\
peer '{pub_key}' allowed-ips 0.0.0.0/0 \\
endpoint '{vps_host}:{port}' persistent-keepalive 25
ip addr add '{client_ip}/32' dev nwg${{WG_IDX}} 2>/dev/null || true
ip link set up dev nwg${{WG_IDX}}
;;
stop) ip link set down dev nwg${{WG_IDX}} 2>/dev/null; ip link delete nwg${{WG_IDX}} 2>/dev/null ;;
restart) \\$0 stop; sleep 1; \\$0 start ;;
esac
INITEOF
chmod +x /opt/etc/init.d/S50hydravpn
[ "$DONE" = "1" ] && echo "OK: nwg${{WG_IDX}} поднят (VPN работает, UI: Загрузить из файла /opt/etc/wireguard/wg0.conf)"
fi fi
[ "$DONE" = "0" ] && {{ echo 'ОШИБКА: VPN не удалось настроить'; exit 1; }} [ "$DONE" = "0" ] && {{ echo 'ОШИБКА: VPN не удалось настроить'; exit 1; }}
echo '=== OK ===' echo '=== OK ==='
echo 'VPN IP: {client_ip}' echo 'VPN IP: {client_ip}'
wg show 2>/dev/null | head -10 || ip addr show wg0 2>/dev/null | grep inet || true wg show 2>/dev/null | head -12 || true
""" """
rc, out, err = await _ssh_on_router(rcfg, script, timeout=120) rc, out, err = await _ssh_on_router(rcfg, script, timeout=120)