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
+74 -1
View File
@@ -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; }
}
}
}