# ========================================== # 📦 SING-BOX NATIVE INSTALLER # ========================================== param( [switch]$Force, [switch]$Debug, [string]$SubscriptionUrl = "" ) $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path . "$ScriptDir\lib\Common.ps1" . "$ScriptDir\lib\Net.ps1" . "$ScriptDir\lib\System.ps1" # --- CONFIG --- $SingboxVersion = "1.11.4" $InstallDir = "C:\Tools\sing-box" $LocalProxyPort = 1080 $SingboxUrl = "https://github.com/SagerNet/sing-box/releases/download/v$SingboxVersion/sing-box-$SingboxVersion-windows-amd64.zip" $TaskName = "SingBoxProxy" Ensure-Admin # --- LOGIC --- function Select-Server { param($Config) $outbounds = $Config.outbounds $servers = @() foreach ($outbound in $outbounds) { if ($outbound.type -in @("vless", "vmess", "trojan", "shadowsocks", "hysteria2")) { $servers += @{ tag = $outbound.tag type = $outbound.type server = $outbound.server server_port = $outbound.server_port outbound = $outbound } } } if ($servers.Count -eq 0) { Write-Error "Серверы не найдены в подписке!" return $null } $options = [Ordered]@{} for ($i = 0; $i -lt $servers.Count; $i++) { $s = $servers[$i] $options["$($i+1)"] = "$($s.tag) ($($s.server):$($s.server_port))" } $choice = Show-Menu -Title "🌐 Доступные серверы" -Options $options -Prompt "👉 Выберите сервер (номер)" $index = [int]$choice - 1 if ($index -lt 0 -or $index -ge $servers.Count) { Write-Error "Неверный выбор!" return $null } return $servers[$index] } function New-SingboxConfig { param($Outbound, $Port) return @{ log = @{ level = "info"; timestamp = $true } dns = @{ independent_cache = $true } inbounds = @( @{ type = "socks" tag = "socks-in" listen = "0.0.0.0" listen_port = $Port } ) outbounds = @( $Outbound, @{ type = "direct"; tag = "direct" } ) route = @{ final = $Outbound.tag auto_detect_interface = $true } } } function Parse-VlessUrl { param([string]$Url) if (-not $Url.StartsWith("vless://")) { throw "URL должен начинаться с vless://" } # Remove scheme $raw = $Url.Substring(8) # Split fragment $tag = "reality" if ($raw -match "#(.*)$") { $tag = [System.Web.HttpUtility]::UrlDecode($matches[1]) $raw = $raw -replace "#.*$", "" } # Split query $queryStr = "" if ($raw -match "\?(.*)$") { $queryStr = $matches[1] $raw = $raw -replace "\?.*$", "" } # Parse UUID@HOST:PORT if ($raw -notmatch "([^@]+)@([^:]+):(\d+)") { throw "Неверный формат vless (ожидается uuid@host:port)" } $uuid = $matches[1][0] $serverHost = $matches[2][0] $port = [int]$matches[3][0] # Fix for regex object access in PS if (-not $uuid) { # Fallback if regex returns match info differently in different PS versions $uuid = $matches[1] $serverHost = $matches[2] $port = [int]$matches[3] } # Parse Query $params = @{} if ($queryStr) { $parts = $queryStr -split "&" foreach ($p in $parts) { $kv = $p -split "=" if ($kv.Count -eq 2) { $params[[System.Web.HttpUtility]::UrlDecode($kv[0])] = [System.Web.HttpUtility]::UrlDecode($kv[1]) } } } # Extract $pbk = if ($params["pbk"]) { $params["pbk"] } else { throw "Отсутствует параметр pbk (Public Key)" } $sid = if ($params["sid"]) { $params["sid"] } else { throw "Отсутствует параметр sid (Short ID)" } $sni = if ($params["sni"]) { $params["sni"] } else { $serverHost } $fp = if ($params["fp"]) { $params["fp"] } else { "chrome" } $flow = if ($params["flow"]) { $params["flow"] } else { "" } return @{ uuid = $uuid server = $serverHost server_port = $port tag = $tag public_key = $pbk short_id = $sid server_name = $sni fingerprint = $fp flow = $flow } } # --- MAIN --- if ($Debug) { Set-DebugMode -Enabled $true } Write-Header "NATIVE SING-BOX (UDP ПОДДЕРЖКА)" -ClearScreen $taskStatus = Get-TaskStatus -Name $TaskName if ($taskStatus -and -not $Force) { Write-Info "Sing-box уже установлен." Write-Host " Статус: $taskStatus" -ForegroundColor ($taskStatus -eq "Running" ? "Green" : "Red") Write-Host "" $opts = [Ordered]@{ "1" = "Сменить сервер (из подписки)" "2" = "Ввести новую ссылку на подписку" "3" = "Перезапустить службу" "4" = "Остановить службу" "5" = "Показать конфиг" "6" = "Переустановить" "b" = "Назад" } $act = Show-Menu -Options $opts switch ($act) { "1" { # Reload existing sub logic could be added here, currently just re-runs install flow partially # Simplification: treat as new setup but try to load saved sub url $Force = $true } "2" { $SubscriptionUrl = ""; $Force = $true } "3" { Manage-ScheduledTask -Name $TaskName -Action "Start"; Write-Success "Запущено!"; exit } "4" { Manage-ScheduledTask -Name $TaskName -Action "Stop"; Write-Success "Остановлено!"; exit } "5" { Get-Content "$InstallDir\config.json"; exit } "6" { $Force = $true } "b" { exit } } } if ($Force -or -not $taskStatus) { # 1. Загрузка Write-Step "Установка Sing-box..." if (!(Test-Path "$InstallDir\sing-box.exe")) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null $zipCtx = "$env:TEMP\sing-box.zip" if (Download-File -Url $SingboxUrl -Destination $zipCtx) { Expand-Archive -Path $zipCtx -DestinationPath $env:TEMP -Force $extracted = Get-ChildItem "$env:TEMP\sing-box-*" -Directory | Select -First 1 Copy-Item "$($extracted.FullName)\sing-box.exe" "$InstallDir\sing-box.exe" -Force Remove-Item $zipCtx; Remove-Item $extracted.FullName -Recurse -Force Write-Success "Sing-box скачан" } else { Read-Host "Нажмите Enter для выхода..." exit 1 } } # 2. Подписка if ([string]::IsNullOrWhiteSpace($SubscriptionUrl)) { # Try load saved $savedSub = "$InstallDir\sub_info.json" if (Test-Path $savedSub) { try { $json = Get-Content $savedSub -Raw | ConvertFrom-Json if ($json.url) { Write-Info "Найдена сохраненная подписка: $($json.url)" if ((Read-Host "Использовать? (y/n)") -eq 'y') { $SubscriptionUrl = $json.url } } } catch {} } } if ([string]::IsNullOrWhiteSpace($SubscriptionUrl)) { $SubscriptionUrl = Read-Host "`n🔗 Введите URL подписки (VLESS)" } if ([string]::IsNullOrWhiteSpace($SubscriptionUrl)) { Write-Error "Url не указан" Read-Host "Нажмите Enter для выхода..." exit } # --- PARSING --- $data = @{ success = $false; config = $null; error = "" } if ($SubscriptionUrl.StartsWith("vless://")) { try { $p = Parse-VlessUrl -Url $SubscriptionUrl $outbound = [Ordered]@{ type = "vless" tag = $p.tag server = $p.server server_port = $p.server_port uuid = $p.uuid flow = $p.flow tls = @{ enabled = $true server_name = $p.server_name utls = @{ enabled = $true; fingerprint = $p.fingerprint } reality = @{ enabled = $true public_key = $p.public_key short_id = $p.short_id } } packet_encoding = "xudp" } $data.success = $true $data.config = @{ outbounds = @($outbound) } } catch { $data.error = $_.Exception.Message } } else { $data = Get-SubscriptionData -Url $SubscriptionUrl -Headers (Get-SubscriptionHeaders) } # --- PARSING LOGIC ENHANCEMENT --- if (-not $data.success) { # Fallback: Try to handle non-JSON body (Base64 or Plain Text) try { Write-Info "JSON парсинг не удался, пробую как список ссылок..." $content = $data.rawContent # Base64 decode if needed if ($content -match "^[A-Za-z0-9+/=]+$") { try { $bytes = [System.Convert]::FromBase64String($content) $content = [System.Text.Encoding]::UTF8.GetString($bytes) } catch {} } # Try to find vless:// links $links = $content -split "[\r\n]+" | Where-Object { $_ -match "^vless://" } if ($links.Count -gt 0) { Write-Success "Найдено ссылок: $($links.Count)" # Mock a config object with these links as "outbounds" # Note: We can't fully parsing VLESS query params in pure PS easily without a lot of regex # So we will try a simpler approach: Let sing-box do it? No, sing-box needs config. # WORKAROUND: Create a minimal outbound for each link # Parsing `vless://UUID@HOST:PORT?security=reality&...#NAME` $parsedOutbounds = @() foreach ($link in $links) { if ($link -match "vless://([^@]+)@([^:]+):(\d+)(\?.*)?(#.*)?") { $uuid = $matches[1] $server = $matches[2] $port = [int]$matches[3] $query = $matches[4] $hash = $matches[5] $tag = if ($hash) { $hash.Substring(1) } else { "${server}:${port}" } $tag = [System.Web.HttpUtility]::UrlDecode($tag) # Parse Query Params $flow = ""; $fp = ""; $pbk = ""; $sid = ""; $sni = ""; $serviceName = "" if ($query) { if ($query -match "flow=([^&]+)") { $flow = $matches[1] } if ($query -match "fp=([^&]+)") { $fp = $matches[1] } if ($query -match "pbk=([^&]+)") { $pbk = $matches[1] } if ($query -match "sid=([^&]+)") { $sid = $matches[1] } if ($query -match "sni=([^&]+)") { $sni = $matches[1] } if ($query -match "serviceName=([^&]+)") { $serviceName = $matches[1] } } # Construct Sing-box outbound (REALITY based assumption for modern vless) $out = [Ordered]@{ type = "vless" tag = $tag server = $server server_port = $port uuid = $uuid flow = $flow tls = @{ enabled = $true server_name = $sni utls = @{ enabled = $true; fingerprint = $fp } reality = @{ enabled = $true public_key = $pbk short_id = $sid } } packet_encoding = "xudp" } $parsedOutbounds += $out } } if ($parsedOutbounds.Count -gt 0) { $data.success = $true $data.config = @{ outbounds = $parsedOutbounds } $data.error = $null } else { throw "Не удалось распарсить VLESS ссылки" } } else { throw $data.error } } catch { Write-Error "Ошибка обработки подписки: $_" Write-Host " Скрипт поддерживает: SIP008 (JSON) или список VLESS+Reality ссылок." -ForegroundColor Yellow Read-Host "Нажмите Enter для выхода..." exit } } # Save sub info @{ url = $SubscriptionUrl } | ConvertTo-Json | Set-Content "$InstallDir\sub_info.json" # 3. Выбор сервера $server = Select-Server -Config $data.config if (!$server) { Read-Host "Нажмите Enter для выхода..." exit } # 4. Конфиг $cfg = New-SingboxConfig -Outbound $server.outbound -Port $LocalProxyPort $cfg | ConvertTo-Json -Depth 10 | Set-Content "$InstallDir\config.json" -Encoding UTF8 # 5. Задача Manage-ScheduledTask -Name $TaskName -ExePath "$InstallDir\sing-box.exe" -Arguments "run -c `"$InstallDir\config.json`"" -WorkDir $InstallDir -Action "Install" Manage-ScheduledTask -Name $TaskName -Action "Start" # 6. Firewall if (Ensure-FirewallPort -Port $LocalProxyPort -Name "SingBox-Proxy-Port") { Write-Success "Правило Firewall создано (порт $LocalProxyPort)" } Write-Success "Успешно установлено и запущено!" Write-Info "Локальный прокси: 127.0.0.1:$LocalProxyPort" $ips = Get-LocalIPs if ($ips) { Write-Info "Доступно из сети по адресам:" foreach ($ip in $ips) { Write-Host " ${ip}:$LocalProxyPort" -ForegroundColor Gray } } Start-Sleep -Seconds 3 }