Files
vpn-proxy/scripts/setup-singbox.ps1

418 lines
15 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ==========================================
# 📦 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; output = "$InstallDir\singbox.log" }
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
}