From 39b58e1a77e52107cf0646a743ad1ea36fda740e Mon Sep 17 00:00:00 2001 From: gusadmin Date: Fri, 10 Jul 2026 14:58:21 +0300 Subject: [PATCH] Add AmneziaWG mode (wireproxy-awg userspace SOCKS) Third mode alongside proxy and VLESS+Reality. Bundles wireproxy-awg built from a pinned, audited source commit (fork of wireproxy over the official amneziawg-go), exposing a local SOCKS5 tunnel with no TUN/driver/admin. User picks their AmneziaWG .conf; the app appends a [Socks5] section and routes Claude through it with the same fail-closed firewall. build-wireproxy.ps1 reproducibly builds the binary (portable Go + pinned commit); it is embedded as a resource and not stored in git. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 ++ NOTICE.md | 14 +++++++ README.md | 22 ++++++++++- build-native.ps1 | 11 +++++- build-wireproxy.ps1 | 55 +++++++++++++++++++++++++++ native/Core.cs | 75 +++++++++++++++++++++++++++++++++++- native/MainForm.cs | 92 ++++++++++++++++++++++++++++++++++----------- native/Support.cs | 33 +++++++++------- 8 files changed, 266 insertions(+), 39 deletions(-) create mode 100644 build-wireproxy.ps1 diff --git a/.gitignore b/.gitignore index b65e3da..6ab1ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ xray.zip xray-embed.b64 +# wireproxy-awg binary (built from pinned source by build-wireproxy.ps1) - not stored in git +wireproxy-awg.exe + # preview / debug artifacts *-preview.png preview.png diff --git a/NOTICE.md b/NOTICE.md index 73e006f..167916c 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -13,3 +13,17 @@ This project bundles an **unmodified** binary of [Xray-core](https://github.com/ Bundling and invoking Xray-core as an independent executable ("mere aggregation") does not place this project's own code (MIT-licensed, see `LICENSE`) under the MPL. Redistribution of the Xray binary is permitted by MPL-2.0 provided its license text is included, which it is. + +## wireproxy-awg (AmneziaWG mode) + +The AmneziaWG mode bundles **wireproxy-awg**, built from source and run as a separate process. It +is a thin fork of `wireproxy` that uses the official AmneziaWG userspace implementation. + +- Built from a pinned, audited commit — see `build-wireproxy.ps1` (repo + commit hash). +- `wireproxy` / `wireproxy-awg`: **ISC** license (© Tsz Fung Wong). +- `github.com/amnezia-vpn/amneziawg-go` and `golang.zx2c4.com/wireguard`: **MIT** license. +- All run as independent executables ("mere aggregation"); this project's code stays MIT. + +Reproducible build: `build-wireproxy.ps1` fetches a portable Go toolchain, checks out the pinned +commit, and compiles a static `wireproxy-awg.exe`. It is embedded as a resource by +`build-native.ps1` and is **not** stored in git. diff --git a/README.md b/README.md index 8fa0deb..03a7811 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,20 @@ firewall-правила (при вкл/выкл рубильника). Парсинг ссылки: Reality (pbk/sni/sid/spx/fp/flow) и транспорты tcp/grpc/ws. +## Режим AmneziaWG + +Третий режим — **AmneziaWG** (WireGuard с обфускацией против DPI). Тоже userspace, без TUN/драйвера/ +админа: встроенный `wireproxy-awg` поднимает локальный SOCKS5 поверх AmneziaWG-туннеля, Claude идёт +в него (fail-closed как в VLESS). + +1. Переключи режим на **AmneziaWG**. +2. Кнопка **«Обзор...»** → выбери свой `.conf` от AmneziaWG (экспорт из приложения Amnezia или + с сервера — с обфускацией Jc/Jmin/Jmax/S1/S2/H1–H4). «Сохранить». +3. Рубильник **ВКЛ** — бинарь распакуется из встроенного, поднимется туннель, Claude уйдёт в него. + +`wireproxy-awg` собирается **из исходников** (`build-wireproxy.ps1`) из зафиксированного, +проаудированного коммита (форк известного `wireproxy` с официальным `amnezia-vpn/amneziawg-go`). + ## Требования - Windows 10/11 (нужен встроенный .NET Framework 4.x — есть из коробки). @@ -63,8 +77,12 @@ powershell -ExecutionPolicy Bypass -File build-native.ps1 ``` Компилит через `csc` из .NET Framework (`C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe`, -есть на любой Windows — SDK не нужен). При первой сборке скачивает Xray-core (пинованную версию, -с проверкой SHA256) и встраивает как ресурс; иконку генерит `make-icon.ps1`; манифест — asInvoker. +есть на любой Windows — SDK не нужен). При первой сборке: +- скачивает **Xray-core** (пинованную версию, с проверкой SHA256); +- собирает **wireproxy-awg** из исходников (`build-wireproxy.ps1`: портативный Go + пиновый коммит) — + нужны `git` и интернет; +- оба бинаря встраивает как ресурсы; иконку генерит `make-icon.ps1`; манифест — asInvoker. + Результат — `dist\ClaudeProxy.exe`. Исходники (`native/`): diff --git a/build-native.ps1 b/build-native.ps1 index 44b15c6..bdb6e29 100644 --- a/build-native.ps1 +++ b/build-native.ps1 @@ -34,6 +34,14 @@ if (-not (Test-Path $xrayZip)) { Write-Host " Xray SHA256 ok." -ForegroundColor DarkGray } +# wireproxy-awg (AmneziaWG userspace SOCKS). Built from pinned source; not stored in git. +$awgExe = Join-Path $root 'wireproxy-awg.exe' +if (-not (Test-Path $awgExe)) { + Write-Host "Building wireproxy-awg.exe (needs git; downloads a portable Go toolchain)..." -ForegroundColor Cyan + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $root 'build-wireproxy.ps1') + if (-not (Test-Path $awgExe)) { throw "wireproxy-awg.exe build failed." } +} + $out = Join-Path $dist 'ClaudeProxy.exe' $refs = @('System.dll', 'System.Core.dll', 'System.Windows.Forms.dll', 'System.Drawing.dll', 'System.IO.Compression.dll', 'System.IO.Compression.FileSystem.dll', 'Microsoft.CSharp.dll') @@ -42,7 +50,8 @@ $args = @( '/nologo', '/target:winexe', '/platform:x64', "/out:$out", "/win32icon:$(Join-Path $root 'icon.ico')", "/win32manifest:$(Join-Path $root 'native\app.manifest')", - "/resource:$(Join-Path $root 'xray.zip'),ClaudeProxy.xray.zip" + "/resource:$(Join-Path $root 'xray.zip'),ClaudeProxy.xray.zip", + "/resource:$(Join-Path $root 'wireproxy-awg.exe'),ClaudeProxy.awg.exe" ) foreach ($r in $refs) { $args += "/reference:$r" } $args += (Join-Path $root 'native\Core.cs') diff --git a/build-wireproxy.ps1 b/build-wireproxy.ps1 new file mode 100644 index 0000000..8dfa512 --- /dev/null +++ b/build-wireproxy.ps1 @@ -0,0 +1,55 @@ +<# + build-wireproxy.ps1 - reproducibly build wireproxy-awg.exe from a pinned, audited commit. + Output: wireproxy-awg.exe in the repo root (embedded into ClaudeProxy.exe by build-native.ps1). + Uses a portable Go toolchain in a temp cache (does not touch the system Go). +#> +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $MyInvocation.MyCommand.Path +$out = Join-Path $root 'wireproxy-awg.exe' + +# pinned, audited source +$repo = 'https://github.com/artem-russkikh/wireproxy-awg.git' +$commit = '5d12c1937a72591342f1c78bc92a8e50e7ee92bb' # v1.0.17 +$goVer = 'go1.26.5' + +$cache = Join-Path $env:TEMP 'claude-proxy-gobuild' +New-Item -ItemType Directory -Force -Path $cache | Out-Null +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + +# portable Go +$goExe = Join-Path $cache 'goroot\go\bin\go.exe' +if (-not (Test-Path $goExe)) { + Write-Host "Fetching portable $goVer..." -ForegroundColor Cyan + $zip = Join-Path $cache 'go.zip' + Invoke-WebRequest "https://go.dev/dl/$goVer.windows-amd64.zip" -OutFile $zip -UseBasicParsing + Expand-Archive $zip -DestinationPath (Join-Path $cache 'goroot') -Force + Remove-Item $zip +} + +# fetch the exact pinned commit +$src = Join-Path $cache 'wireproxy-awg' +if (Test-Path $src) { Remove-Item $src -Recurse -Force } +New-Item -ItemType Directory -Force -Path $src | Out-Null +Push-Location $src +try { + git init -q + git remote add origin $repo + git fetch -q --depth 1 origin $commit + git checkout -q FETCH_HEAD + $head = (git rev-parse HEAD).Trim() + if ($head -ne $commit) { throw "commit mismatch: got $head, expected $commit" } + Write-Host "Source pinned at $commit" -ForegroundColor DarkGray + + $env:GOROOT = Join-Path $cache 'goroot\go' + $env:GOPATH = Join-Path $cache 'gopath' + $env:GOCACHE = Join-Path $cache 'gocache' + $env:GOTOOLCHAIN = 'local' + $env:GOOS = 'windows'; $env:GOARCH = 'amd64'; $env:CGO_ENABLED = '0' + Write-Host "Building wireproxy-awg.exe..." -ForegroundColor Cyan + & $goExe build -trimpath -ldflags '-s -w' -o $out ./cmd/wireproxy + if ($LASTEXITCODE -ne 0) { throw "go build failed ($LASTEXITCODE)" } +} +finally { Pop-Location } + +$sz = [math]::Round((Get-Item $out).Length / 1MB, 1) +Write-Host "OK: $out ($sz MB)" -ForegroundColor Green diff --git a/native/Core.cs b/native/Core.cs index 0bdf27d..be6f60a 100644 --- a/native/Core.cs +++ b/native/Core.cs @@ -89,6 +89,7 @@ namespace ClaudeProxy case "mode": c.Mode = v.Trim(); break; case "proxy": c.Proxy = v.Trim(); break; case "vless": c.Vless = v; break; + case "awg": c.Awg = v.Trim(); break; case "localPort": int.TryParse(v.Trim(), out c.LocalPort); break; case "failClosed": c.FailClosed = v.Trim() == "1" || v.Trim().ToLower() == "true"; break; } @@ -108,6 +109,7 @@ namespace ClaudeProxy sb.AppendLine("mode=" + c.Mode); sb.AppendLine("proxy=" + c.Proxy); sb.AppendLine("vless=" + (c.Vless ?? "")); + sb.AppendLine("awg=" + (c.Awg ?? "")); sb.AppendLine("localPort=" + c.LocalPort); sb.AppendLine("failClosed=" + (c.FailClosed ? "1" : "0")); File.WriteAllText(ConfigPath, sb.ToString()); @@ -271,7 +273,7 @@ namespace ClaudeProxy var app = GetClaudeApp(); RemoveFirewall(); dynamic policy = FwPolicy(); - if (cfg.Mode == "vless") + if (cfg.Mode == "vless" || cfg.Mode == "awg") { // tunnel: block ALL external (loopback is exempt from firewall filtering) AddBlockRule(policy, "ClaudeProxyLock: tunnel", app.Exe, null); @@ -389,5 +391,76 @@ namespace ClaudeProxy public static bool XrayRunning() { return Process.GetProcessesByName("xray").Any(p => { try { return p.MainModule.FileName.StartsWith(BinDir, StringComparison.OrdinalIgnoreCase); } catch { return false; } }); } + + // ---- AmneziaWG (wireproxy-awg userspace SOCKS) ---------------------------- + public static string AwgExe { get { return Path.Combine(BinDir, "wireproxy-awg.exe"); } } + public static string AwgRuntimeCfg { get { return Path.Combine(ConfigDir, "awg-runtime.conf"); } } + public static bool AwgPresent() { return File.Exists(AwgExe); } + + public static void EnsureAwg() + { + if (AwgPresent()) return; + Directory.CreateDirectory(BinDir); + var asm = Assembly.GetExecutingAssembly(); + string res = asm.GetManifestResourceNames().FirstOrDefault(n => n.EndsWith("awg.exe", StringComparison.OrdinalIgnoreCase)); + if (res == null) throw new Exception("Встроенный wireproxy-awg не найден."); + using (var rs = asm.GetManifestResourceStream(res)) + using (var fs = File.Create(AwgExe)) + rs.CopyTo(fs); + if (!AwgPresent()) throw new Exception("Не удалось распаковать wireproxy-awg."); + } + + public static void StopAwg() + { + foreach (var p in Process.GetProcessesByName("wireproxy-awg")) + try { if (p.MainModule.FileName.StartsWith(BinDir, StringComparison.OrdinalIgnoreCase)) p.Kill(); } catch { } + Thread.Sleep(300); + } + public static bool AwgRunning() { return Process.GetProcessesByName("wireproxy-awg").Any(p => + { try { return p.MainModule.FileName.StartsWith(BinDir, StringComparison.OrdinalIgnoreCase); } catch { return false; } }); } + + // Take the user's AmneziaWG .conf, append a [Socks5] section, run wireproxy-awg on it. + public static void StartAwg(string confPath, int localPort) + { + EnsureAwg(); + if (string.IsNullOrWhiteSpace(confPath) || !File.Exists(confPath)) + throw new Exception("Файл AmneziaWG-конфига не найден: " + confPath); + string cfg = File.ReadAllText(confPath); + if (cfg.IndexOf("[Socks5]", StringComparison.OrdinalIgnoreCase) < 0) + cfg = cfg.TrimEnd() + "\r\n\r\n[Socks5]\r\nBindAddress = 127.0.0.1:" + localPort + "\r\n"; + File.WriteAllText(AwgRuntimeCfg, cfg); + StopAwg(); + var psi = new ProcessStartInfo + { + FileName = AwgExe, + Arguments = "-s -c \"" + AwgRuntimeCfg + "\"", + UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden + }; + Process.Start(psi); + for (int i = 0; i < 40; i++) { if (LocalSocksUp(localPort)) return; Thread.Sleep(200); } + StopAwg(); + throw new Exception("wireproxy-awg не поднял SOCKS на 127.0.0.1:" + localPort + ". Проверь конфиг AmneziaWG."); + } + + // Info line for the AmneziaWG config (its Peer Endpoint). WG is UDP, so we don't TCP-ping it. + public static bool AwgConfigInfo(string confPath, out string info) + { + info = "не выбран"; + if (string.IsNullOrWhiteSpace(confPath) || !File.Exists(confPath)) return false; + try + { + foreach (var line in File.ReadAllLines(confPath)) + { + var l = line.Trim(); + if (l.StartsWith("Endpoint", StringComparison.OrdinalIgnoreCase)) + { + int eq = l.IndexOf('='); + if (eq >= 0) { info = l.Substring(eq + 1).Trim(); return true; } + } + } + info = "выбран"; return true; + } + catch { info = "ошибка чтения"; return false; } + } } } diff --git a/native/MainForm.cs b/native/MainForm.cs index 871d569..1456383 100644 --- a/native/MainForm.cs +++ b/native/MainForm.cs @@ -23,7 +23,7 @@ namespace ClaudeProxy fPill = new Font("Segoe UI Semibold", 12f), fMono = new Font("Consolas", 8.5f); bool switchOn = false, busy = false; - RadioButton rbProxy, rbVless; + RadioButton rbProxy, rbVless, rbAwg; BufferedPanel mark, tgl, card; Label title, subtitle, lblFieldCap, lblReach, rDot, lblFwCap, lblFw, lblClCap, lblCl, lblXrayCap, lblXray; TextBox txtField, log; @@ -32,7 +32,7 @@ namespace ClaudeProxy Panel sep; Timer timer; - string Mode { get { return rbVless.Checked ? "vless" : "proxy"; } } + string Mode { get { return rbAwg.Checked ? "awg" : (rbVless.Checked ? "vless" : "proxy"); } } public MainForm() { @@ -53,9 +53,10 @@ namespace ClaudeProxy subtitle = MkLabel("весь трафик Claude через прокси", fSub, Muted, 84, 48); subtitle.AutoSize = false; subtitle.Size = new Size(196, 18); Controls.Add(subtitle); - rbProxy = MkRadio("Прямой прокси", 292, 18); - rbVless = MkRadio("VLESS + Reality", 292, 42); - Controls.Add(rbProxy); Controls.Add(rbVless); + rbProxy = MkRadio("Прямой прокси", 292, 14); + rbVless = MkRadio("VLESS + Reality", 292, 36); + rbAwg = MkRadio("AmneziaWG", 292, 58); + Controls.Add(rbProxy); Controls.Add(rbVless); Controls.Add(rbAwg); tgl = new BufferedPanel { Size = new Size(172, 58), Location = new Point(24, 88), BackColor = Cream, Cursor = Cursors.Hand }; tgl.Paint += (s, e) => PaintToggle(e.Graphics, tgl.Width, tgl.Height); @@ -83,7 +84,7 @@ namespace ClaudeProxy lblXrayCap = MkLabel("Xray-core", fReg, Muted, 18, 68); lblXrayCap.AutoSize = true; card.Controls.Add(lblXrayCap); lblXray = MkLabel("", fSemi, Ink, 120, 68); lblXray.AutoSize = true; card.Controls.Add(lblXray); - btnXray = MkButton("Распаковать", 300, 64, 96, 26, false); btnXray.Click += (s, e) => OnXray(); card.Controls.Add(btnXray); + btnXray = MkButton("Распаковать", 300, 64, 96, 26, false); btnXray.Click += (s, e) => { if (Mode == "awg") OnBrowseAwg(); else OnXray(); }; card.Controls.Add(btnXray); sep = new Panel { Location = new Point(18, 86), Size = new Size(378, 1), BackColor = Border }; card.Controls.Add(sep); lblFwCap = MkLabel("Firewall-лок", fReg, Muted, 18, 98); lblFwCap.AutoSize = true; card.Controls.Add(lblFwCap); @@ -102,7 +103,8 @@ namespace ClaudeProxy log = new TextBox { Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical, Location = new Point(20, 430), Size = new Size(412, 66), BorderStyle = BorderStyle.FixedSingle, BackColor = Card, ForeColor = Muted, Font = fMono }; Controls.Add(log); - rbVless.CheckedChanged += (s, e) => { ApplyModeLayout(); var c = Core.LoadConfig(); c.Mode = Mode; Core.SaveConfig(c); UpdateUi(true); }; + EventHandler modeCh = (s, e) => { if (!((RadioButton)s).Checked) return; ApplyModeLayout(); var c = Core.LoadConfig(); c.Mode = Mode; Core.SaveConfig(c); UpdateUi(true); }; + rbVless.CheckedChanged += modeCh; rbAwg.CheckedChanged += modeCh; rbProxy.CheckedChanged += modeCh; timer = new Timer { Interval = 4000 }; timer.Tick += (s, e) => { if (!busy) UpdateUi(false); }; @@ -111,7 +113,7 @@ namespace ClaudeProxy Load += (s, e) => { var cfg = Core.LoadConfig(); chkFail.Checked = cfg.FailClosed; - if (cfg.Mode == "vless") rbVless.Checked = true; else rbProxy.Checked = true; + if (cfg.Mode == "awg") rbAwg.Checked = true; else if (cfg.Mode == "vless") rbVless.Checked = true; else rbProxy.Checked = true; ApplyModeLayout(); Log("готово. рубильник управляет проксированием Claude."); UpdateUi(true); @@ -182,20 +184,27 @@ namespace ClaudeProxy void ApplyModeLayout() { var cfg = Core.LoadConfig(); + bool tunnel = Mode == "vless" || Mode == "awg"; + lblXrayCap.Visible = lblXray.Visible = btnXray.Visible = tunnel; + if (tunnel) { sep.Top = 100; lblFwCap.Top = lblFw.Top = 112; lblClCap.Top = lblCl.Top = 134; } + else { sep.Top = 74; lblFwCap.Top = lblFw.Top = 86; lblClCap.Top = lblCl.Top = 108; } + if (Mode == "vless") { lblFieldCap.Text = "VLESS"; subtitle.Text = "через VLESS + Reality тоннель"; txtField.UseSystemPasswordChar = true; txtField.Text = cfg.Vless ?? ""; - lblXrayCap.Visible = lblXray.Visible = btnXray.Visible = true; - sep.Top = 100; lblFwCap.Top = lblFw.Top = 112; lblClCap.Top = lblCl.Top = 134; - UpdateXrayStatus(); + lblXrayCap.Text = "Xray-core"; UpdateXrayStatus(); + } + else if (Mode == "awg") + { + lblFieldCap.Text = "Конфиг"; subtitle.Text = "через AmneziaWG тоннель"; + txtField.UseSystemPasswordChar = false; txtField.Text = cfg.Awg ?? ""; + lblXrayCap.Text = "AmneziaWG"; UpdateAwgStatus(); } else { lblFieldCap.Text = "Прокси"; subtitle.Text = "весь трафик Claude через прокси"; txtField.UseSystemPasswordChar = false; txtField.Text = cfg.Proxy ?? ""; - lblXrayCap.Visible = lblXray.Visible = btnXray.Visible = false; - sep.Top = 74; lblFwCap.Top = lblFw.Top = 86; lblClCap.Top = lblCl.Top = 108; } } @@ -203,6 +212,14 @@ namespace ClaudeProxy { if (Core.XrayPresent()) { lblXray.Text = "готов (" + Core.XrayVersion + ")"; lblXray.ForeColor = OkC; btnXray.Text = "Переустановить"; } else { lblXray.Text = "встроен (" + Core.XrayVersion + ")"; lblXray.ForeColor = OkC; btnXray.Text = "Распаковать"; } + btnXray.Width = 96; btnXray.Left = 300; + } + + void UpdateAwgStatus() + { + bool ok = !string.IsNullOrWhiteSpace(txtField.Text) && System.IO.File.Exists(txtField.Text.Trim()); + lblXray.Text = ok ? "конфиг выбран" : "не выбран"; lblXray.ForeColor = ok ? OkC : RedC; + btnXray.Text = "Обзор..."; btnXray.Width = 96; btnXray.Left = 300; } // ---- helpers ---- @@ -216,13 +233,15 @@ namespace ClaudeProxy } // Mode can't be switched while the lock is active (ON) or during an operation. - void UpdateModeLock() { rbProxy.Enabled = rbVless.Enabled = !switchOn && !busy; } + void UpdateModeLock() { rbProxy.Enabled = rbVless.Enabled = rbAwg.Enabled = !switchOn && !busy; } Config CaptureConfig() { var c = Core.LoadConfig(); c.Mode = Mode; c.FailClosed = chkFail.Checked; - if (Mode == "vless") c.Vless = txtField.Text; else c.Proxy = txtField.Text; + if (Mode == "vless") c.Vless = txtField.Text; + else if (Mode == "awg") c.Awg = txtField.Text; + else c.Proxy = txtField.Text; Core.SaveConfig(c); return c; } @@ -246,13 +265,15 @@ namespace ClaudeProxy if (checkReach) { - bool ok; string ep; - if (Mode == "vless") ok = Vless.ServerReachable(txtField.Text, out ep); - else { string h; int p; if (Core.TryParseProxy(txtField.Text, out h, out p)) { ep = h + ":" + p; ok = Core.TcpReachable(h, p, 1500); } else { ep = "некорректный адрес"; ok = false; } } + bool ok; string text; + if (Mode == "vless") { string ep; ok = Vless.ServerReachable(txtField.Text, out ep); text = (ok ? "сервер доступен " : "не отвечает ") + ep; } + else if (Mode == "awg") { string ep; ok = Core.AwgConfigInfo(txtField.Text, out ep); text = ok ? ("конфиг: " + ep) : "конфиг не выбран"; } + else { string h; int p; if (Core.TryParseProxy(txtField.Text, out h, out p)) { ok = Core.TcpReachable(h, p, 1500); text = (ok ? "сервер доступен " : "не отвечает ") + h + ":" + p; } else { ok = false; text = "некорректный адрес"; } } rDot.ForeColor = ok ? OkC : RedC; - lblReach.Text = (ok ? "сервер доступен " : "не отвечает ") + ep; + lblReach.Text = text; } if (Mode == "vless") UpdateXrayStatus(); + else if (Mode == "awg") UpdateAwgStatus(); } catch (Exception ex) { Log("ошибка обновления: " + ex.Message); } } @@ -275,6 +296,18 @@ namespace ClaudeProxy string ep; if (!Vless.ServerReachable(txtField.Text, out ep)) if (MessageBox.Show("Сервер " + ep + " не отвечает. Всё равно продолжить?", "Сервер недоступен", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return false; } + else if (Mode == "awg") + { + if (string.IsNullOrWhiteSpace(txtField.Text) || !System.IO.File.Exists(txtField.Text.Trim())) + { MessageBox.Show("Выбери файл AmneziaWG .conf (кнопка «Обзор...»).", "Нужен конфиг", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } + if (!Core.AwgPresent()) + { + SetBusy(true); + try { Core.EnsureAwg(); UpdateAwgStatus(); } + catch (Exception ex) { Log("ОШИБКА AmneziaWG: " + ex.Message); MessageBox.Show(ex.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); SetBusy(false); return false; } + SetBusy(false); + } + } else { string h; int p; @@ -291,10 +324,11 @@ namespace ClaudeProxy var cfg = Core.LoadConfig(); string proxyArg; if (cfg.Mode == "vless") { Core.EnsureXray(); Core.StartXray(cfg.Vless, cfg.LocalPort); proxyArg = "socks5://127.0.0.1:" + cfg.LocalPort; } + else if (cfg.Mode == "awg") { Core.EnsureAwg(); Core.StartAwg(cfg.Awg, cfg.LocalPort); proxyArg = "socks5://127.0.0.1:" + cfg.LocalPort; } else proxyArg = Core.ProxyArg(cfg.Proxy); if (cfg.FailClosed) - if (!Core.ElevateSelf("fwapply")) { if (cfg.Mode == "vless") Core.StopXray(); throw new Exception("Установка firewall отменена в UAC."); } + if (!Core.ElevateSelf("fwapply")) { Core.StopXray(); Core.StopAwg(); throw new Exception("Установка firewall отменена в UAC."); } if (restart) { Core.StopClaude(); string a = "--proxy-server=" + proxyArg; Core.LaunchClaude(a); } } @@ -304,7 +338,7 @@ namespace ClaudeProxy string exe = null; try { exe = Core.GetClaudeApp().Exe; } catch { } if (Core.FirewallState(exe) != "absent") if (!Core.ElevateSelf("fwremove")) throw new Exception("Снятие firewall отменено в UAC (лок остался)."); - Core.StopXray(); + Core.StopXray(); Core.StopAwg(); if (restart) { Core.StopClaude(); Core.LaunchClaude(""); } } @@ -368,6 +402,7 @@ namespace ClaudeProxy try { if (Mode == "vless") { string h, u, rm; int p; Vless.Parse(txtField.Text, out h, out p, out u, out rm); CaptureConfig(); Log("VLESS сохранён: " + h + ":" + p + " [" + rm + "]"); } + else if (Mode == "awg") { if (string.IsNullOrWhiteSpace(txtField.Text) || !System.IO.File.Exists(txtField.Text.Trim())) throw new Exception("Файл AmneziaWG-конфига не найден."); CaptureConfig(); Log("AmneziaWG-конфиг сохранён: " + txtField.Text.Trim()); } else { string h; int p; if (!Core.TryParseProxy(txtField.Text, out h, out p)) throw new Exception("Некорректный адрес."); CaptureConfig(); Log("прокси сохранён: " + Core.ProxyArg(txtField.Text)); } if (switchOn) Log("ВНИМАНИЕ: режим активен со старой настройкой — переключи рубильник, чтобы применить."); UpdateUi(true); @@ -387,5 +422,20 @@ namespace ClaudeProxy catch (Exception ex) { Log("ОШИБКА: " + ex.Message); MessageBox.Show(ex.Message, "Ошибка Xray", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { SetBusy(false); } } + + void OnBrowseAwg() + { + using (var dlg = new OpenFileDialog()) + { + dlg.Title = "Выбери AmneziaWG .conf"; + dlg.Filter = "AmneziaWG config (*.conf)|*.conf|Все файлы (*.*)|*.*"; + if (dlg.ShowDialog() != DialogResult.OK) return; + txtField.Text = dlg.FileName; + CaptureConfig(); + UpdateAwgStatus(); + Log("конфиг выбран: " + dlg.FileName); + UpdateUi(true); + } + } } } diff --git a/native/Support.cs b/native/Support.cs index c07bc11..e59a390 100644 --- a/native/Support.cs +++ b/native/Support.cs @@ -10,9 +10,10 @@ namespace ClaudeProxy { class Config { - public string Mode = "proxy"; // proxy | vless + public string Mode = "proxy"; // proxy | vless | awg public string Proxy = Core.DefaultProxy; public string Vless = ""; + public string Awg = ""; // path to the user's AmneziaWG .conf public int LocalPort = 10808; public bool FailClosed = true; } @@ -163,24 +164,27 @@ namespace ClaudeProxy catch { } return set; } - static HashSet XrayPids() + static HashSet ProcPids(string name) { var set = new HashSet(); - foreach (var p in Process.GetProcessesByName("xray")) + foreach (var p in Process.GetProcessesByName(name)) try { if (p.MainModule.FileName.StartsWith(Core.BinDir, StringComparison.OrdinalIgnoreCase)) set.Add(p.Id); } catch { } return set; } + static HashSet XrayPids() { return ProcPids("xray"); } + static HashSet AwgPids() { return ProcPids("wireproxy-awg"); } public static VerifyResult Run(Config cfg) { var r = new VerifyResult { Mode = cfg.Mode }; HashSet proxySet; bool loopIsProxy; - if (cfg.Mode == "vless") + if (cfg.Mode == "vless" || cfg.Mode == "awg") { proxySet = new HashSet { "127.0.0.1", "::1" }; - r.Endpoint = "127.0.0.1:" + cfg.LocalPort + " (VLESS+Reality)"; + string lbl = cfg.Mode == "awg" ? "AmneziaWG" : "VLESS+Reality"; + r.Endpoint = "127.0.0.1:" + cfg.LocalPort + " (" + lbl + ")"; loopIsProxy = true; } else @@ -205,22 +209,23 @@ namespace ClaudeProxy r.Leaks.Add(entry); } - if (cfg.Mode == "vless") + if (cfg.Mode == "vless" || cfg.Mode == "awg") { - var xp = XrayPids(); - if (xp.Count == 0) r.Tunnel = "xray НЕ запущен"; + string proc = cfg.Mode == "awg" ? "wireproxy-awg" : "xray"; + var tp = cfg.Mode == "awg" ? AwgPids() : XrayPids(); + if (tp.Count == 0) r.Tunnel = proc + " НЕ запущен"; else { - var xc = TcpConns(xp).FirstOrDefault(c => c.State == "Established" && c.Remote != "127.0.0.1" && c.Remote != "::1"); - r.Tunnel = xc != null ? "xray -> " + xc.Remote + ":" + xc.RemotePort : "xray запущен, внешнего коннекта пока нет"; + var xc = TcpConns(tp).FirstOrDefault(c => c.State == "Established" && c.Remote != "127.0.0.1" && c.Remote != "::1"); + r.Tunnel = xc != null ? proc + " -> " + xc.Remote + ":" + xc.RemotePort : proc + " запущен, внешнего коннекта пока нет"; } } - bool tunnelDown = cfg.Mode == "vless" && r.Tunnel == "xray НЕ запущен"; - string modeLabel = cfg.Mode == "vless" ? "VLESS+Reality" : "прямой прокси"; + bool tunnelDown = (cfg.Mode == "vless" || cfg.Mode == "awg") && r.Tunnel.EndsWith("НЕ запущен"); + string modeLabel = cfg.Mode == "awg" ? "AmneziaWG" : (cfg.Mode == "vless" ? "VLESS+Reality" : "прямой прокси"); r.Lines.Add("Проверка трафика Claude (" + modeLabel + ")"); r.Lines.Add(" вход : " + r.Endpoint); - if (cfg.Mode == "vless") r.Lines.Add(" тоннель : " + r.Tunnel); + if (cfg.Mode == "vless" || cfg.Mode == "awg") r.Lines.Add(" тоннель : " + r.Tunnel); r.Lines.Add(" процессов Claude: " + r.Procs); r.Lines.Add(" через прокси : " + r.ViaProxy.Count); r.Lines.Add(" локальные : " + r.Local.Count); @@ -233,7 +238,7 @@ namespace ClaudeProxy else if (r.Leaks.Count > 0) r.Verdict = "ПРОВАЛ: есть прямой трафик мимо прокси"; else if (tunnelDown) r.Verdict = "ПРОВАЛ: тоннель (xray) не запущен"; else if (r.ViaProxy.Count == 0) r.Verdict = "НЕОПРЕДЕЛЁННО: нет активных коннектов (подожди загрузки Claude)"; - else if (cfg.Mode == "vless") r.Verdict = "OK: весь трафик Claude идёт в VLESS+Reality тоннель"; + else if (cfg.Mode == "vless" || cfg.Mode == "awg") r.Verdict = "OK: весь трафик Claude идёт в тоннель (" + modeLabel + ")"; else r.Verdict = "OK: весь внешний трафик Claude идёт через прокси"; r.Lines.Add(" ВЕРДИКТ : " + r.Verdict); r.Ok = r.Leaks.Count == 0 && r.ViaProxy.Count > 0 && !tunnelDown;