39b58e1a77
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>
299 lines
14 KiB
C#
299 lines
14 KiB
C#
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());
|
||
}
|
||
}
|
||
}
|