claude-proxy: native C# GUI to force Claude desktop traffic through a proxy

Windows tray "switch" that routes all Claude MSIX-app traffic through a
SOCKS5/HTTP proxy or a VLESS+Reality tunnel (embedded Xray-core), with a
fail-closed Windows Firewall lock. Runs de-elevated so it launches the
user's normal Claude instance; elevates only for firewall changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 13:46:23 +03:00
commit ab2d3ed8cd
16 changed files with 2707 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace ClaudeProxy
{
class Config
{
public string Mode = "proxy"; // proxy | vless
public string Proxy = Core.DefaultProxy;
public string Vless = "";
public int LocalPort = 10808;
public bool FailClosed = true;
}
// ---- VLESS share-link (from 3x-ui) -> Xray config -----------------------------
static class Vless
{
public static Dictionary<string, string> Parse(string link, out string host, out int port, out string uuid, out string remark)
{
link = (link ?? "").Trim();
if (!link.StartsWith("vless://", StringComparison.OrdinalIgnoreCase))
throw new Exception("Ожидается vless://... ссылка (share-link из 3x-ui).");
var uri = new Uri(link);
uuid = uri.UserInfo;
if (string.IsNullOrEmpty(uuid)) throw new Exception("В ссылке нет UUID.");
host = uri.Host; port = uri.Port;
remark = Uri.UnescapeDataString(uri.Fragment.TrimStart('#'));
var q = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in uri.Query.TrimStart('?').Split('&'))
{
if (pair.Length == 0) continue;
int i = pair.IndexOf('=');
if (i < 0) q[pair] = "";
else q[pair.Substring(0, i)] = Uri.UnescapeDataString(pair.Substring(i + 1));
}
return q;
}
static string J(string s) { return "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; }
static string GetQ(Dictionary<string, string> q, string k, string d) { return q.ContainsKey(k) && q[k].Length > 0 ? q[k] : d; }
public static string BuildXrayConfig(string link, int localPort)
{
string host, uuid, remark; int port;
var q = Parse(link, out host, out port, out uuid, out remark);
string type = GetQ(q, "type", "tcp");
string security = GetQ(q, "security", "none");
string flow = GetQ(q, "flow", "");
var stream = new StringBuilder();
stream.Append("\"network\":" + J(type));
stream.Append(",\"security\":" + J(security));
if (security == "reality")
{
if (!q.ContainsKey("pbk")) throw new Exception("Reality: в ссылке нет publicKey (pbk).");
stream.Append(",\"realitySettings\":{");
stream.Append("\"serverName\":" + J(GetQ(q, "sni", "")));
stream.Append(",\"fingerprint\":" + J(GetQ(q, "fp", "chrome")));
stream.Append(",\"publicKey\":" + J(GetQ(q, "pbk", "")));
stream.Append(",\"shortId\":" + J(GetQ(q, "sid", "")));
stream.Append(",\"spiderX\":" + J(GetQ(q, "spx", "")));
stream.Append("}");
}
else if (security == "tls")
{
stream.Append(",\"tlsSettings\":{\"serverName\":" + J(GetQ(q, "sni", "")) + ",\"fingerprint\":" + J(GetQ(q, "fp", "chrome")) + "}");
}
if (type == "grpc")
stream.Append(",\"grpcSettings\":{\"serviceName\":" + J(GetQ(q, "serviceName", "")) + "}");
else if (type == "ws")
stream.Append(",\"wsSettings\":{\"path\":" + J(GetQ(q, "path", "/")) + ",\"headers\":{\"Host\":" + J(GetQ(q, "host", "")) + "}}");
var user = new StringBuilder();
user.Append("{\"id\":" + J(uuid) + ",\"encryption\":\"none\"");
if (flow.Length > 0) user.Append(",\"flow\":" + J(flow));
user.Append("}");
var sb = new StringBuilder();
sb.Append("{");
sb.Append("\"log\":{\"loglevel\":\"warning\"},");
sb.Append("\"inbounds\":[{\"tag\":\"socks-in\",\"listen\":\"127.0.0.1\",\"port\":" + localPort +
",\"protocol\":\"socks\",\"settings\":{\"udp\":true,\"auth\":\"noauth\"}}],");
sb.Append("\"outbounds\":[{\"tag\":\"proxy\",\"protocol\":\"vless\",\"settings\":{\"vnext\":[{\"address\":" +
J(host) + ",\"port\":" + port + ",\"users\":[" + user + "]}]},\"streamSettings\":{" + stream + "}}]");
sb.Append("}");
return sb.ToString();
}
// reachability of the VLESS server host:port
public static bool ServerReachable(string link, out string endpoint)
{
endpoint = "";
try
{
string host, uuid, remark; int port;
Parse(link, out host, out port, out uuid, out remark);
endpoint = host + ":" + port;
return Core.TcpReachable(host, port, 1500);
}
catch { endpoint = "некорректная ссылка"; return false; }
}
}
class VerifyResult
{
public string Mode, Endpoint, Tunnel = "";
public int Procs;
public List<string> ViaProxy = new List<string>();
public List<string> Local = new List<string>();
public List<string> Leaks = new List<string>();
public List<string> Blocked = new List<string>();
public List<string> Lines = new List<string>();
public string Verdict; public bool Ok;
}
static class Verify
{
class Conn { public string Remote; public int RemotePort; public string State; public int Pid; }
// Parse `netstat -ano` for TCP connections owned by the given PIDs.
static List<Conn> TcpConns(HashSet<int> pids)
{
var res = new List<Conn>();
try
{
var psi = new ProcessStartInfo("netstat", "-ano")
{ UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.ASCII };
var p = Process.Start(psi);
string outp = p.StandardOutput.ReadToEnd();
p.WaitForExit();
foreach (var raw in outp.Split('\n'))
{
var line = raw.Trim();
if (!line.StartsWith("TCP", StringComparison.OrdinalIgnoreCase)) continue;
var t = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (t.Length < 5) continue;
int pid; if (!int.TryParse(t[4], out pid)) continue;
if (!pids.Contains(pid)) continue;
string state = t[3];
if (state != "ESTABLISHED" && state != "SYN_SENT") continue;
string remote = t[2];
int rp = 0; string rip = remote;
int c = remote.LastIndexOf(':');
if (c > 0) { rip = remote.Substring(0, c); int.TryParse(remote.Substring(c + 1), out rp); }
rip = rip.Trim('[', ']');
res.Add(new Conn { Remote = rip, RemotePort = rp, State = state == "SYN_SENT" ? "SynSent" : "Established", Pid = pid });
}
}
catch { }
return res;
}
static HashSet<int> ClaudePids()
{
var set = new HashSet<int>();
foreach (var p in Process.GetProcessesByName("claude"))
try { if (p.MainModule.FileName.IndexOf("WindowsApps", StringComparison.OrdinalIgnoreCase) >= 0) set.Add(p.Id); }
catch { }
return set;
}
static HashSet<int> XrayPids()
{
var set = new HashSet<int>();
foreach (var p in Process.GetProcessesByName("xray"))
try { if (p.MainModule.FileName.StartsWith(Core.BinDir, StringComparison.OrdinalIgnoreCase)) set.Add(p.Id); }
catch { }
return set;
}
public static VerifyResult Run(Config cfg)
{
var r = new VerifyResult { Mode = cfg.Mode };
HashSet<string> proxySet;
bool loopIsProxy;
if (cfg.Mode == "vless")
{
proxySet = new HashSet<string> { "127.0.0.1", "::1" };
r.Endpoint = "127.0.0.1:" + cfg.LocalPort + " (VLESS+Reality)";
loopIsProxy = true;
}
else
{
string host; int port; Core.TryParseProxy(cfg.Proxy, out host, out port);
var ips = new HashSet<string>();
try { foreach (var a in System.Net.Dns.GetHostAddresses(host)) ips.Add(a.ToString()); } catch { }
proxySet = ips; r.Endpoint = host + ":" + port; loopIsProxy = false;
}
var pids = ClaudePids();
r.Procs = pids.Count;
foreach (var c in TcpConns(pids))
{
string entry = c.Remote + ":" + c.RemotePort + " [" + c.State + "]";
bool isLoop = c.Remote == "127.0.0.1" || c.Remote == "::1";
bool otherLocal = c.Remote == "0.0.0.0" || c.Remote == "::" || c.Remote.StartsWith("fe80");
if (isLoop) { if (loopIsProxy) r.ViaProxy.Add(entry); else r.Local.Add(entry); continue; }
if (otherLocal) { r.Local.Add(entry); continue; }
if (!loopIsProxy && proxySet.Contains(c.Remote)) { r.ViaProxy.Add(entry); continue; }
if (c.State == "SynSent") { r.Blocked.Add(entry); continue; }
r.Leaks.Add(entry);
}
if (cfg.Mode == "vless")
{
var xp = XrayPids();
if (xp.Count == 0) r.Tunnel = "xray НЕ запущен";
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 запущен, внешнего коннекта пока нет";
}
}
bool tunnelDown = cfg.Mode == "vless" && r.Tunnel == "xray НЕ запущен";
string modeLabel = cfg.Mode == "vless" ? "VLESS+Reality" : "прямой прокси";
r.Lines.Add("Проверка трафика Claude (" + modeLabel + ")");
r.Lines.Add(" вход : " + r.Endpoint);
if (cfg.Mode == "vless") r.Lines.Add(" тоннель : " + r.Tunnel);
r.Lines.Add(" процессов Claude: " + r.Procs);
r.Lines.Add(" через прокси : " + r.ViaProxy.Count);
r.Lines.Add(" локальные : " + r.Local.Count);
r.Lines.Add(" заблокировано : " + r.Blocked.Count + " (firewall отбил обход)");
r.Lines.Add(" УТЕЧКИ : " + r.Leaks.Count);
foreach (var l in r.Leaks) r.Lines.Add(" ! прямой коннект: " + l);
foreach (var b in r.Blocked) r.Lines.Add(" ~ отбито: " + b);
if (r.Procs == 0) r.Verdict = "НЕТ ДАННЫХ: Claude не запущен";
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 r.Verdict = "OK: весь внешний трафик Claude идёт через прокси";
r.Lines.Add(" ВЕРДИКТ : " + r.Verdict);
r.Ok = r.Leaks.Count == 0 && r.ViaProxy.Count > 0 && !tunnelDown;
return r;
}
public static string WriteLog(VerifyResult r, string stamp)
{
try
{
System.IO.Directory.CreateDirectory(Core.ConfigDir);
string path = System.IO.Path.Combine(Core.ConfigDir, "verify-" + stamp + ".log");
System.IO.File.WriteAllLines(path, r.Lines.ToArray());
return path;
}
catch { return ""; }
}
}
static class Program
{
[STAThread]
static void Main(string[] args)
{
// elevated firewall-helper mode (invoked by the GUI via runas). Native args arrive clean.
if (args.Contains("fwapply") || args.Contains("fwremove"))
{
if (Core.IsAdmin())
{
try { if (args.Contains("fwapply")) Core.ApplyFirewallForConfig(); else Core.RemoveFirewall(); }
catch { }
}
return;
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (args.Contains("smoketest"))
{
var f = new MainForm();
f.StartPosition = FormStartPosition.Manual;
f.Location = new System.Drawing.Point(-3000, -3000);
f.Show();
Application.DoEvents();
System.Threading.Thread.Sleep(600);
Application.DoEvents();
using (var bmp = new System.Drawing.Bitmap(f.ClientSize.Width, f.ClientSize.Height))
{
f.DrawToBitmap(bmp, new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height));
bmp.Save(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "claudeproxy-native-preview.png"));
}
f.Close();
return;
}
Application.Run(new MainForm());
}
}
}