Files
claude-proxy/native/Core.cs
T
gusadmin 39b58e1a77 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>
2026-07-10 14:58:21 +03:00

467 lines
21 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text;
using System.Threading;
namespace ClaudeProxy
{
// ---- MSIX activation (launch a packaged app with arguments) -------------------
[ComImport, Guid("2e941141-7f97-4756-ba1d-9decde894a3d"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IApplicationActivationManager
{
int ActivateApplication([In, MarshalAs(UnmanagedType.LPWStr)] string appUserModelId,
[In, MarshalAs(UnmanagedType.LPWStr)] string arguments,
[In] int options, [Out] out uint processId);
int ActivateForFile();
int ActivateForProtocol();
}
[ComImport, Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
class ApplicationActivationManager { }
class ClaudeApp { public string InstallLocation; public string Exe; public string Aumid; }
static class Core
{
public const string XrayVersion = "v26.3.27";
public const string PackageFamily = "Claude_pzs8sxrjxfjjc";
public const string Aumid = "Claude_pzs8sxrjxfjjc!Claude";
public const string FwGroup = "ClaudeProxyLock";
public const string DefaultProxy = "socks5://127.0.0.1:1080";
public static string ConfigDir { get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ClaudeProxy"); } }
public static string ConfigPath { get { return Path.Combine(ConfigDir, "config.ini"); } }
public static string BinDir { get { return Path.Combine(ConfigDir, "bin"); } }
public static string XrayExe { get { return Path.Combine(BinDir, "xray.exe"); } }
public static string XrayCfgPath { get { return Path.Combine(ConfigDir, "xray-config.json"); } }
// ---- admin ----------------------------------------------------------------
public static bool IsAdmin()
{
using (var id = WindowsIdentity.GetCurrent())
return new WindowsPrincipal(id).IsInRole(WindowsBuiltInRole.Administrator);
}
// Relaunch self elevated with a single token argument; wait for it. Returns false if cancelled.
public static bool ElevateSelf(string arg)
{
try
{
var psi = new ProcessStartInfo
{
FileName = Process.GetCurrentProcess().MainModule.FileName,
Arguments = arg,
Verb = "runas",
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
};
var p = Process.Start(psi);
p.WaitForExit();
return true;
}
catch { return false; } // UAC cancelled / error
}
// ---- config ---------------------------------------------------------------
public static Config LoadConfig()
{
var c = new Config();
try
{
if (File.Exists(ConfigPath))
foreach (var line in File.ReadAllLines(ConfigPath))
{
int i = line.IndexOf('=');
if (i <= 0) continue;
string k = line.Substring(0, i).Trim(), v = line.Substring(i + 1);
switch (k)
{
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;
}
}
}
catch { }
if (c.LocalPort <= 0) c.LocalPort = 10808;
if (string.IsNullOrEmpty(c.Mode)) c.Mode = "proxy";
if (string.IsNullOrEmpty(c.Proxy)) c.Proxy = DefaultProxy;
return c;
}
public static void SaveConfig(Config c)
{
Directory.CreateDirectory(ConfigDir);
var sb = new StringBuilder();
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());
}
// ---- proxy parsing --------------------------------------------------------
public static bool TryParseProxy(string uri, out string host, out int port)
{
host = null; port = 0;
if (string.IsNullOrWhiteSpace(uri)) return false;
var m = System.Text.RegularExpressions.Regex.Match(uri.Trim(),
@"^(socks5|socks5h|socks4|http|https)://([^:/]+):(\d+)/?$");
if (!m.Success) return false;
host = m.Groups[2].Value; port = int.Parse(m.Groups[3].Value);
return true;
}
public static string ProxyArg(string uri) { return uri.Trim().TrimEnd('/'); }
public static bool TcpReachable(string host, int port, int timeoutMs)
{
try
{
using (var c = new TcpClient())
{
var ar = c.BeginConnect(host, port, null, null);
if (ar.AsyncWaitHandle.WaitOne(timeoutMs) && c.Connected) { c.EndConnect(ar); return true; }
return false;
}
}
catch { return false; }
}
// ---- Claude app discovery -------------------------------------------------
public static ClaudeApp GetClaudeApp()
{
string exe = null;
// 1. from a running instance (works de-elevated)
foreach (var p in Process.GetProcessesByName("claude"))
{
try
{
string fn = p.MainModule.FileName;
if (fn.IndexOf("WindowsApps", StringComparison.OrdinalIgnoreCase) >= 0 &&
fn.IndexOf(PackageFamily.Split('_')[1], StringComparison.OrdinalIgnoreCase) >= 0)
{ exe = fn; break; }
}
catch { }
}
// 2. enumerate WindowsApps (works when elevated)
if (exe == null)
{
try
{
string wa = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsApps");
foreach (var dir in Directory.GetDirectories(wa, "Claude_*__" + PackageFamily.Split('_')[1]))
{
string cand = Path.Combine(dir, "app", "Claude.exe");
if (File.Exists(cand)) { exe = cand; break; }
}
}
catch { }
}
if (exe == null) throw new Exception("Не найден Claude (десктопный MSIX-пакет).");
string install = Directory.GetParent(Directory.GetParent(exe).FullName).FullName;
return new ClaudeApp { Exe = exe, InstallLocation = install, Aumid = Aumid };
}
public static bool ClaudeRunning()
{
foreach (var p in Process.GetProcessesByName("claude"))
try { var fn = p.MainModule.FileName; if (fn.IndexOf("WindowsApps", StringComparison.OrdinalIgnoreCase) >= 0) return true; }
catch { }
return false;
}
public static void StopClaude()
{
foreach (var p in Process.GetProcessesByName("claude"))
{
try
{
string fn = p.MainModule.FileName;
if (fn.IndexOf("WindowsApps", StringComparison.OrdinalIgnoreCase) >= 0) { p.Kill(); }
}
catch { }
}
Thread.Sleep(900);
}
public static uint LaunchClaude(string argLine)
{
var mgr = (IApplicationActivationManager)(new ApplicationActivationManager());
uint pid;
int hr = mgr.ActivateApplication(Aumid, argLine ?? "", 0, out pid);
if (hr < 0) throw new Exception("Не удалось запустить Claude (HRESULT 0x" + hr.ToString("X8") + ").");
return pid;
}
// ---- firewall (Windows Firewall COM API via dynamic late binding) ---------
// Returns all IPv4 addresses EXCEPT the given ones, as "a-b" range strings.
public static List<string> Ipv4Complement(IEnumerable<string> ips)
{
var vals = ips.Select(IpToUInt).Distinct().OrderBy(x => x).ToList();
var res = new List<string>();
ulong cursor = 0, max = 4294967295UL;
foreach (var v in vals)
{
if (v > cursor) res.Add(UIntToIp(cursor) + "-" + UIntToIp(v - 1));
cursor = v + 1;
}
if (cursor <= max) res.Add(UIntToIp(cursor) + "-" + UIntToIp(max));
return res;
}
static ulong IpToUInt(string ip)
{
var b = IPAddress.Parse(ip).GetAddressBytes();
return ((ulong)b[0] << 24) | ((ulong)b[1] << 16) | ((ulong)b[2] << 8) | b[3];
}
static string UIntToIp(ulong v)
{
return string.Format("{0}.{1}.{2}.{3}", (v >> 24) & 255, (v >> 16) & 255, (v >> 8) & 255, v & 255);
}
const string V6All = "::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";
static dynamic FwPolicy()
{
return Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FwPolicy2"));
}
static void AddBlockRule(dynamic policy, string name, string exe, string remote)
{
dynamic r = Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FWRule"));
r.Name = name;
r.Grouping = FwGroup;
r.Direction = 2; // NET_FW_RULE_DIR_OUT
r.Action = 0; // NET_FW_ACTION_BLOCK
r.ApplicationName = exe;
if (!string.IsNullOrEmpty(remote)) r.RemoteAddresses = remote;
r.Profiles = 0x7FFFFFFF; // all profiles
r.Enabled = true;
policy.Rules.Add(r);
}
public static void RemoveFirewall()
{
try
{
dynamic policy = FwPolicy();
var names = new List<string>();
foreach (dynamic r in policy.Rules)
{
try { if ((string)r.Grouping == FwGroup) names.Add((string)r.Name); } catch { }
}
foreach (var n in names) { try { policy.Rules.Remove(n); } catch { } }
}
catch { }
}
public static void ApplyFirewallForConfig()
{
var cfg = LoadConfig();
var app = GetClaudeApp();
RemoveFirewall();
dynamic policy = FwPolicy();
if (cfg.Mode == "vless" || cfg.Mode == "awg")
{
// tunnel: block ALL external (loopback is exempt from firewall filtering)
AddBlockRule(policy, "ClaudeProxyLock: tunnel", app.Exe, null);
}
else
{
string host; int port;
if (!TryParseProxy(cfg.Proxy, out host, out port)) throw new Exception("Некорректный прокси.");
var addrs = Dns.GetHostAddresses(host);
var v4 = addrs.Where(a => a.AddressFamily == AddressFamily.InterNetwork).Select(a => a.ToString()).Distinct().ToList();
var v6 = addrs.Where(a => a.AddressFamily == AddressFamily.InterNetworkV6).Select(a => a.ToString()).Distinct().ToList();
if (v4.Count > 0)
AddBlockRule(policy, "ClaudeProxyLock: IPv4", app.Exe, string.Join(",", Ipv4Complement(v4)));
if (v6.Count == 0)
AddBlockRule(policy, "ClaudeProxyLock: IPv6", app.Exe, V6All);
else
{
// allow proxy v6, block the rest
dynamic allow = Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FWRule"));
allow.Name = "ClaudeProxyLock: IPv6 allow"; allow.Grouping = FwGroup;
allow.Direction = 2; allow.Action = 1; allow.ApplicationName = app.Exe;
allow.RemoteAddresses = string.Join(",", v6); allow.Profiles = 0x7FFFFFFF; allow.Enabled = true;
policy.Rules.Add(allow);
AddBlockRule(policy, "ClaudeProxyLock: IPv6 block", app.Exe, V6All);
}
}
}
// absent | stale | ok (path mismatch -> stale)
public static string FirewallState(string exe)
{
try
{
dynamic policy = FwPolicy();
int count = 0; bool stale = false;
foreach (dynamic r in policy.Rules)
{
try
{
if ((string)r.Grouping != FwGroup) continue;
count++;
string prog = null; try { prog = (string)r.ApplicationName; } catch { }
if (!string.IsNullOrEmpty(prog) && exe != null &&
!prog.Equals(exe, StringComparison.OrdinalIgnoreCase)) stale = true;
}
catch { }
}
if (count == 0) return "absent";
return stale ? "stale" : "ok";
}
catch { return "absent"; }
}
// ---- Xray -----------------------------------------------------------------
public static bool XrayPresent() { return File.Exists(XrayExe); }
public static void EnsureXray()
{
if (XrayPresent()) return;
Directory.CreateDirectory(BinDir);
// embedded resource "ClaudeProxy.xray.zip"
var asm = Assembly.GetExecutingAssembly();
string resName = asm.GetManifestResourceNames().FirstOrDefault(n => n.EndsWith("xray.zip", StringComparison.OrdinalIgnoreCase));
if (resName != null)
{
string tmp = Path.Combine(Path.GetTempPath(), "claudeproxy-xray.zip");
using (var rs = asm.GetManifestResourceStream(resName))
using (var fs = File.Create(tmp))
rs.CopyTo(fs);
using (var z = ZipFile.OpenRead(tmp))
foreach (var e in z.Entries)
{
if (string.IsNullOrEmpty(e.Name)) continue;
string dest = Path.Combine(BinDir, e.Name);
e.ExtractToFile(dest, true);
}
try { File.Delete(tmp); } catch { }
}
if (!XrayPresent()) throw new Exception("Не удалось распаковать встроенный Xray.");
}
public static void StopXray()
{
foreach (var p in Process.GetProcessesByName("xray"))
{
try
{
if (p.MainModule.FileName.StartsWith(BinDir, StringComparison.OrdinalIgnoreCase)) p.Kill();
}
catch { }
}
Thread.Sleep(300);
}
public static bool LocalSocksUp(int port) { return TcpReachable("127.0.0.1", port, 500); }
public static void StartXray(string vlessLink, int localPort)
{
EnsureXray();
StopXray();
File.WriteAllText(XrayCfgPath, Vless.BuildXrayConfig(vlessLink, localPort));
var psi = new ProcessStartInfo
{
FileName = XrayExe,
Arguments = "run -config \"" + XrayCfgPath + "\"",
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
Process.Start(psi);
for (int i = 0; i < 30; i++) { if (LocalSocksUp(localPort)) return; Thread.Sleep(200); }
StopXray();
throw new Exception("Xray не поднял локальный SOCKS на 127.0.0.1:" + localPort + ". Проверь ссылку и сервер.");
}
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; }
}
}
}