diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c2d52f..c95c132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ phobos-update beta # или: phobos-update dev ## stable ### v1.2.8 +- **fix:** Android PhobosWG больше не получает `PresharedKey = none` в phobos payload, из-за чего импорт мог падать с «Неправильная длина ключа в PublicKey». +- **fix:** клиентский router package теперь содержит обязательные `lib-client.sh`, `install-obfuscator.sh`, `install-wireguard.sh` и дополнительные helper-скрипты из PCA repo, поэтому `wget .../init/TOKEN.sh | sh` больше не падает с `can't open './lib-client.sh'`. - **fix:** поле имени клиента больше не даёт ошибку в консоли Chrome из-за HTML `pattern`. - **fix:** fresh stable install теперь кладёт обязательный `lib-core.sh` рядом с `phobos-client.sh`, поэтому `/clients` не сыпется на `check_root/load_env/die/log_*: command not found`. - **fix:** шаг `[6/9] PCA patches` больше не падает с `curl: (23) Failure writing output to destination`, если папки `repo/server/scripts` или `repo/client/templates` не были созданы upstream sparse clone. @@ -55,6 +57,8 @@ phobos-update beta # или: phobos-update dev |--------|----------|---------| | 1.2.7 / 1.3.x | PhobosWG: «невозможно импортировать туннель» | `phobos://` дополняется `= none` для всех обязательных полей (`PresharedKey`) по спеке формата | | 1.2.8 / 1.3.x | Stable не показывал кнопку скачивания APK без локального файла | кнопка **PhobosWG (APK)** всегда доступна на Android-странице; `/download/apk` ведет на public stable release | +| 1.2.8 / 1.3.x | Android PhobosWG: «Неправильная длина ключа в PublicKey» | Android payload нормализован без `PresharedKey = none`, QR/link нужно открыть заново | +| 1.2.8 / 1.3.x | Router init: `can't open './lib-client.sh'` | обязательные router helper-скрипты добавлены в PCA overlay и managed update | | 1.2.8 / 1.3.x | Stable `/clients`: `lib-core.sh: No such file or directory`, `check_root/load_env: command not found` | `lib-core.sh` добавлен в public repo, installer и updater | | 1.2.8 / 1.3.x | Install `[6/9] PCA patches`: `curl: (23) Failure writing output to destination` | `fetch()` создаёт папку назначения перед `curl -o` | | 1.2.8 / 1.3.x | VPS x86_64: `ERROR: obfuscator binary for x86_64 missing` | авто-загрузка `linux-x64` из ClusterM releases + нормализация `amd64 -> x86_64` | diff --git a/app.py b/app.py index 7d5c062..0d8b4a5 100644 --- a/app.py +++ b/app.py @@ -713,20 +713,17 @@ def build_phone_config(cid, mode="android"): if os.path.exists(instpath): inst = open(instpath).read().strip() conf = wg + "\n\n" + inst + "\n" - # PhobosWG requires ALL fields present; missing -> "= none" before base64 - # (spec 2.3). Ground-Zerro configs lack PresharedKey -> import fails otherwise. - padded = _pad_conf_with_none(conf) - b64 = base64.urlsafe_b64encode(padded.encode()).decode().rstrip("=") + payload = _prepare_phoboswg_payload(conf) + b64 = base64.urlsafe_b64encode(payload.encode()).decode() link = "phobos://" + b64 + "#" + urllib.parse.quote(cid or "none") - return conf, link + return payload, link -def _pad_conf_with_none(conf): - """Insert ' = none' for every missing mandatory PhobosWG field - (phobos:// spec 2.3). Only for phobos:// payload, NOT raw .conf.""" +def _prepare_phoboswg_payload(conf): + """Normalize Android PhobosWG phobos:// payload. Do not use for raw .conf.""" mandatory = { "[interface]": ["PrivateKey", "Address", "MTU", "DNS"], - "[peer]": ["PublicKey", "PresharedKey", "AllowedIPs", "PersistentKeepalive", "Endpoint"], + "[peer]": ["PublicKey", "AllowedIPs", "PersistentKeepalive", "Endpoint"], "[instance]": ["source-if", "source-lport", "target", "key", "masking", "verbose", "idle-timeout", "max-dummy"], } @@ -741,6 +738,27 @@ def _pad_conf_with_none(conf): cur[1].append(ln) out = [] for header, lines in sections: + normalized = [] + for ln in lines: + if "=" not in ln: + normalized.append(ln) + continue + key, value = ln.split("=", 1) + k = key.strip().lower() + v = value.strip() + if k == "presharedkey" and v.lower() in ("", "none"): + continue + if header.lower() == "[peer]" and k == "allowedips": + parts = [p.strip() for p in v.split(",") if p.strip()] + if "0.0.0.0/0" not in parts: + parts.insert(0, "0.0.0.0/0") + if "::/0" not in parts: + parts.append("::/0") + ln = "%s = %s" % (key.strip(), ", ".join(parts)) + elif header.lower() == "[instance]" and k == "masking" and v.upper() == "AUTO": + ln = "%s = STUN" % key.strip() + normalized.append(ln) + lines = normalized keys = {ln.split("=", 1)[0].strip().lower() for ln in lines if "=" in ln} while lines and lines[-1].strip() == "": lines.pop() @@ -753,6 +771,11 @@ def _pad_conf_with_none(conf): return "\n".join(out).rstrip("\n") + "\n" +def _pad_conf_with_none(conf): + """Legacy helper kept for old endpoints that may call it.""" + return _prepare_phoboswg_payload(conf) + + def qr_datauri(data): try: import qrcode, io, base64 diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index b045112..5670309 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -259,6 +259,26 @@ phobos-update stable Потом вставь новую команду установки на роутере. +### Команда на роутере пишет `line 8: can't open './lib-client.sh'` + +Причина: пакет клиента собран без обязательных helper-файлов (`lib-client.sh`, `install-obfuscator.sh`, `install-wireguard.sh`). Это происходило, когда installer брал часть templates из внешнего upstream repo, где эти файлы могли отсутствовать. + +Решение на сервере: + +```bash +phobos-update stable +/opt/Phobos/repo/server/scripts/phobos-client.sh package CLIENT_ID +/opt/Phobos/repo/server/scripts/phobos-client.sh link CLIENT_ID +``` + +Потом скопируй новую команду установки из панели и запусти её на роутере. + +Проверка пакета: + +```bash +tar tzf /opt/Phobos/packages/phobos-CLIENT_ID.tar.gz | grep -E 'lib-client.sh|install-obfuscator.sh|install-wireguard.sh' +``` + ### Пакет битый: `Bad package`, `not in gzip format`, HTML вместо tar.gz Роутер скачал не пакет, а страницу ошибки. @@ -533,6 +553,24 @@ phobos-update stable Потом заново открой Android-страницу клиента и сканируй QR прямо с панели. Если не помогло, создай нового клиента для телефона. Один клиент = одно устройство; роутер и телефон должны иметь разные клиенты. +### Android PhobosWG: `Неправильная длина ключа в PublicKey` + +Причина: старый phobos payload мог добавлять `PresharedKey = none` или отдавать старый QR/ссылку после обновления панели. PhobosWG воспринимает это как битый ключ. + +Решение: + +```bash +phobos-update stable +systemctl restart phobos-panel +``` + +Потом: + +1. Открой клиента в панели заново. +2. Нажми Android/PhobosWG. +3. Сканируй новый QR или копируй новую `phobos://` ссылку. +4. Если клиент был создан старой версией и ошибка осталась, создай нового клиента именно для телефона. + ### Android импорт прошел, но интернета нет В `[Peer]` должны быть маршруты: diff --git a/install.sh b/install.sh index 2b21dc8..bb33afa 100755 --- a/install.sh +++ b/install.sh @@ -206,8 +206,14 @@ fetch() { fetch overlay/lib-core.sh "$PHOBOS_DIR/repo/server/scripts/lib-core.sh" && chmod +x "$PHOBOS_DIR/repo/server/scripts/lib-core.sh" fetch overlay/phobos-client.sh "$PHOBOS_DIR/repo/server/scripts/phobos-client.sh" && chmod +x "$PHOBOS_DIR/repo/server/scripts/phobos-client.sh" fetch overlay/install-router.sh.template "$PHOBOS_DIR/repo/client/templates/install-router.sh.template" +fetch overlay/lib-client.sh "$PHOBOS_DIR/repo/client/templates/lib-client.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/lib-client.sh" +fetch overlay/install-obfuscator.sh "$PHOBOS_DIR/repo/client/templates/install-obfuscator.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/install-obfuscator.sh" +fetch overlay/install-wireguard.sh "$PHOBOS_DIR/repo/client/templates/install-wireguard.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/install-wireguard.sh" fetch overlay/router-configure-wireguard.sh "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard.sh" +fetch overlay/router-configure-wireguard-openwrt.sh "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard-openwrt.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard-openwrt.sh" fetch overlay/phobos-pull.sh "$PHOBOS_DIR/repo/client/templates/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-pull.sh" +fetch overlay/phobos-uninstall.sh "$PHOBOS_DIR/repo/client/templates/phobos-uninstall.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-uninstall.sh" +fetch overlay/3xui.sh "$PHOBOS_DIR/repo/client/templates/3xui.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/3xui.sh" fetch server/phobos-health.sh "$PHOBOS_DIR/server/phobos-health.sh" && chmod +x "$PHOBOS_DIR/server/phobos-health.sh" fetch server/phobos-pull.sh "$PHOBOS_DIR/server/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/server/phobos-pull.sh" fetch server/phobos-router-watchdog.py "$PHOBOS_DIR/server/phobos-router-watchdog.py" diff --git a/overlay/3xui.sh b/overlay/3xui.sh new file mode 100755 index 0000000..cc4edd4 --- /dev/null +++ b/overlay/3xui.sh @@ -0,0 +1,456 @@ +#!/bin/bash + +GREEN='\033[92m' +YELLOW='\033[93m' +RED='\033[91m' +BLUE='\033[94m' +CYAN='\033[96m' +RESET='\033[0m' +BOLD='\033[1m' + +print_status() { + local message="$1" + local status="${2:-info}" + local prefix="" + case "$status" in + success) prefix="${GREEN}[OK]" ;; + error) prefix="${RED}[ERR]" ;; + warning) prefix="${YELLOW}[WARN]" ;; + info) prefix="${BLUE}[INFO]" ;; + debug) prefix="${CYAN}[DBG]" ;; + esac + echo -e "${prefix} ${message}${RESET}" +} + +check_dependencies() { + local missing=() + for cmd in jq sqlite3; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing+=("$cmd") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + print_status "Installing missing dependencies: ${missing[*]}" "info" + if command -v apt-get >/dev/null 2>&1; then + apt-get update -qq && apt-get install -y -qq "${missing[@]}" >/dev/null 2>&1 + elif command -v yum >/dev/null 2>&1; then + yum install -y -q "${missing[@]/sqlite3/sqlite}" >/dev/null 2>&1 + elif command -v dnf >/dev/null 2>&1; then + dnf install -y -q "${missing[@]/sqlite3/sqlite}" >/dev/null 2>&1 + elif command -v apk >/dev/null 2>&1; then + apk add --quiet "${missing[@]/sqlite3/sqlite}" >/dev/null 2>&1 + fi + for cmd in "${missing[@]}"; do + if ! command -v "$cmd" >/dev/null 2>&1; then + print_status "Failed to install $cmd" "error" + exit 1 + fi + done + print_status "Dependencies installed successfully" "success" + fi +} + +parse_wireguard_conf() { + local config_path="$1" + local section="" + + if [[ ! -f "$config_path" ]]; then + print_status "File $config_path not found" "error" + exit 1 + fi + + INTERFACE_PRIVATEKEY="" + INTERFACE_ADDRESS="" + INTERFACE_MTU="1420" + PEER_PUBLICKEY="" + PEER_ENDPOINT="" + PEER_ALLOWEDIPS="" + PEER_PRESHAREDKEY="" + PEER_KEEPALIVE="0" + + while IFS= read -r line || [[ -n "$line" ]]; do + line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + [[ -z "$line" || "$line" == \#* ]] && continue + + if [[ "$line" == "["*"]" ]]; then + section=$(echo "$line" | tr -d '[]' | tr '[:upper:]' '[:lower:]') + continue + fi + + if [[ "$line" == *"="* ]]; then + local key=$(echo "$line" | cut -d'=' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + local value=$(echo "$line" | cut -d'=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + if [[ "$section" == "interface" ]]; then + case "$key" in + PrivateKey) INTERFACE_PRIVATEKEY="$value" ;; + Address) INTERFACE_ADDRESS="$value" ;; + MTU) INTERFACE_MTU="$value" ;; + esac + elif [[ "$section" == "peer" ]]; then + case "$key" in + PublicKey) PEER_PUBLICKEY="$value" ;; + Endpoint) PEER_ENDPOINT="$value" ;; + AllowedIPs) PEER_ALLOWEDIPS="$value" ;; + PresharedKey|PreSharedKey) PEER_PRESHAREDKEY="$value" ;; + PersistentKeepalive) PEER_KEEPALIVE="$value" ;; + esac + fi + fi + done < "$config_path" +} + +extract_endpoint() { + local endpoint="$1" + if [[ "$endpoint" == "["*"]:"* ]]; then + SERVER_ADDRESS=$(echo "$endpoint" | sed 's/^\[\([^]]*\)\]:.*/\1/') + SERVER_PORT=$(echo "$endpoint" | sed 's/.*\]:\([0-9]*\).*/\1/') + else + SERVER_ADDRESS=$(echo "$endpoint" | rev | cut -d':' -f2- | rev) + SERVER_PORT=$(echo "$endpoint" | rev | cut -d':' -f1 | rev) + fi + [[ -z "$SERVER_PORT" ]] && SERVER_PORT="51820" +} + +build_local_addresses() { + local addresses="$1" + local result="[]" + + IFS=',' read -ra ADDR_ARRAY <<< "$addresses" + for addr in "${ADDR_ARRAY[@]}"; do + addr=$(echo "$addr" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + [[ -z "$addr" ]] && continue + + local ip_part="$addr" + if [[ "$addr" == *"/"* ]]; then + ip_part=$(echo "$addr" | cut -d'/' -f1) + fi + + if [[ "$ip_part" == *":"* ]]; then + result=$(echo "$result" | jq --arg ip "${ip_part}/128" '. + [$ip]') + else + result=$(echo "$result" | jq --arg ip "${ip_part}/32" '. + [$ip]') + fi + done + + echo "$result" +} + +build_allowed_ips() { + local allowed="$1" + local result="[]" + + if [[ -z "$allowed" ]]; then + echo '["0.0.0.0/0", "::/0"]' + return + fi + + IFS=',' read -ra IP_ARRAY <<< "$allowed" + for ip in "${IP_ARRAY[@]}"; do + ip=$(echo "$ip" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + [[ -z "$ip" ]] && continue + result=$(echo "$result" | jq --arg ip "$ip" '. + [$ip]') + done + + if [[ "$result" == "[]" ]]; then + echo '["0.0.0.0/0", "::/0"]' + else + echo "$result" + fi +} + +convert_to_xray_outbound() { + local tag="${1:-Phobos}" + + extract_endpoint "$PEER_ENDPOINT" + + local local_addresses=$(build_local_addresses "$INTERFACE_ADDRESS") + local allowed_ips=$(build_allowed_ips "$PEER_ALLOWEDIPS") + + local peer_config + peer_config=$(jq -n \ + --arg pubkey "$PEER_PUBLICKEY" \ + --argjson allowed "$allowed_ips" \ + --arg endpoint "${SERVER_ADDRESS}:${SERVER_PORT}" \ + '{ + "publicKey": $pubkey, + "allowedIPs": $allowed, + "endpoint": $endpoint + }') + + if [[ -n "$PEER_KEEPALIVE" && "$PEER_KEEPALIVE" != "0" ]]; then + peer_config=$(echo "$peer_config" | jq --argjson ka "$PEER_KEEPALIVE" '. + {"keepAlive": $ka}') + fi + + if [[ -n "$PEER_PRESHAREDKEY" ]]; then + peer_config=$(echo "$peer_config" | jq --arg psk "$PEER_PRESHAREDKEY" '. + {"preSharedKey": $psk}') + fi + + local mtu="${INTERFACE_MTU:-1420}" + + OUTBOUND_JSON=$(jq -n \ + --arg tag "$tag" \ + --argjson mtu "$mtu" \ + --arg secret "$INTERFACE_PRIVATEKEY" \ + --argjson addresses "$local_addresses" \ + --argjson peer "$peer_config" \ + '{ + "protocol": "wireguard", + "settings": { + "mtu": $mtu, + "secretKey": $secret, + "address": $addresses, + "workers": 2, + "peers": [$peer], + "noKernelTun": true + }, + "tag": $tag + }') +} + +get_default_xray_config() { + cat <<'XRAY_CONFIG' +{ + "api": { + "services": ["HandlerService", "StatsService", "LoggerService"], + "tag": "api" + }, + "inbounds": [ + { + "listen": "127.0.0.1", + "port": 62789, + "protocol": "dokodemo-door", + "settings": {"address": "127.0.0.1"}, + "tag": "api" + } + ], + "outbounds": [ + {"protocol": "freedom", "settings": {}, "tag": "direct"}, + {"protocol": "blackhole", "settings": {}, "tag": "block"} + ], + "policy": { + "levels": {"0": {"statsUserDownlink": true, "statsUserUplink": true}}, + "system": { + "statsInboundDownlink": true, + "statsInboundUplink": true, + "statsOutboundDownlink": true, + "statsOutboundUplink": true + } + }, + "routing": { + "domainStrategy": "AsIs", + "rules": [ + {"inboundTag": ["api"], "outboundTag": "api", "type": "field"}, + {"ip": ["geoip:private"], "outboundTag": "block", "type": "field"} + ] + }, + "stats": {} +} +XRAY_CONFIG +} + +find_database() { + local paths=( + "/etc/x-ui/x-ui.db" + "/usr/local/x-ui/x-ui.db" + "/opt/x-ui/x-ui.db" + "./x-ui.db" + ) + + for path in "${paths[@]}"; do + if [[ -f "$path" ]]; then + print_status "Database found: $path" "success" >&2 + echo "$path" + return 0 + fi + done + + return 1 +} + +diagnose_database() { + local db_path="$1" + + local tables=$(sqlite3 "$db_path" "SELECT name FROM sqlite_master WHERE type='table';" 2>/dev/null) + print_status "Found tables: $tables" "debug" >&2 + + if echo "$tables" | grep -q "settings"; then + local keys=$(sqlite3 "$db_path" "SELECT key FROM settings;" 2>/dev/null) + print_status "Keys in settings: $keys" "debug" >&2 + + for key in xrayTemplateConfig xrayConfig xray_config config; do + local result=$(sqlite3 "$db_path" "SELECT value FROM settings WHERE key = '$key';" 2>/dev/null) + if [[ -n "$result" ]]; then + print_status "Found config key: $key" "success" >&2 + echo "$key" + return 0 + fi + done + fi + + return 1 +} + +write_setting_to_db() { + local db_path="$1" + local config_key="$2" + local config_value="$3" + local tmp + tmp=$(mktemp) + printf '%s' "$config_value" > "$tmp" + sqlite3 "$db_path" "INSERT OR REPLACE INTO settings (key, value) VALUES ('$config_key', readfile('$tmp'));" + local ret=$? + rm -f "$tmp" + return $ret +} + +import_via_database() { + local db_path="$1" + + print_status "Importing to database..." "info" + + if [[ ! -f "$db_path" ]]; then + print_status "Database not found: $db_path" "error" + return 1 + fi + + local config_key=$(diagnose_database "$db_path") + + if [[ -z "$config_key" ]]; then + print_status "Xray config not found in database, creating new..." "warning" + + local base_config=$(get_default_xray_config) + local new_config=$(echo "$base_config" | jq --argjson ob "$OUTBOUND_JSON" '.outbounds = [$ob] + .outbounds') + + if write_setting_to_db "$db_path" "xrayTemplateConfig" "$new_config"; then + print_status "Base Xray config created with outbound" "success" + return 0 + else + print_status "Failed to create config" "error" + return 1 + fi + fi + + local current_config=$(sqlite3 "$db_path" "SELECT value FROM settings WHERE key = '$config_key';" 2>/dev/null) + + if [[ -z "$current_config" ]]; then + print_status "Xray config not found" "error" + return 1 + fi + + if ! echo "$current_config" | jq . >/dev/null 2>&1; then + print_status "Invalid JSON in config" "error" + return 1 + fi + + local tag=$(echo "$OUTBOUND_JSON" | jq -r '.tag') + local existing_tag=$(echo "$current_config" | jq -r --arg t "$tag" '.outbounds[]? | select(.tag == $t) | .tag' 2>/dev/null) + + if [[ -n "$existing_tag" ]]; then + print_status "Outbound '$tag' exists, replacing..." "warning" + current_config=$(echo "$current_config" | jq --arg t "$tag" '.outbounds = [.outbounds[] | select(.tag != $t)]') + fi + + local new_config=$(echo "$current_config" | jq --argjson ob "$OUTBOUND_JSON" '.outbounds = [$ob] + .outbounds') + + if write_setting_to_db "$db_path" "$config_key" "$new_config"; then + print_status "Outbound added to database" "success" + return 0 + else + print_status "Failed to update database" "error" + return 1 + fi +} + +restart_xui_service() { + print_status "Restarting x-ui service..." "info" + + if systemctl restart x-ui 2>/dev/null; then + sleep 1 + if systemctl is-active --quiet x-ui; then + print_status "x-ui service restarted and active" "success" + return 0 + fi + fi + + for cmd in "service x-ui restart" "/etc/init.d/x-ui restart" "x-ui restart"; do + if $cmd 2>/dev/null; then + print_status "Service restarted via: $cmd" "success" + return 0 + fi + done + + print_status "Failed to restart service automatically" "error" + print_status "Restart manually: systemctl restart x-ui" "info" + return 1 +} + +main() { + echo -e "\n${BOLD}=== WireGuard to 3x-ui Auto-Import Tool ===${RESET}\n" + + check_dependencies + + local config_path="$1" + if [[ -z "$config_path" ]]; then + echo -n "Path to wireguard.conf: " + read config_path + fi + + print_status "Parsing $config_path..." "info" + parse_wireguard_conf "$config_path" + + print_status "Converting to Xray format..." "info" + convert_to_xray_outbound "Phobos" + + echo -e "\n${BOLD}Outbound configuration:${RESET}" + echo "$OUTBOUND_JSON" | jq . + echo + + local db_path="/etc/x-ui/x-ui.db" + if [[ ! -f "$db_path" ]]; then + print_status "Database not found at: $db_path" "warning" + db_path=$(find_database) + if [[ -z "$db_path" ]]; then + print_status "Database not found in standard locations" "error" + print_status "Make sure 3x-ui is installed" "info" + exit 1 + fi + fi + + systemctl stop x-ui 2>/dev/null + if ! import_via_database "$db_path"; then + systemctl start x-ui 2>/dev/null + print_status "Database import failed" "error" + exit 1 + fi + + local output_path="${config_path%.*}_3xui_outbound.json" + echo "$OUTBOUND_JSON" | jq . > "$output_path" 2>/dev/null + if [[ $? -eq 0 ]]; then + print_status "JSON saved to: $output_path" "success" + fi + + echo + local restart_ok=0 + restart_xui_service && restart_ok=1 + + echo + print_status "Import completed successfully!" "success" + echo + print_status "${BOLD}Info:${RESET}" "info" + print_status " Outbound 'Phobos' added to config" "info" + if [[ $restart_ok -eq 1 ]]; then + print_status " x-ui service restarted" "info" + else + print_status " x-ui service needs manual restart" "warning" + fi + print_status " JSON config saved to $output_path" "info" + echo + print_status "${BOLD}Verification:${RESET}" "info" + print_status " Open 3x-ui web panel" "info" + print_status " Go to Outbounds section" "info" + print_status " Verify 'Phobos' outbound is present" "info" + echo +} + +main "$@" diff --git a/overlay/install-obfuscator.sh b/overlay/install-obfuscator.sh new file mode 100755 index 0000000..6b8b480 --- /dev/null +++ b/overlay/install-obfuscator.sh @@ -0,0 +1,248 @@ +OBF_BINARY_NAME="wg-obfuscator" +OBF_CONF_NAME="wg-obfuscator.conf" +OBF_INIT_NAME="S49wg-obfuscator" +OBF_SERVICE_NAME="phobos-obfuscator" +OBF_WG_IFACE="phobos" + +resolve_install_names() { + local binary_dir + local binary_path + + if [ "$ROUTER_PLATFORM" = "openwrt" ]; then + binary_dir="/usr/bin" + elif [ "$ROUTER_PLATFORM" = "linux" ]; then + binary_dir="/usr/local/bin" + else + binary_dir="/opt/bin" + fi + + binary_path="${binary_dir}/wg-obfuscator" + + if [ ! -f "$binary_path" ] && [ ! -f "$PHOBOS_DIR/wg-obfuscator.conf" ]; then + return + fi + + if [ -f "$binary_path" ] && [ -f "$PHOBOS_DIR/${CLIENT_NAME}.conf" ]; then + return + fi + + local idx=1 + while [ -f "${binary_dir}/wg-obfuscator${idx}" ]; do + idx=$((idx + 1)) + done + + local init_num=50 + while [ -f "/opt/etc/init.d/S${init_num}wg-obfuscator" ]; do + init_num=$((init_num + 1)) + done + + OBF_BINARY_NAME="wg-obfuscator${idx}" + OBF_CONF_NAME="wg-obfuscator${idx}.conf" + OBF_INIT_NAME="S${init_num}wg-obfuscator" + OBF_SERVICE_NAME="phobos-obfuscator${idx}" + OBF_WG_IFACE="phobos${idx}" +} + +install_obfuscator() { + local arch=$1 + + log "Установка wg-obfuscator для архитектуры $arch..." + + if [ "$OBF_BINARY_NAME" = "wg-obfuscator" ]; then + if [ -f /opt/etc/init.d/S49wg-obfuscator ]; then + log "Остановка текущего процесса wg-obfuscator..." + /opt/etc/init.d/S49wg-obfuscator stop >/dev/null 2>&1 || true + fi + + if [ -f /etc/init.d/phobos-obfuscator ]; then + log "Остановка текущего процесса wg-obfuscator..." + /etc/init.d/phobos-obfuscator stop >/dev/null 2>&1 || true + fi + fi + + local binary_name="wg-obfuscator-$arch" + + if [ ! -f "bin/$binary_name" ]; then + log "Ошибка: бинарник $binary_name не найден в архиве" + log "Доступные бинарники:" + ls -1 bin/ + exit 1 + fi + + local target_path="/opt/bin/${OBF_BINARY_NAME}" + if [ "$ROUTER_PLATFORM" = "openwrt" ]; then + target_path="/usr/bin/${OBF_BINARY_NAME}" + fi + + if [ -f "$target_path" ]; then + rm "$target_path" + fi + + if [ "$ROUTER_PLATFORM" = "openwrt" ]; then + mkdir -p /usr/bin + else + mkdir -p /opt/bin + fi + + cp "bin/$binary_name" "$target_path" + chmod +x "$target_path" + + log "Бинарник wg-obfuscator установлен в $target_path" +} + +create_init_script() { + log "Создание init-скрипта для obfuscator..." + + cat > /opt/etc/init.d/${OBF_INIT_NAME} </dev/null 2>&1 && pidof ${OBF_BINARY_NAME} >/dev/null 2>&1; then + process_found=1 + elif command -v pgrep >/dev/null 2>&1 && pgrep -f wg-obfuscator >/dev/null 2>&1; then + process_found=1 + elif ps w 2>/dev/null | grep -v grep | grep -q "wg-obfuscator\|/opt/bin/wg-obfuscator\|/usr/bin/wg-obfuscator"; then + process_found=1 + elif ps 2>/dev/null | grep -v grep | grep -q "wg-obfuscator"; then + process_found=1 + fi + + if [ $process_found -eq 0 ]; then + if [ "$ROUTER_PLATFORM" = "keenetic" ] && [ -f /opt/etc/init.d/${OBF_INIT_NAME} ]; then + local status_output=$(/opt/etc/init.d/${OBF_INIT_NAME} status 2>&1 || echo "") + if echo "$status_output" | grep -q "alive"; then + process_found=1 + fi + elif [ "$ROUTER_PLATFORM" = "openwrt" ] && [ -f /etc/init.d/${OBF_SERVICE_NAME} ]; then + if /etc/init.d/${OBF_SERVICE_NAME} status >/dev/null 2>&1; then + process_found=1 + fi + fi + fi + + if [ $process_found -eq 1 ]; then + log "✓ wg-obfuscator успешно запущен" + else + log "✗ wg-obfuscator не запущен. Проверьте логи." + fi +} + +create_procd_init_script() { + log "Создание procd init-скрипта для obfuscator..." + + cat > /etc/init.d/${OBF_SERVICE_NAME} < /etc/systemd/system/${OBF_SERVICE_NAME}.service </dev/null 2>&1; then + apt-get update || log "ПРЕДУПРЕЖДЕНИЕ: Не удалось обновить список пакетов" + apt-get install -y wireguard wireguard-tools resolvconf net-tools || { + log "ОШИБКА: Не удалось установить WireGuard" + return 1 + } + else + log "ОШИБКА: apt-get не найден. Поддерживаются только Ubuntu/Debian" + return 1 + fi + + log "✓ WireGuard установлен" + return 0 +} + +configure_wireguard_linux() { + log "" + log "==> Настройка WireGuard через systemd..." + + local wg_interface="${OBF_WG_IFACE}" + local wg_config_dir="/etc/wireguard" + mkdir -p "$wg_config_dir" + + cp "$PHOBOS_DIR/${CLIENT_NAME}.conf" "$wg_config_dir/${wg_interface}.conf" + chmod 600 "$wg_config_dir/${wg_interface}.conf" + + log "Настройка routing: VPN как запасной интерфейс..." + + local client_ip=$(grep '^Address' "$wg_config_dir/${wg_interface}.conf" | cut -d'=' -f2 | tr -d ' ' | cut -d',' -f1 | cut -d'/' -f1) + local route_target=$(echo "$client_ip" | cut -d'.' -f1-2).0.0/16 + + sed -i '/^MTU/a Table = off' "$wg_config_dir/${wg_interface}.conf" + sed -i "/^Table = off/a PostUp = ip route add $route_target dev %i" "$wg_config_dir/${wg_interface}.conf" + sed -i "/^PostUp/a PostDown = ip route del $route_target dev %i || true" "$wg_config_dir/${wg_interface}.conf" + + log "Настройка зависимости WireGuard от obfuscator..." + mkdir -p "/etc/systemd/system/wg-quick@${wg_interface}.service.d" + cat > "/etc/systemd/system/wg-quick@${wg_interface}.service.d/override.conf" </dev/null | grep -q "latest handshake"; then + log "✓ Туннель установлен и работает" + else + log " Ожидание установки туннеля..." + fi + + return 0 + else + log "ПРЕДУПРЕЖДЕНИЕ: WireGuard может быть не запущен, проверьте статус" + return 1 + fi +} + +configure_ufw_linux() { + log "" + log "==> Проверка UFW firewall..." + + if ! command -v ufw >/dev/null 2>&1; then + log "UFW не установлен, пропуск настройки" + return 0 + fi + + log "✓ UFW обнаружен" + log " Клиентская конфигурация не требует открытия портов:" + log " - obfuscator слушает на 127.0.0.1:13255 (локальный интерфейс)" + log " - obfuscator делает исходящие соединения (разрешены по умолчанию)" + + if ufw status 2>/dev/null | grep -q "Status: active"; then + log " UFW активен, клиент будет работать корректно" + else + log " UFW неактивен" + fi +} diff --git a/overlay/lib-client.sh b/overlay/lib-client.sh new file mode 100755 index 0000000..cc31df5 --- /dev/null +++ b/overlay/lib-client.sh @@ -0,0 +1,93 @@ +detect_router_platform() { + if [ -f /etc/openwrt_release ] || [ -f /etc/openwrt_version ]; then + echo "openwrt" + elif [ -f /opt/etc/.entware_install_log ] || [ -d /opt/etc/ndm ]; then + echo "keenetic" + elif [ -f /etc/debian_version ]; then + echo "linux" + elif [ -d /etc/config ] && command -v uci >/dev/null 2>&1; then + echo "openwrt" + elif [ -f /opt/etc/init.d/rc.func ]; then + echo "keenetic" + elif [ -d /run/systemd/system ] || pidof systemd >/dev/null 2>&1; then + echo "linux" + else + local uname_output=$(uname -a) + if echo "$uname_output" | grep -qi "Keenetic\|Netcraze"; then + echo "keenetic" + elif echo "$uname_output" | grep -qi "OpenWrt\|LEDE\|ImmortalWrt"; then + echo "openwrt" + elif echo "$uname_output" | grep -qi "Linux"; then + echo "linux" + else + echo "unknown" + fi + fi +} + +detect_phobos_dir() { + local platform="$1" + + if [ "$platform" = "keenetic" ]; then + echo "/opt/etc/Phobos" + elif [ "$platform" = "openwrt" ]; then + echo "/etc/Phobos" + elif [ "$platform" = "linux" ]; then + echo "/opt/Phobos" + else + if [ -d "/opt" ] && [ -d "/opt/etc" ]; then + echo "/opt/etc/Phobos" + else + echo "/etc/Phobos" + fi + fi +} + +detect_arch() { + local arch=$(uname -m) + case "$arch" in + mips) + if [ "$(echo -n I | hexdump -o 2>/dev/null | awk 'NR==1{print $2}')" = "000111" ]; then + echo "mipsel" + else + echo "mips" + fi + ;; + aarch64|arm64) + echo "aarch64" + ;; + armv7l|armv6l) + echo "armv7" + ;; + x86_64) + echo "x86_64" + ;; + *) + echo "unknown" + ;; + esac +} + +check_root() { + if [ "$(id -u)" -ne 0 ]; then + echo "Этот скрипт требует root привилегии. Запустите: su -c '$0'" + exit 1 + fi +} + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +print_status() { + local status="$1" + local message="$2" + + if [ "${status}" = "OK" ]; then + printf "\\033[0;32m✓\\033[0m %s\n" "${message}" + elif [ "${status}" = "WARN" ]; then + printf "\\033[1;33m⚠\\033[0m %s\n" "${message}" + else + printf "\\033[0;31m✗\\033[0m %s\n" "${message}" + fi +} diff --git a/overlay/phobos-client.sh b/overlay/phobos-client.sh index 65394de..df37d3a 100755 --- a/overlay/phobos-client.sh +++ b/overlay/phobos-client.sh @@ -216,7 +216,7 @@ action_package() { cp "$dir/wg-obfuscator.conf" "$pkg_root/wg-obfuscator.conf" # Fetch missing router-arch binaries from ClusterM on-demand - local _tag; _tag=$(curl -fsSL -m10 https://api.github.com/repos/ClusterM/wg-obfuscator/releases/latest 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*//') + local _tag; _tag=$(curl -fsSL -m10 https://api.github.com/repos/ClusterM/wg-obfuscator/releases/latest 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*/\1/') local _cb="https://github.com/ClusterM/wg-obfuscator/releases/download/${_tag}" declare -A _am=([aarch64]="linux-arm64" [mipsel]="linux-mipsel-mips32" [mips]="linux-mips-mips32" [armv7]="linux-armv7-hf" [x86_64]="linux-x64") for _arch in aarch64 mipsel mips armv7 x86_64; do @@ -235,12 +235,16 @@ action_package() { local tpl_dir="$REPO_DIR/client/templates" if [[ -d "$tpl_dir" ]]; then + for required in install-router.sh.template lib-client.sh install-obfuscator.sh install-wireguard.sh; do + [[ -f "$tpl_dir/$required" ]] || die "Обязательный шаблон $required не найден в $tpl_dir. Выполни phobos-update stable и пересоздай пакет." + done cp "$tpl_dir/install-router.sh.template" "$pkg_root/install-router.sh" sed -i "s|{{CLIENT_NAME}}|${id}|g" "$pkg_root/install-router.sh" chmod +x "$pkg_root/install-router.sh" - [[ -f "$tpl_dir/lib-client.sh" ]] && cp "$tpl_dir/lib-client.sh" "$pkg_root/lib-client.sh" - [[ -f "$tpl_dir/install-obfuscator.sh" ]] && cp "$tpl_dir/install-obfuscator.sh" "$pkg_root/install-obfuscator.sh" - [[ -f "$tpl_dir/install-wireguard.sh" ]] && cp "$tpl_dir/install-wireguard.sh" "$pkg_root/install-wireguard.sh" + cp "$tpl_dir/lib-client.sh" "$pkg_root/lib-client.sh" + cp "$tpl_dir/install-obfuscator.sh" "$pkg_root/install-obfuscator.sh" + cp "$tpl_dir/install-wireguard.sh" "$pkg_root/install-wireguard.sh" + chmod +x "$pkg_root/lib-client.sh" "$pkg_root/install-obfuscator.sh" "$pkg_root/install-wireguard.sh" for f in router-configure-wireguard router-configure-wireguard-openwrt phobos-uninstall 3xui; do [[ -f "$tpl_dir/$f.sh" ]] && cp "$tpl_dir/$f.sh" "$pkg_root/$f.sh" && chmod +x "$pkg_root/$f.sh" done diff --git a/overlay/phobos-uninstall.sh b/overlay/phobos-uninstall.sh new file mode 100755 index 0000000..1adafe3 --- /dev/null +++ b/overlay/phobos-uninstall.sh @@ -0,0 +1,543 @@ +#!/bin/sh +set -e + +PHOBOS_DIR="" +ROUTER_PLATFORM="" +SELECTED_CONF="" +IS_FULL_REMOVAL="1" +OBFUSCATOR_CONFIGS="" +OBFUSCATOR_CONFIG_COUNT=0 + +. "$(dirname "$0")/lib-client.sh" + +collect_obf_configs() { + OBFUSCATOR_CONFIGS="" + OBFUSCATOR_CONFIG_COUNT=0 + [ -d "$PHOBOS_DIR" ] || return + for f in "$PHOBOS_DIR"/wg-obfuscator*.conf; do + [ -f "$f" ] || continue + OBFUSCATOR_CONFIGS="${OBFUSCATOR_CONFIGS}$(basename "$f" .conf) " + OBFUSCATOR_CONFIG_COUNT=$((OBFUSCATOR_CONFIG_COUNT + 1)) + done +} + +get_config_label() { + local obf_conf_base="$1" + local idx_suffix="${obf_conf_base#wg-obfuscator}" + local wg_iface="phobos${idx_suffix}" + local addr="" + if [ -f "/etc/wireguard/${wg_iface}.conf" ]; then + addr=$(grep '^Address' "/etc/wireguard/${wg_iface}.conf" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' | cut -d'/' -f1 | cut -d',' -f1) || addr="" + fi + if [ -n "$addr" ]; then + printf "%s [интерфейс: %s, адрес: %s]" "$obf_conf_base" "$wg_iface" "$addr" + else + printf "%s" "$obf_conf_base" + fi +} + +select_config_to_remove() { + collect_obf_configs + + if [ "$OBFUSCATOR_CONFIG_COUNT" -eq 0 ]; then + IS_FULL_REMOVAL="1" + SELECTED_CONF="all" + return + fi + + if [ "$OBFUSCATOR_CONFIG_COUNT" -eq 1 ]; then + SELECTED_CONF=$(echo "$OBFUSCATOR_CONFIGS" | tr -s ' ' '\n' | grep -v '^$' | head -1) + IS_FULL_REMOVAL="1" + log "Обнаружена конфигурация: ${SELECTED_CONF}" + return + fi + + log "" + log "Обнаружено несколько конфигураций:" + log "" + + local i=1 + for conf_base in $OBFUSCATOR_CONFIGS; do + local label + label=$(get_config_label "$conf_base") + log " ${i}) ${label}" + i=$((i + 1)) + done + + local all_num=$i + log " ${all_num}) Удалить все" + log "" + + printf "Введите номер конфигурации для удаления: " + read -r answer || answer="" + + case "$answer" in + ''|*[!0-9]*) + log "ОШИБКА: Неверный ввод" + exit 1 + ;; + esac + + if [ "$answer" -eq "$all_num" ]; then + IS_FULL_REMOVAL="1" + SELECTED_CONF="all" + return + fi + + if [ "$answer" -lt 1 ] || [ "$answer" -ge "$all_num" ]; then + log "ОШИБКА: Номер вне диапазона" + exit 1 + fi + + local selected_i=1 + for conf_base in $OBFUSCATOR_CONFIGS; do + if [ "$selected_i" -eq "$answer" ]; then + SELECTED_CONF="$conf_base" + IS_FULL_REMOVAL="0" + return + fi + selected_i=$((selected_i + 1)) + done +} + +remove_single_obfuscator_instance() { + local obf_conf_base="$1" + local idx_suffix="${obf_conf_base#wg-obfuscator}" + local binary_name="wg-obfuscator${idx_suffix}" + local service_name="phobos-obfuscator${idx_suffix}" + local wg_iface="phobos${idx_suffix}" + + log "Удаление конфигурации ${obf_conf_base}..." + + local link_file="$PHOBOS_DIR/${obf_conf_base}.link" + local linked_client="" + if [ -f "$link_file" ]; then + linked_client=$(cat "$link_file" 2>/dev/null) || linked_client="" + fi + + local privkey_fallback="" + if [ -z "$linked_client" ] && [ "$ROUTER_PLATFORM" = "linux" ] && [ -f "/etc/wireguard/${wg_iface}.conf" ]; then + privkey_fallback=$(grep '^PrivateKey' "/etc/wireguard/${wg_iface}.conf" 2>/dev/null | cut -d'=' -f2- | tr -d ' \t') || privkey_fallback="" + fi + + for f in /opt/etc/init.d/S[0-9]*wg-obfuscator*; do + [ -f "$f" ] || continue + if grep -q "^PROCS=${binary_name}$" "$f" 2>/dev/null; then + "$f" stop >/dev/null 2>&1 || true + rm -f "$f" + log " ✓ Удален init-скрипт: $f" + break + fi + done + + if [ -f "/etc/init.d/${service_name}" ]; then + /etc/init.d/${service_name} stop >/dev/null 2>&1 || true + /etc/init.d/${service_name} disable >/dev/null 2>&1 || true + rm -f "/etc/init.d/${service_name}" + log " ✓ Удален procd init-скрипт: /etc/init.d/${service_name}" + fi + + if command -v systemctl >/dev/null 2>&1 && [ -f "/etc/systemd/system/${service_name}.service" ]; then + systemctl stop "${service_name}" >/dev/null 2>&1 || true + systemctl disable "${service_name}" >/dev/null 2>&1 || true + rm -f "/etc/systemd/system/${service_name}.service" + log " ✓ Удален systemd сервис: /etc/systemd/system/${service_name}.service" + fi + + for dir in /opt/bin /usr/bin /usr/local/bin; do + if [ -f "${dir}/${binary_name}" ]; then + rm -f "${dir}/${binary_name}" + log " ✓ Удален бинарник: ${dir}/${binary_name}" + fi + done + + if [ "$ROUTER_PLATFORM" = "linux" ] && command -v systemctl >/dev/null 2>&1; then + systemctl stop "wg-quick@${wg_iface}" >/dev/null 2>&1 || true + systemctl disable "wg-quick@${wg_iface}" >/dev/null 2>&1 || true + if [ -d "/etc/systemd/system/wg-quick@${wg_iface}.service.d" ]; then + rm -rf "/etc/systemd/system/wg-quick@${wg_iface}.service.d" + log " ✓ Удален systemd override: wg-quick@${wg_iface}" + fi + if [ -f "/etc/wireguard/${wg_iface}.conf" ]; then + rm -f "/etc/wireguard/${wg_iface}.conf" + log " ✓ Удален WireGuard конфиг: /etc/wireguard/${wg_iface}.conf" + fi + systemctl daemon-reload >/dev/null 2>&1 || true + elif [ "$ROUTER_PLATFORM" = "keenetic" ]; then + if command -v curl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1 && [ -n "$linked_client" ]; then + local target_desc="Phobos-${linked_client}" + log " Удаление WireGuard интерфейса Keenetic: ${target_desc}..." + local iface_list=$(curl -s "http://127.0.0.1:79/rci/show/interface" 2>/dev/null || echo "") + if [ -n "$iface_list" ] && echo "$iface_list" | jq -e . >/dev/null 2>&1; then + local iface_id=$(echo "$iface_list" | jq -r --arg desc "$target_desc" 'to_entries[] | select((.value.description? // "") == $desc) | .key' 2>/dev/null) + if [ -n "$iface_id" ]; then + local del_json=$(cat </dev/null) + if echo "$del_result" | jq -e '.status == "error"' >/dev/null 2>&1; then + local err_msg=$(echo "$del_result" | jq -r '.message // "Unknown error"' 2>/dev/null) + log " ОШИБКА при удалении ${iface_id}: $err_msg" + else + log " ✓ Интерфейс ${iface_id} (${target_desc}) удален" + curl -s -X POST \ + -H "Content-Type: application/json" \ + -d '{"system":{"configuration":{"save":{}}}}' \ + "http://127.0.0.1:79/rci/" >/dev/null 2>&1 + log " ✓ Конфигурация сохранена" + fi + else + log " Интерфейс с описанием '${target_desc}' не найден" + fi + else + log " RCI API недоступен, пропускаем удаление интерфейса Keenetic" + fi + else + log " curl/jq не доступны или имя клиента не определено, пропускаем удаление интерфейса Keenetic" + fi + elif [ "$ROUTER_PLATFORM" = "openwrt" ]; then + remove_wireguard_interfaces_openwrt + remove_firewall_zone_openwrt + fi + + if [ -f "$PHOBOS_DIR/${obf_conf_base}.conf" ]; then + rm -f "$PHOBOS_DIR/${obf_conf_base}.conf" + log " ✓ Удален конфиг obfuscator: $PHOBOS_DIR/${obf_conf_base}.conf" + fi + + [ -f "$link_file" ] && rm -f "$link_file" + + if [ -n "$linked_client" ] && [ -f "$PHOBOS_DIR/${linked_client}.conf" ]; then + rm -f "$PHOBOS_DIR/${linked_client}.conf" + log " ✓ Удален конфиг клиента: ${linked_client}.conf" + elif [ -n "$privkey_fallback" ]; then + for client_conf in "$PHOBOS_DIR"/*.conf; do + [ -f "$client_conf" ] || continue + case "$(basename "$client_conf")" in + wg-obfuscator*.conf) continue ;; + esac + if grep -qF "PrivateKey = ${privkey_fallback}" "$client_conf" 2>/dev/null; then + rm -f "$client_conf" + log " ✓ Удален конфиг клиента: $(basename "$client_conf")" + break + fi + done + fi + + local remaining=0 + for f in "$PHOBOS_DIR"/*.conf; do + [ -f "$f" ] && remaining=$((remaining + 1)) + done + + if [ "$remaining" -eq 0 ] && [ -d "$PHOBOS_DIR" ]; then + rm -rf "$PHOBOS_DIR" + log " ✓ Удалена директория: $PHOBOS_DIR" + fi +} + +stop_obfuscator() { + log "Остановка wg-obfuscator..." + + for f in /opt/etc/init.d/S[0-9]*wg-obfuscator*; do + [ -f "$f" ] && "$f" stop >/dev/null 2>&1 || true + done + + for f in /etc/init.d/phobos-obfuscator*; do + if [ -f "$f" ]; then + "$f" stop >/dev/null 2>&1 || true + "$f" disable >/dev/null 2>&1 || true + fi + done + + if command -v systemctl >/dev/null 2>&1; then + systemctl list-units --no-legend --plain 'phobos-obfuscator*.service' | awk '{print $1}' | while read -r unit; do + systemctl stop "$unit" >/dev/null 2>&1 || true + systemctl disable "$unit" >/dev/null 2>&1 || true + done + fi + + if ps | grep -v grep | grep -q wg-obfuscator; then + log " Принудительное завершение процесса..." + pkill -f wg-obfuscator >/dev/null 2>&1 || true + fi +} + +remove_wireguard_interfaces_keenetic() { + log "Удаление WireGuard интерфейсов Phobos (Keenetic)..." + + if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + log " curl или jq не установлен, пропускаем удаление интерфейсов" + return 0 + fi + + local interfaces=$(curl -s "http://127.0.0.1:79/rci/show/interface" 2>/dev/null || echo "") + + if [ -z "$interfaces" ] || ! echo "$interfaces" | jq -e . >/dev/null 2>&1; then + log " RCI API недоступен или вернул некорректный JSON" + return 0 + fi + + local removed_count=0 + + local phobos_interfaces=$(echo "$interfaces" | jq -r 'to_entries[] | select(.value.description? // "" | startswith("Phobos-")) | .key' 2>/dev/null) + + if [ -z "$phobos_interfaces" ]; then + log " Интерфейсы Phobos не найдены в системе" + return 0 + fi + + echo "$phobos_interfaces" | while read -r interface_id; do + if [ -z "$interface_id" ]; then + continue + fi + + local interface_desc=$(echo "$interfaces" | jq -r --arg id "$interface_id" '.[$id].description // "unknown"' 2>/dev/null) + + log " Удаление интерфейса: $interface_id ($interface_desc)" + + local delete_json=$(cat </dev/null) + + if echo "$result" | jq -e '.status == "error"' >/dev/null 2>&1; then + local error_msg=$(echo "$result" | jq -r '.message // "Unknown error"' 2>/dev/null) + log " ОШИБКА при удалении $interface_id: $error_msg" + else + log " ✓ Интерфейс $interface_id удален" + removed_count=$((removed_count + 1)) + fi + done + + local save_result=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -d '{"system":{"configuration":{"save":{}}}}' \ + "http://127.0.0.1:79/rci/" 2>/dev/null) + + if [ $removed_count -gt 0 ]; then + log "✓ Удалено интерфейсов: $removed_count" + log "✓ Конфигурация сохранена" + fi +} + +remove_wireguard_interfaces_openwrt() { + log "Удаление WireGuard интерфейсов Phobos (OpenWRT)..." + + if ! command -v uci >/dev/null 2>&1; then + log " uci не установлен, пропускаем удаление интерфейсов" + return 0 + fi + + local interface_name="phobos_wg" + + if uci -q get network.${interface_name} >/dev/null 2>&1; then + log " Удаление интерфейса: ${interface_name}" + + ifdown ${interface_name} >/dev/null 2>&1 || true + + uci -q delete network.${interface_name} || true + + local peers=$(uci show network 2>/dev/null | grep "wireguard_${interface_name}" | cut -d'.' -f2 | cut -d'=' -f1 | sort -u) + for peer in $peers; do + uci -q delete network.$peer || true + done + + uci commit network + + log " ✓ Интерфейс ${interface_name} удален" + else + log " Интерфейс ${interface_name} не найден" + fi + + /etc/init.d/network reload >/dev/null 2>&1 || true +} + +remove_firewall_zone_openwrt() { + log "Удаление файрволл зоны Phobos (OpenWRT)..." + + if ! command -v uci >/dev/null 2>&1; then + log " uci не установлен, пропускаем удаление зоны" + return 0 + fi + + local zone_name="phobos" + + if uci -q get firewall.${zone_name} >/dev/null 2>&1; then + log " Удаление зоны: ${zone_name}" + uci -q delete firewall.${zone_name} || true + uci commit firewall + log " ✓ Зона ${zone_name} удалена" + + /etc/init.d/firewall reload >/dev/null 2>&1 || true + else + log " Зона ${zone_name} не найдена" + fi +} + +remove_wireguard_interfaces_linux() { + log "Удаление WireGuard интерфейсов Phobos (Linux)..." + + if ! command -v systemctl >/dev/null 2>&1; then + log " systemctl не найден, пропускаем удаление интерфейсов" + return 0 + fi + + for wg_conf in /etc/wireguard/phobos*.conf; do + [ -f "$wg_conf" ] || continue + local wg_interface + wg_interface=$(basename "$wg_conf" .conf) + + if systemctl is-enabled --quiet wg-quick@${wg_interface} 2>/dev/null || systemctl is-active --quiet wg-quick@${wg_interface} 2>/dev/null; then + log " Остановка и удаление WireGuard сервиса: wg-quick@${wg_interface}" + systemctl stop wg-quick@${wg_interface} >/dev/null 2>&1 || true + systemctl disable wg-quick@${wg_interface} >/dev/null 2>&1 || true + log " ✓ Сервис wg-quick@${wg_interface} остановлен и отключен" + fi + + if [ -d "/etc/systemd/system/wg-quick@${wg_interface}.service.d" ]; then + log " Удаление systemd override: /etc/systemd/system/wg-quick@${wg_interface}.service.d" + rm -rf "/etc/systemd/system/wg-quick@${wg_interface}.service.d" + log " ✓ Systemd override удален" + fi + + log " Удаление конфигурации: $wg_conf" + rm -f "$wg_conf" + log " ✓ Конфигурационный файл WireGuard удален" + done + + systemctl daemon-reload >/dev/null 2>&1 || true +} + +remove_files() { + log "Удаление файлов Phobos..." + + for f in /opt/etc/init.d/S[0-9]*wg-obfuscator*; do + if [ -f "$f" ]; then + rm -f "$f" + log " ✓ Удален init-скрипт: $f" + fi + done + + for f in /etc/init.d/phobos-obfuscator*; do + if [ -f "$f" ]; then + rm -f "$f" + log " ✓ Удален procd init-скрипт: $f" + fi + done + + for f in /opt/bin/wg-obfuscator*; do + if [ -f "$f" ]; then + rm -f "$f" + log " ✓ Удален бинарник: $f" + fi + done + + for f in /usr/bin/wg-obfuscator*; do + if [ -f "$f" ]; then + rm -f "$f" + log " ✓ Удален бинарник: $f" + fi + done + + for f in /usr/local/bin/wg-obfuscator*; do + if [ -f "$f" ]; then + rm -f "$f" + log " ✓ Удален бинарник: $f" + fi + done + + local daemon_reload_needed=0 + for f in /etc/systemd/system/phobos-obfuscator*.service; do + if [ -f "$f" ]; then + rm -f "$f" + daemon_reload_needed=1 + log " ✓ Удален systemd сервис: $f" + fi + done + + if [ $daemon_reload_needed -eq 1 ]; then + systemctl daemon-reload >/dev/null 2>&1 || true + fi + + if [ -d "$PHOBOS_DIR" ]; then + rm -rf "$PHOBOS_DIR" + log " ✓ Удалена директория: $PHOBOS_DIR" + fi +} + +show_final_info() { + log "" + log "╔════════════════════════════════════════════════════════════╗" + log "║ Phobos успешно удален ║" + log "╚════════════════════════════════════════════════════════════╝" + log "" + log "Удалены компоненты:" + log " - wg-obfuscator (процесс, init-скрипт/сервис, бинарник)" + log " - WireGuard интерфейсы Phobos" + log " - Конфигурационные файлы" + log "" +} + +main() { + ROUTER_PLATFORM=$(detect_router_platform) + PHOBOS_DIR=$(detect_phobos_dir "$ROUTER_PLATFORM") + + log "==> Начало удаления Phobos (платформа: $ROUTER_PLATFORM)" + log "==> Директория: $PHOBOS_DIR" + + check_root + + select_config_to_remove + + if [ "$IS_FULL_REMOVAL" = "1" ]; then + stop_obfuscator + + if [ "$ROUTER_PLATFORM" = "keenetic" ]; then + remove_wireguard_interfaces_keenetic + elif [ "$ROUTER_PLATFORM" = "openwrt" ]; then + remove_wireguard_interfaces_openwrt + remove_firewall_zone_openwrt + elif [ "$ROUTER_PLATFORM" = "linux" ]; then + remove_wireguard_interfaces_linux + else + log "ПРЕДУПРЕЖДЕНИЕ: Неизвестная платформа, пропускаем удаление WireGuard интерфейсов" + fi + + remove_files + + show_final_info + else + log "==> Удаление конфигурации: ${SELECTED_CONF}" + remove_single_obfuscator_instance "$SELECTED_CONF" + log "" + log "╔════════════════════════════════════════════════════════════╗" + log "║ Конфигурация успешно удалена ║" + log "╚════════════════════════════════════════════════════════════╝" + log "" + fi + + log "==> Удаление завершено." +} + +main "$@" diff --git a/overlay/router-configure-wireguard-openwrt.sh b/overlay/router-configure-wireguard-openwrt.sh new file mode 100755 index 0000000..2b32fc6 --- /dev/null +++ b/overlay/router-configure-wireguard-openwrt.sh @@ -0,0 +1,349 @@ +#!/bin/sh +set -e + +CLIENT_NAME="" +CLIENT_PRIVATE_KEY="" +CLIENT_IP="" +CLIENT_IPV6="" +SERVER_PUBLIC_KEY="" +ENDPOINT_PORT=13255 +KEEPALIVE=25 +MTU=1420 +FALLBACK_CONFIG="" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +error() { + echo "[ERROR] $*" >&2 + log "ERROR: $*" +} + +usage() { + cat </dev/null 2>&1; then + missing="$missing $cmd" + fi + done + + if [ -n "$missing" ]; then + error "Missing required utilities:$missing" + return 1 + fi + + return 0 +} + +parse_args() { + while [ $# -gt 0 ]; do + case "$1" in + --client-name) + CLIENT_NAME="$2" + shift 2 + ;; + --client-private-key) + CLIENT_PRIVATE_KEY="$2" + shift 2 + ;; + --client-ip) + CLIENT_IP="$2" + shift 2 + ;; + --client-ipv6) + CLIENT_IPV6="$2" + shift 2 + ;; + --server-public-key) + SERVER_PUBLIC_KEY="$2" + shift 2 + ;; + --endpoint-port) + ENDPOINT_PORT="$2" + shift 2 + ;; + --keepalive) + KEEPALIVE="$2" + shift 2 + ;; + --mtu) + MTU="$2" + shift 2 + ;; + --fallback-config) + FALLBACK_CONFIG="$2" + shift 2 + ;; + --help) + usage + ;; + *) + error "Unknown option: $1" + usage + ;; + esac + done + + if [ -z "${CLIENT_NAME}" ] || [ -z "${CLIENT_PRIVATE_KEY}" ] || \ + [ -z "${CLIENT_IP}" ] || [ -z "${CLIENT_IPV6}" ] || \ + [ -z "${SERVER_PUBLIC_KEY}" ]; then + error "Missing required parameters" + usage + fi +} + +check_existing_interface() { + local interface_name="phobos_wg" + + if uci -q get network.${interface_name} >/dev/null 2>&1; then + log "Найден существующий интерфейс: ${interface_name}" + return 0 + else + log "Существующий интерфейс Phobos не найден" + return 1 + fi +} + +remove_existing_interface() { + local interface_name="phobos_wg" + + log "Удаление существующего интерфейса ${interface_name}..." + + uci -q delete network.${interface_name} || true + + local peers=$(uci show network | grep "wireguard_${interface_name}" | cut -d'.' -f2 | cut -d'=' -f1 | sort -u) + for peer in $peers; do + uci -q delete network.$peer || true + done + + uci commit network + log "Существующий интерфейс удален" +} + +configure_wireguard_interface() { + local interface_name="phobos_wg" + + log "Настройка интерфейса ${interface_name} через UCI..." + + uci set network.${interface_name}=interface + uci set network.${interface_name}.proto='wireguard' + uci set network.${interface_name}.private_key="${CLIENT_PRIVATE_KEY}" + uci set network.${interface_name}.mtu="${MTU}" + + uci add_list network.${interface_name}.addresses="${CLIENT_IP}" + + if [ "${CLIENT_IPV6}" != "none" ] && [ -n "${CLIENT_IPV6}" ]; then + uci add_list network.${interface_name}.addresses="${CLIENT_IPV6}" + fi + + local peer_name="wgpeer_${interface_name}" + uci add network wireguard_${interface_name} + uci rename network.@wireguard_${interface_name}[-1]="${peer_name}" + uci set network.${peer_name}.public_key="${SERVER_PUBLIC_KEY}" + uci set network.${peer_name}.description="Phobos VPS Server" + uci set network.${peer_name}.endpoint_host='127.0.0.1' + uci set network.${peer_name}.endpoint_port="${ENDPOINT_PORT}" + uci set network.${peer_name}.persistent_keepalive="${KEEPALIVE}" + uci set network.${peer_name}.route_allowed_ips='0' + + uci add_list network.${peer_name}.allowed_ips='0.0.0.0/0' + uci add_list network.${peer_name}.allowed_ips='::/0' + + uci commit network + + log "Интерфейс ${interface_name} настроен ✓" + return 0 +} + +configure_firewall_zone() { + local zone_name="phobos" + local interface_name="phobos_wg" + + log "Настройка файрволла для зоны ${zone_name}..." + + if uci -q get firewall.${zone_name} >/dev/null 2>&1; then + log "Зона ${zone_name} уже существует, обновляем..." + uci delete firewall.${zone_name} + fi + + uci add firewall zone + uci rename firewall.@zone[-1]="${zone_name}" + uci set firewall.${zone_name}.name="${zone_name}" + uci add_list firewall.${zone_name}.network="${interface_name}" + uci set firewall.${zone_name}.input='REJECT' + uci set firewall.${zone_name}.output='ACCEPT' + uci set firewall.${zone_name}.forward='REJECT' + uci set firewall.${zone_name}.masq='0' + uci set firewall.${zone_name}.mtu_fix='1' + + uci commit firewall + + log "Файрволл зона ${zone_name} настроена ✓" + return 0 +} + +restart_network_services() { + log "Перезапуск сетевых сервисов..." + + /etc/init.d/network reload >/dev/null 2>&1 || true + sleep 3 + + ifup phobos_wg >/dev/null 2>&1 || true + sleep 2 + + /etc/init.d/firewall reload >/dev/null 2>&1 || true + sleep 1 + + log "Сетевые сервисы перезапущены ✓" +} + +verify_interface_created() { + log "Проверка создания интерфейса WireGuard..." + + local max_attempts=5 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + if ip link show phobos_wg >/dev/null 2>&1; then + log "Интерфейс phobos_wg найден (попытка $attempt/$max_attempts)" + + if wg show phobos_wg >/dev/null 2>&1; then + log "WireGuard интерфейс phobos_wg активен" + + if uci -q get network.phobos_wg >/dev/null 2>&1; then + log "UCI конфигурация phobos_wg найдена" + return 0 + else + log "UCI конфигурация phobos_wg не найдена, но интерфейс работает" + return 0 + fi + else + log "Интерфейс найден, но WireGuard не активен (попытка $attempt/$max_attempts)" + fi + else + log "Интерфейс phobos_wg еще не создан (попытка $attempt/$max_attempts)..." + fi + + if [ $attempt -lt $max_attempts ]; then + sleep 2 + fi + attempt=$((attempt + 1)) + done + + error "Интерфейс phobos_wg не найден после $max_attempts попыток" + return 1 +} + +show_fallback_instructions() { + cat </dev/null | sed 's/^/ /' || true + log "" + + exit 0 + else + log "" + log "Не удалось подтвердить создание интерфейса WireGuard" + log "" + log "Проверьте вручную:" + log " ip link show phobos_wg" + log " wg show phobos_wg" + log " uci show network.phobos_wg" + log "" + log "Если интерфейс существует (ip link show phobos_wg работает)," + log "то настройка прошла успешно, несмотря на ошибку проверки." + log "" + exit 1 + fi +} + +main "$@" \ No newline at end of file diff --git a/update.sh b/update.sh index b847ecb..d009143 100755 --- a/update.sh +++ b/update.sh @@ -50,8 +50,14 @@ $PANEL_DIR/app.py|app.py $PHOBOS_DIR/repo/server/scripts/lib-core.sh|overlay/lib-core.sh $PHOBOS_DIR/repo/server/scripts/phobos-client.sh|overlay/phobos-client.sh $PHOBOS_DIR/repo/client/templates/install-router.sh.template|overlay/install-router.sh.template +$PHOBOS_DIR/repo/client/templates/lib-client.sh|overlay/lib-client.sh +$PHOBOS_DIR/repo/client/templates/install-obfuscator.sh|overlay/install-obfuscator.sh +$PHOBOS_DIR/repo/client/templates/install-wireguard.sh|overlay/install-wireguard.sh $PHOBOS_DIR/repo/client/templates/router-configure-wireguard.sh|overlay/router-configure-wireguard.sh +$PHOBOS_DIR/repo/client/templates/router-configure-wireguard-openwrt.sh|overlay/router-configure-wireguard-openwrt.sh $PHOBOS_DIR/repo/client/templates/phobos-pull.sh|overlay/phobos-pull.sh +$PHOBOS_DIR/repo/client/templates/phobos-uninstall.sh|overlay/phobos-uninstall.sh +$PHOBOS_DIR/repo/client/templates/3xui.sh|overlay/3xui.sh $PHOBOS_DIR/server/phobos-health.sh|server/phobos-health.sh $PHOBOS_DIR/server/phobos-pull.sh|server/phobos-pull.sh $PHOBOS_DIR/server/phobos-router-watchdog.py|server/phobos-router-watchdog.py