feat: Реализована нативная установка sing-box с системными утилитами и веб-сервером, заменяя устаревшие скрипты.
This commit is contained in:
414
scripts/setup-singbox.ps1
Normal file
414
scripts/setup-singbox.ps1
Normal file
@@ -0,0 +1,414 @@
|
||||
# ==========================================
|
||||
# 📦 SING-BOX NATIVE INSTALLER
|
||||
# ==========================================
|
||||
|
||||
param(
|
||||
[switch]$Force,
|
||||
[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 ---
|
||||
|
||||
Write-Header "NATIVE SING-BOX (UDP ПОДДЕРЖКА)"
|
||||
|
||||
$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
|
||||
}
|
||||
Reference in New Issue
Block a user