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 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 14:58:21 +03:00
parent 88d5486607
commit 39b58e1a77
8 changed files with 266 additions and 39 deletions
+3
View File
@@ -5,6 +5,9 @@
xray.zip xray.zip
xray-embed.b64 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 / debug artifacts
*-preview.png *-preview.png
preview.png preview.png
+14
View File
@@ -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 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 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. 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.
+20 -2
View File
@@ -48,6 +48,20 @@ firewall-правила (при вкл/выкл рубильника).
Парсинг ссылки: Reality (pbk/sni/sid/spx/fp/flow) и транспорты tcp/grpc/ws. Парсинг ссылки: 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/H1H4). «Сохранить».
3. Рубильник **ВКЛ** — бинарь распакуется из встроенного, поднимется туннель, Claude уйдёт в него.
`wireproxy-awg` собирается **из исходников** (`build-wireproxy.ps1`) из зафиксированного,
проаудированного коммита (форк известного `wireproxy` с официальным `amnezia-vpn/amneziawg-go`).
## Требования ## Требования
- Windows 10/11 (нужен встроенный .NET Framework 4.x — есть из коробки). - 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`, Компилит через `csc` из .NET Framework (`C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe`,
есть на любой Windows — SDK не нужен). При первой сборке скачивает Xray-core (пинованную версию, есть на любой Windows — SDK не нужен). При первой сборке:
с проверкой SHA256) и встраивает как ресурс; иконку генерит `make-icon.ps1`; манифест — asInvoker. - скачивает **Xray-core** (пинованную версию, с проверкой SHA256);
- собирает **wireproxy-awg** из исходников (`build-wireproxy.ps1`: портативный Go + пиновый коммит) —
нужны `git` и интернет;
- оба бинаря встраивает как ресурсы; иконку генерит `make-icon.ps1`; манифест — asInvoker.
Результат — `dist\ClaudeProxy.exe`. Результат — `dist\ClaudeProxy.exe`.
Исходники (`native/`): Исходники (`native/`):
+10 -1
View File
@@ -34,6 +34,14 @@ if (-not (Test-Path $xrayZip)) {
Write-Host " Xray SHA256 ok." -ForegroundColor DarkGray 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' $out = Join-Path $dist 'ClaudeProxy.exe'
$refs = @('System.dll', 'System.Core.dll', 'System.Windows.Forms.dll', 'System.Drawing.dll', $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') 'System.IO.Compression.dll', 'System.IO.Compression.FileSystem.dll', 'Microsoft.CSharp.dll')
@@ -42,7 +50,8 @@ $args = @(
'/nologo', '/target:winexe', '/platform:x64', "/out:$out", '/nologo', '/target:winexe', '/platform:x64', "/out:$out",
"/win32icon:$(Join-Path $root 'icon.ico')", "/win32icon:$(Join-Path $root 'icon.ico')",
"/win32manifest:$(Join-Path $root 'native\app.manifest')", "/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" } foreach ($r in $refs) { $args += "/reference:$r" }
$args += (Join-Path $root 'native\Core.cs') $args += (Join-Path $root 'native\Core.cs')
+55
View File
@@ -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
+74 -1
View File
@@ -89,6 +89,7 @@ namespace ClaudeProxy
case "mode": c.Mode = v.Trim(); break; case "mode": c.Mode = v.Trim(); break;
case "proxy": c.Proxy = v.Trim(); break; case "proxy": c.Proxy = v.Trim(); break;
case "vless": c.Vless = v; 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 "localPort": int.TryParse(v.Trim(), out c.LocalPort); break;
case "failClosed": c.FailClosed = v.Trim() == "1" || v.Trim().ToLower() == "true"; 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("mode=" + c.Mode);
sb.AppendLine("proxy=" + c.Proxy); sb.AppendLine("proxy=" + c.Proxy);
sb.AppendLine("vless=" + (c.Vless ?? "")); sb.AppendLine("vless=" + (c.Vless ?? ""));
sb.AppendLine("awg=" + (c.Awg ?? ""));
sb.AppendLine("localPort=" + c.LocalPort); sb.AppendLine("localPort=" + c.LocalPort);
sb.AppendLine("failClosed=" + (c.FailClosed ? "1" : "0")); sb.AppendLine("failClosed=" + (c.FailClosed ? "1" : "0"));
File.WriteAllText(ConfigPath, sb.ToString()); File.WriteAllText(ConfigPath, sb.ToString());
@@ -271,7 +273,7 @@ namespace ClaudeProxy
var app = GetClaudeApp(); var app = GetClaudeApp();
RemoveFirewall(); RemoveFirewall();
dynamic policy = FwPolicy(); dynamic policy = FwPolicy();
if (cfg.Mode == "vless") if (cfg.Mode == "vless" || cfg.Mode == "awg")
{ {
// tunnel: block ALL external (loopback is exempt from firewall filtering) // tunnel: block ALL external (loopback is exempt from firewall filtering)
AddBlockRule(policy, "ClaudeProxyLock: tunnel", app.Exe, null); AddBlockRule(policy, "ClaudeProxyLock: tunnel", app.Exe, null);
@@ -389,5 +391,76 @@ namespace ClaudeProxy
public static bool XrayRunning() { return Process.GetProcessesByName("xray").Any(p => public static bool XrayRunning() { return Process.GetProcessesByName("xray").Any(p =>
{ try { return p.MainModule.FileName.StartsWith(BinDir, StringComparison.OrdinalIgnoreCase); } catch { return false; } }); } { 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; }
}
} }
} }
+71 -21
View File
@@ -23,7 +23,7 @@ namespace ClaudeProxy
fPill = new Font("Segoe UI Semibold", 12f), fMono = new Font("Consolas", 8.5f); fPill = new Font("Segoe UI Semibold", 12f), fMono = new Font("Consolas", 8.5f);
bool switchOn = false, busy = false; bool switchOn = false, busy = false;
RadioButton rbProxy, rbVless; RadioButton rbProxy, rbVless, rbAwg;
BufferedPanel mark, tgl, card; BufferedPanel mark, tgl, card;
Label title, subtitle, lblFieldCap, lblReach, rDot, lblFwCap, lblFw, lblClCap, lblCl, lblXrayCap, lblXray; Label title, subtitle, lblFieldCap, lblReach, rDot, lblFwCap, lblFw, lblClCap, lblCl, lblXrayCap, lblXray;
TextBox txtField, log; TextBox txtField, log;
@@ -32,7 +32,7 @@ namespace ClaudeProxy
Panel sep; Panel sep;
Timer timer; Timer timer;
string Mode { get { return rbVless.Checked ? "vless" : "proxy"; } } string Mode { get { return rbAwg.Checked ? "awg" : (rbVless.Checked ? "vless" : "proxy"); } }
public MainForm() public MainForm()
{ {
@@ -53,9 +53,10 @@ namespace ClaudeProxy
subtitle = MkLabel("весь трафик Claude через прокси", fSub, Muted, 84, 48); subtitle = MkLabel("весь трафик Claude через прокси", fSub, Muted, 84, 48);
subtitle.AutoSize = false; subtitle.Size = new Size(196, 18); Controls.Add(subtitle); subtitle.AutoSize = false; subtitle.Size = new Size(196, 18); Controls.Add(subtitle);
rbProxy = MkRadio("Прямой прокси", 292, 18); rbProxy = MkRadio("Прямой прокси", 292, 14);
rbVless = MkRadio("VLESS + Reality", 292, 42); rbVless = MkRadio("VLESS + Reality", 292, 36);
Controls.Add(rbProxy); Controls.Add(rbVless); 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 = 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); 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); 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); 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); 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); 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 }; 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); 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 = new Timer { Interval = 4000 };
timer.Tick += (s, e) => { if (!busy) UpdateUi(false); }; timer.Tick += (s, e) => { if (!busy) UpdateUi(false); };
@@ -111,7 +113,7 @@ namespace ClaudeProxy
Load += (s, e) => { Load += (s, e) => {
var cfg = Core.LoadConfig(); var cfg = Core.LoadConfig();
chkFail.Checked = cfg.FailClosed; 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(); ApplyModeLayout();
Log("готово. рубильник управляет проксированием Claude."); Log("готово. рубильник управляет проксированием Claude.");
UpdateUi(true); UpdateUi(true);
@@ -182,20 +184,27 @@ namespace ClaudeProxy
void ApplyModeLayout() void ApplyModeLayout()
{ {
var cfg = Core.LoadConfig(); 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") if (Mode == "vless")
{ {
lblFieldCap.Text = "VLESS"; subtitle.Text = "через VLESS + Reality тоннель"; lblFieldCap.Text = "VLESS"; subtitle.Text = "через VLESS + Reality тоннель";
txtField.UseSystemPasswordChar = true; txtField.Text = cfg.Vless ?? ""; txtField.UseSystemPasswordChar = true; txtField.Text = cfg.Vless ?? "";
lblXrayCap.Visible = lblXray.Visible = btnXray.Visible = true; lblXrayCap.Text = "Xray-core"; UpdateXrayStatus();
sep.Top = 100; lblFwCap.Top = lblFw.Top = 112; lblClCap.Top = lblCl.Top = 134; }
UpdateXrayStatus(); else if (Mode == "awg")
{
lblFieldCap.Text = "Конфиг"; subtitle.Text = "через AmneziaWG тоннель";
txtField.UseSystemPasswordChar = false; txtField.Text = cfg.Awg ?? "";
lblXrayCap.Text = "AmneziaWG"; UpdateAwgStatus();
} }
else else
{ {
lblFieldCap.Text = "Прокси"; subtitle.Text = "весь трафик Claude через прокси"; lblFieldCap.Text = "Прокси"; subtitle.Text = "весь трафик Claude через прокси";
txtField.UseSystemPasswordChar = false; txtField.Text = cfg.Proxy ?? ""; 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 = "Переустановить"; } if (Core.XrayPresent()) { lblXray.Text = "готов (" + Core.XrayVersion + ")"; lblXray.ForeColor = OkC; btnXray.Text = "Переустановить"; }
else { 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 ---- // ---- helpers ----
@@ -216,13 +233,15 @@ namespace ClaudeProxy
} }
// Mode can't be switched while the lock is active (ON) or during an operation. // 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() Config CaptureConfig()
{ {
var c = Core.LoadConfig(); var c = Core.LoadConfig();
c.Mode = Mode; c.FailClosed = chkFail.Checked; 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; Core.SaveConfig(c); return c;
} }
@@ -246,13 +265,15 @@ namespace ClaudeProxy
if (checkReach) if (checkReach)
{ {
bool ok; string ep; bool ok; string text;
if (Mode == "vless") ok = Vless.ServerReachable(txtField.Text, out ep); if (Mode == "vless") { string ep; ok = Vless.ServerReachable(txtField.Text, out ep); text = (ok ? "сервер доступен " : "не отвечает ") + 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; } } 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; rDot.ForeColor = ok ? OkC : RedC;
lblReach.Text = (ok ? "сервер доступен " : "не отвечает ") + ep; lblReach.Text = text;
} }
if (Mode == "vless") UpdateXrayStatus(); if (Mode == "vless") UpdateXrayStatus();
else if (Mode == "awg") UpdateAwgStatus();
} }
catch (Exception ex) { Log("ошибка обновления: " + ex.Message); } catch (Exception ex) { Log("ошибка обновления: " + ex.Message); }
} }
@@ -275,6 +296,18 @@ namespace ClaudeProxy
string ep; if (!Vless.ServerReachable(txtField.Text, out ep)) string ep; if (!Vless.ServerReachable(txtField.Text, out ep))
if (MessageBox.Show("Сервер " + ep + " не отвечает. Всё равно продолжить?", "Сервер недоступен", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return false; 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 else
{ {
string h; int p; string h; int p;
@@ -291,10 +324,11 @@ namespace ClaudeProxy
var cfg = Core.LoadConfig(); var cfg = Core.LoadConfig();
string proxyArg; string proxyArg;
if (cfg.Mode == "vless") { Core.EnsureXray(); Core.StartXray(cfg.Vless, cfg.LocalPort); proxyArg = "socks5://127.0.0.1:" + cfg.LocalPort; } 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); else proxyArg = Core.ProxyArg(cfg.Proxy);
if (cfg.FailClosed) 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); } 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 { } string exe = null; try { exe = Core.GetClaudeApp().Exe; } catch { }
if (Core.FirewallState(exe) != "absent") if (Core.FirewallState(exe) != "absent")
if (!Core.ElevateSelf("fwremove")) throw new Exception("Снятие firewall отменено в UAC (лок остался)."); if (!Core.ElevateSelf("fwremove")) throw new Exception("Снятие firewall отменено в UAC (лок остался).");
Core.StopXray(); Core.StopXray(); Core.StopAwg();
if (restart) { Core.StopClaude(); Core.LaunchClaude(""); } if (restart) { Core.StopClaude(); Core.LaunchClaude(""); }
} }
@@ -368,6 +402,7 @@ namespace ClaudeProxy
try 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 + "]"); } 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)); } 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("ВНИМАНИЕ: режим активен со старой настройкой — переключи рубильник, чтобы применить."); if (switchOn) Log("ВНИМАНИЕ: режим активен со старой настройкой — переключи рубильник, чтобы применить.");
UpdateUi(true); UpdateUi(true);
@@ -387,5 +422,20 @@ namespace ClaudeProxy
catch (Exception ex) { Log("ОШИБКА: " + ex.Message); MessageBox.Show(ex.Message, "Ошибка Xray", MessageBoxButtons.OK, MessageBoxIcon.Error); } catch (Exception ex) { Log("ОШИБКА: " + ex.Message); MessageBox.Show(ex.Message, "Ошибка Xray", MessageBoxButtons.OK, MessageBoxIcon.Error); }
finally { SetBusy(false); } 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);
}
}
} }
} }
+19 -14
View File
@@ -10,9 +10,10 @@ namespace ClaudeProxy
{ {
class Config class Config
{ {
public string Mode = "proxy"; // proxy | vless public string Mode = "proxy"; // proxy | vless | awg
public string Proxy = Core.DefaultProxy; public string Proxy = Core.DefaultProxy;
public string Vless = ""; public string Vless = "";
public string Awg = ""; // path to the user's AmneziaWG .conf
public int LocalPort = 10808; public int LocalPort = 10808;
public bool FailClosed = true; public bool FailClosed = true;
} }
@@ -163,24 +164,27 @@ namespace ClaudeProxy
catch { } catch { }
return set; return set;
} }
static HashSet<int> XrayPids() static HashSet<int> ProcPids(string name)
{ {
var set = new HashSet<int>(); var set = new HashSet<int>();
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); } try { if (p.MainModule.FileName.StartsWith(Core.BinDir, StringComparison.OrdinalIgnoreCase)) set.Add(p.Id); }
catch { } catch { }
return set; return set;
} }
static HashSet<int> XrayPids() { return ProcPids("xray"); }
static HashSet<int> AwgPids() { return ProcPids("wireproxy-awg"); }
public static VerifyResult Run(Config cfg) public static VerifyResult Run(Config cfg)
{ {
var r = new VerifyResult { Mode = cfg.Mode }; var r = new VerifyResult { Mode = cfg.Mode };
HashSet<string> proxySet; HashSet<string> proxySet;
bool loopIsProxy; bool loopIsProxy;
if (cfg.Mode == "vless") if (cfg.Mode == "vless" || cfg.Mode == "awg")
{ {
proxySet = new HashSet<string> { "127.0.0.1", "::1" }; proxySet = new HashSet<string> { "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; loopIsProxy = true;
} }
else else
@@ -205,22 +209,23 @@ namespace ClaudeProxy
r.Leaks.Add(entry); r.Leaks.Add(entry);
} }
if (cfg.Mode == "vless") if (cfg.Mode == "vless" || cfg.Mode == "awg")
{ {
var xp = XrayPids(); string proc = cfg.Mode == "awg" ? "wireproxy-awg" : "xray";
if (xp.Count == 0) r.Tunnel = "xray НЕ запущен"; var tp = cfg.Mode == "awg" ? AwgPids() : XrayPids();
if (tp.Count == 0) r.Tunnel = proc + " НЕ запущен";
else else
{ {
var xc = TcpConns(xp).FirstOrDefault(c => c.State == "Established" && c.Remote != "127.0.0.1" && c.Remote != "::1"); var xc = TcpConns(tp).FirstOrDefault(c => c.State == "Established" && c.Remote != "127.0.0.1" && c.Remote != "::1");
r.Tunnel = xc != null ? "xray -> " + xc.Remote + ":" + xc.RemotePort : "xray запущен, внешнего коннекта пока нет"; r.Tunnel = xc != null ? proc + " -> " + xc.Remote + ":" + xc.RemotePort : proc + " запущен, внешнего коннекта пока нет";
} }
} }
bool tunnelDown = cfg.Mode == "vless" && r.Tunnel == "xray НЕ запущен"; bool tunnelDown = (cfg.Mode == "vless" || cfg.Mode == "awg") && r.Tunnel.EndsWith("НЕ запущен");
string modeLabel = cfg.Mode == "vless" ? "VLESS+Reality" : "прямой прокси"; string modeLabel = cfg.Mode == "awg" ? "AmneziaWG" : (cfg.Mode == "vless" ? "VLESS+Reality" : "прямой прокси");
r.Lines.Add("Проверка трафика Claude (" + modeLabel + ")"); r.Lines.Add("Проверка трафика Claude (" + modeLabel + ")");
r.Lines.Add(" вход : " + r.Endpoint); 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(" процессов Claude: " + r.Procs);
r.Lines.Add(" через прокси : " + r.ViaProxy.Count); r.Lines.Add(" через прокси : " + r.ViaProxy.Count);
r.Lines.Add(" локальные : " + r.Local.Count); r.Lines.Add(" локальные : " + r.Local.Count);
@@ -233,7 +238,7 @@ namespace ClaudeProxy
else if (r.Leaks.Count > 0) r.Verdict = "ПРОВАЛ: есть прямой трафик мимо прокси"; else if (r.Leaks.Count > 0) r.Verdict = "ПРОВАЛ: есть прямой трафик мимо прокси";
else if (tunnelDown) r.Verdict = "ПРОВАЛ: тоннель (xray) не запущен"; else if (tunnelDown) r.Verdict = "ПРОВАЛ: тоннель (xray) не запущен";
else if (r.ViaProxy.Count == 0) r.Verdict = "НЕОПРЕДЕЛЁННО: нет активных коннектов (подожди загрузки Claude)"; 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 идёт через прокси"; else r.Verdict = "OK: весь внешний трафик Claude идёт через прокси";
r.Lines.Add(" ВЕРДИКТ : " + r.Verdict); r.Lines.Add(" ВЕРДИКТ : " + r.Verdict);
r.Ok = r.Leaks.Count == 0 && r.ViaProxy.Count > 0 && !tunnelDown; r.Ok = r.Leaks.Count == 0 && r.ViaProxy.Count > 0 && !tunnelDown;