Files
claude-proxy/native/Support.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

299 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 | 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;
}
// ---- 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> ProcPids(string name)
{
var set = new HashSet<int>();
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<int> XrayPids() { return ProcPids("xray"); }
static HashSet<int> AwgPids() { return ProcPids("wireproxy-awg"); }
public static VerifyResult Run(Config cfg)
{
var r = new VerifyResult { Mode = cfg.Mode };
HashSet<string> proxySet;
bool loopIsProxy;
if (cfg.Mode == "vless" || cfg.Mode == "awg")
{
proxySet = new HashSet<string> { "127.0.0.1", "::1" };
string lbl = cfg.Mode == "awg" ? "AmneziaWG" : "VLESS+Reality";
r.Endpoint = "127.0.0.1:" + cfg.LocalPort + " (" + lbl + ")";
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" || cfg.Mode == "awg")
{
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(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" || 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" || 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);
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" || 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;
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());
}
}
}