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:
+393
@@ -0,0 +1,393 @@
|
||||
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 "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("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")
|
||||
{
|
||||
// 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; } }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Globalization;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace ClaudeProxy
|
||||
{
|
||||
class BufferedPanel : Panel { public BufferedPanel() { DoubleBuffered = true; } }
|
||||
|
||||
class MainForm : Form
|
||||
{
|
||||
// palette
|
||||
static Color Cream = Color.FromArgb(250, 249, 245), Ink = Color.FromArgb(41, 40, 36),
|
||||
Head = Color.FromArgb(26, 25, 21), Muted = Color.FromArgb(131, 129, 123),
|
||||
Card = Color.FromArgb(255, 255, 255), Border = Color.FromArgb(232, 229, 220),
|
||||
Coral = Color.FromArgb(193, 95, 60), CoralLo = Color.FromArgb(224, 137, 101),
|
||||
CoralHi = Color.FromArgb(168, 79, 48), TrackOff = Color.FromArgb(206, 202, 192),
|
||||
OkC = Color.FromArgb(74, 122, 80), RedC = Color.FromArgb(196, 74, 61),
|
||||
Glyph = Color.FromArgb(248, 246, 239);
|
||||
Font fReg = new Font("Segoe UI", 9.5f), fSemi = new Font("Segoe UI Semibold", 9.5f),
|
||||
fHead = new Font("Segoe UI Semibold", 15f), fSub = new Font("Segoe UI", 9f),
|
||||
fPill = new Font("Segoe UI Semibold", 12f), fMono = new Font("Consolas", 8.5f);
|
||||
|
||||
bool switchOn = false, busy = false;
|
||||
RadioButton rbProxy, rbVless;
|
||||
BufferedPanel mark, tgl, card;
|
||||
Label title, subtitle, lblFieldCap, lblReach, rDot, lblFwCap, lblFw, lblClCap, lblCl, lblXrayCap, lblXray;
|
||||
TextBox txtField, log;
|
||||
CheckBox chkRestart, chkFail;
|
||||
Button btnSave, btnLaunch, btnVerify, btnRefresh, btnXray;
|
||||
Panel sep;
|
||||
Timer timer;
|
||||
|
||||
string Mode { get { return rbVless.Checked ? "vless" : "proxy"; } }
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
Text = "Claude Proxy";
|
||||
AutoScaleMode = AutoScaleMode.None;
|
||||
ClientSize = new Size(452, 512);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
MaximizeBox = false;
|
||||
BackColor = Cream; ForeColor = Ink; Font = fReg;
|
||||
try { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); } catch { }
|
||||
|
||||
mark = new BufferedPanel { Size = new Size(46, 46), Location = new Point(24, 22), BackColor = Cream };
|
||||
mark.Paint += (s, e) => PaintTile(e.Graphics, 46);
|
||||
Controls.Add(mark);
|
||||
|
||||
title = MkLabel("Claude Proxy", fHead, Head, 82, 22); title.AutoSize = true; Controls.Add(title);
|
||||
subtitle = MkLabel("весь трафик Claude через прокси", fSub, Muted, 84, 48);
|
||||
subtitle.AutoSize = false; subtitle.Size = new Size(196, 18); Controls.Add(subtitle);
|
||||
|
||||
rbProxy = MkRadio("Прямой прокси", 292, 18);
|
||||
rbVless = MkRadio("VLESS + Reality", 292, 42);
|
||||
Controls.Add(rbProxy); Controls.Add(rbVless);
|
||||
|
||||
tgl = new BufferedPanel { Size = new Size(172, 58), Location = new Point(24, 88), BackColor = Cream, Cursor = Cursors.Hand };
|
||||
tgl.Paint += (s, e) => PaintToggle(e.Graphics, tgl.Width, tgl.Height);
|
||||
tgl.Click += (s, e) => ToggleSwitch();
|
||||
Controls.Add(tgl);
|
||||
|
||||
var hint = MkLabel("Включено = наружу только через прокси, всё остальное заблокировано", fSub, Muted, 208, 92);
|
||||
hint.AutoSize = false; hint.Size = new Size(224, 52); Controls.Add(hint);
|
||||
|
||||
// status card
|
||||
card = new BufferedPanel { Size = new Size(412, 158), Location = new Point(20, 162), BackColor = Cream };
|
||||
card.Paint += (s, e) => { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
using (var path = Round(card.Width - 1, card.Height - 1, 14))
|
||||
using (var b = new SolidBrush(Card)) using (var pen = new Pen(Border, 1))
|
||||
{ g.FillPath(b, path); g.DrawPath(pen, path); } };
|
||||
Controls.Add(card);
|
||||
|
||||
lblFieldCap = MkLabel("Прокси", fReg, Muted, 18, 18); lblFieldCap.AutoSize = true; card.Controls.Add(lblFieldCap);
|
||||
txtField = new TextBox { Location = new Point(84, 15), Size = new Size(214, 24), BorderStyle = BorderStyle.FixedSingle, BackColor = Cream, ForeColor = Ink };
|
||||
card.Controls.Add(txtField);
|
||||
btnSave = MkButton("Сохранить", 308, 13, 88, 28, false); btnSave.Click += (s, e) => OnSave(); card.Controls.Add(btnSave);
|
||||
|
||||
rDot = MkLabel("●", new Font("Segoe UI", 11f), Muted, 17, 54); rDot.AutoSize = true; card.Controls.Add(rDot);
|
||||
lblReach = MkLabel("", fReg, Ink, 38, 56); lblReach.AutoSize = true; card.Controls.Add(lblReach);
|
||||
|
||||
lblXrayCap = MkLabel("Xray-core", fReg, Muted, 18, 68); lblXrayCap.AutoSize = true; card.Controls.Add(lblXrayCap);
|
||||
lblXray = MkLabel("", fSemi, Ink, 120, 68); lblXray.AutoSize = true; card.Controls.Add(lblXray);
|
||||
btnXray = MkButton("Распаковать", 300, 64, 96, 26, false); btnXray.Click += (s, e) => OnXray(); card.Controls.Add(btnXray);
|
||||
|
||||
sep = new Panel { Location = new Point(18, 86), Size = new Size(378, 1), BackColor = Border }; card.Controls.Add(sep);
|
||||
lblFwCap = MkLabel("Firewall-лок", fReg, Muted, 18, 98); lblFwCap.AutoSize = true; card.Controls.Add(lblFwCap);
|
||||
lblFw = MkLabel("", fSemi, Ink, 120, 98); lblFw.AutoSize = true; card.Controls.Add(lblFw);
|
||||
lblClCap = MkLabel("Claude", fReg, Muted, 18, 122); lblClCap.AutoSize = true; card.Controls.Add(lblClCap);
|
||||
lblCl = MkLabel("", fSemi, Ink, 120, 122); lblCl.AutoSize = true; card.Controls.Add(lblCl);
|
||||
|
||||
chkRestart = new CheckBox { Text = "Перезапускать Claude при переключении", ForeColor = Ink, AutoSize = true, Checked = true, Location = new Point(22, 324), Font = fReg };
|
||||
chkFail = new CheckBox { Text = "Fail-closed: блокировать любой трафик мимо прокси (firewall)", ForeColor = Ink, AutoSize = true, Checked = true, Location = new Point(22, 348), Font = fReg };
|
||||
Controls.Add(chkRestart); Controls.Add(chkFail);
|
||||
|
||||
btnLaunch = MkButton("Запустить Claude", 20, 384, 170, 36, true); btnLaunch.Click += (s, e) => LaunchProxied(); Controls.Add(btnLaunch);
|
||||
btnVerify = MkButton("Проверить", 198, 386, 142, 32, false); btnVerify.Click += (s, e) => OnVerify(); Controls.Add(btnVerify);
|
||||
btnRefresh = MkButton("Обновить", 348, 386, 84, 32, false); btnRefresh.Click += (s, e) => { UpdateUi(true); Log("статус обновлён."); }; Controls.Add(btnRefresh);
|
||||
|
||||
log = new TextBox { Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical, Location = new Point(20, 430), Size = new Size(412, 66), BorderStyle = BorderStyle.FixedSingle, BackColor = Card, ForeColor = Muted, Font = fMono };
|
||||
Controls.Add(log);
|
||||
|
||||
rbVless.CheckedChanged += (s, e) => { ApplyModeLayout(); var c = Core.LoadConfig(); c.Mode = Mode; Core.SaveConfig(c); UpdateUi(true); };
|
||||
|
||||
timer = new Timer { Interval = 4000 };
|
||||
timer.Tick += (s, e) => { if (!busy) UpdateUi(false); };
|
||||
timer.Start();
|
||||
|
||||
Load += (s, e) => {
|
||||
var cfg = Core.LoadConfig();
|
||||
chkFail.Checked = cfg.FailClosed;
|
||||
if (cfg.Mode == "vless") rbVless.Checked = true; else rbProxy.Checked = true;
|
||||
ApplyModeLayout();
|
||||
Log("готово. рубильник управляет проксированием Claude.");
|
||||
UpdateUi(true);
|
||||
};
|
||||
}
|
||||
|
||||
// ---- factories ----
|
||||
Label MkLabel(string t, Font f, Color c, int x, int y)
|
||||
{ return new Label { Text = t, Font = f, ForeColor = c, Location = new Point(x, y), BackColor = Color.Transparent }; }
|
||||
RadioButton MkRadio(string t, int x, int y)
|
||||
{ return new RadioButton { Text = t, ForeColor = Ink, Font = fReg, AutoSize = false, TextAlign = ContentAlignment.MiddleLeft, BackColor = Cream, Size = new Size(150, 22), Location = new Point(x, y) }; }
|
||||
Button MkButton(string t, int x, int y, int w, int h, bool primary)
|
||||
{
|
||||
var b = new Button { Text = t, Location = new Point(x, y), Size = new Size(w, h), FlatStyle = FlatStyle.Flat, Cursor = Cursors.Hand };
|
||||
b.FlatAppearance.BorderSize = primary ? 0 : 1;
|
||||
if (primary) { b.BackColor = Coral; b.ForeColor = Color.White; b.Font = fSemi; b.FlatAppearance.MouseOverBackColor = CoralHi; }
|
||||
else { b.BackColor = Card; b.ForeColor = Ink; b.Font = fReg; b.FlatAppearance.BorderColor = Border; b.FlatAppearance.MouseOverBackColor = Cream; }
|
||||
b.Region = new Region(Round(w, h, 9));
|
||||
b.SizeChanged += (s, e) => { try { b.Region = new Region(Round(b.Width, b.Height, 9)); } catch { } };
|
||||
return b;
|
||||
}
|
||||
|
||||
static GraphicsPath Round(int w, int h, int r)
|
||||
{
|
||||
int d = r * 2; var p = new GraphicsPath();
|
||||
p.AddArc(0, 0, d, d, 180, 90); p.AddArc(w - d, 0, d, d, 270, 90);
|
||||
p.AddArc(w - d, h - d, d, d, 0, 90); p.AddArc(0, h - d, d, d, 90, 90);
|
||||
p.CloseFigure(); return p;
|
||||
}
|
||||
|
||||
// ---- painting ----
|
||||
void PaintTile(Graphics g, int S)
|
||||
{
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
using (var path = Round(S - 1, S - 1, 12))
|
||||
using (var br = new LinearGradientBrush(new Rectangle(0, 0, S, S), CoralLo, CoralHi, 90f))
|
||||
g.FillPath(br, path);
|
||||
DrawToggleGlyph(g, S);
|
||||
}
|
||||
void DrawToggleGlyph(Graphics g, float S)
|
||||
{
|
||||
float tw = S * 0.58f, th = S * 0.34f, tx = (S - tw) / 2f, ty = (S - th) / 2f;
|
||||
using (var tp = new GraphicsPath())
|
||||
{
|
||||
tp.AddArc(tx, ty, th, th, 90, 180); tp.AddArc(tx + tw - th, ty, th, th, 270, 180); tp.CloseFigure();
|
||||
using (var tb = new SolidBrush(Glyph)) g.FillPath(tb, tp);
|
||||
}
|
||||
float kd = th - S * 0.10f, kx = tx + tw - kd - S * 0.05f, ky = ty + (th - kd) / 2f;
|
||||
using (var kb = new SolidBrush(CoralHi)) g.FillEllipse(kb, kx, ky, kd, kd);
|
||||
}
|
||||
void PaintToggle(Graphics g, int w, int h)
|
||||
{
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
using (var path = Round(w, h, h / 2))
|
||||
{
|
||||
if (switchOn) using (var br = new LinearGradientBrush(new Rectangle(0, 0, w, h), CoralLo, Coral, 0f)) g.FillPath(br, path);
|
||||
else using (var br = new SolidBrush(TrackOff)) g.FillPath(br, path);
|
||||
}
|
||||
int knobD = h - 12, knobX = switchOn ? w - knobD - 6 : 6;
|
||||
using (var sh = new SolidBrush(Color.FromArgb(40, 0, 0, 0))) g.FillEllipse(sh, knobX + 1, 7, knobD, knobD);
|
||||
using (var kb = new SolidBrush(Color.White)) g.FillEllipse(kb, knobX, 6, knobD, knobD);
|
||||
string txt = switchOn ? "ВКЛ" : "ВЫКЛ";
|
||||
using (var tb = new SolidBrush(switchOn ? Color.White : Color.FromArgb(106, 104, 98)))
|
||||
g.DrawString(txt, fPill, tb, switchOn ? 22 : 66, 16);
|
||||
}
|
||||
|
||||
// ---- mode layout ----
|
||||
void ApplyModeLayout()
|
||||
{
|
||||
var cfg = Core.LoadConfig();
|
||||
if (Mode == "vless")
|
||||
{
|
||||
lblFieldCap.Text = "VLESS"; subtitle.Text = "через VLESS + Reality тоннель";
|
||||
txtField.UseSystemPasswordChar = true; txtField.Text = cfg.Vless ?? "";
|
||||
lblXrayCap.Visible = lblXray.Visible = btnXray.Visible = true;
|
||||
sep.Top = 100; lblFwCap.Top = lblFw.Top = 112; lblClCap.Top = lblCl.Top = 134;
|
||||
UpdateXrayStatus();
|
||||
}
|
||||
else
|
||||
{
|
||||
lblFieldCap.Text = "Прокси"; subtitle.Text = "весь трафик Claude через прокси";
|
||||
txtField.UseSystemPasswordChar = false; txtField.Text = cfg.Proxy ?? "";
|
||||
lblXrayCap.Visible = lblXray.Visible = btnXray.Visible = false;
|
||||
sep.Top = 74; lblFwCap.Top = lblFw.Top = 86; lblClCap.Top = lblCl.Top = 108;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateXrayStatus()
|
||||
{
|
||||
if (Core.XrayPresent()) { lblXray.Text = "готов (" + Core.XrayVersion + ")"; lblXray.ForeColor = OkC; btnXray.Text = "Переустановить"; }
|
||||
else { lblXray.Text = "встроен (" + Core.XrayVersion + ")"; lblXray.ForeColor = OkC; btnXray.Text = "Распаковать"; }
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
void Log(string m) { log.AppendText(DateTime.Now.ToString("HH:mm:ss") + " " + m + "\r\n"); }
|
||||
void SetBusy(bool b)
|
||||
{
|
||||
busy = b;
|
||||
btnSave.Enabled = btnRefresh.Enabled = btnVerify.Enabled = btnLaunch.Enabled = btnXray.Enabled = !b;
|
||||
UpdateModeLock();
|
||||
Cursor = b ? Cursors.WaitCursor : Cursors.Default;
|
||||
}
|
||||
|
||||
// Mode can't be switched while the lock is active (ON) or during an operation.
|
||||
void UpdateModeLock() { rbProxy.Enabled = rbVless.Enabled = !switchOn && !busy; }
|
||||
|
||||
Config CaptureConfig()
|
||||
{
|
||||
var c = Core.LoadConfig();
|
||||
c.Mode = Mode; c.FailClosed = chkFail.Checked;
|
||||
if (Mode == "vless") c.Vless = txtField.Text; else c.Proxy = txtField.Text;
|
||||
Core.SaveConfig(c); return c;
|
||||
}
|
||||
|
||||
void UpdateUi(bool checkReach)
|
||||
{
|
||||
try
|
||||
{
|
||||
ClaudeApp app = null;
|
||||
try { app = Core.GetClaudeApp(); } catch { }
|
||||
string fw = app != null ? Core.FirewallState(app.Exe) : Core.FirewallState(null);
|
||||
bool newOn = fw == "ok";
|
||||
if (newOn != switchOn) { switchOn = newOn; tgl.Invalidate(); }
|
||||
UpdateModeLock();
|
||||
|
||||
lblFw.Text = fw == "ok" ? "установлен" : (fw == "stale" ? "устарел (путь изменился)" : "нет");
|
||||
lblFw.ForeColor = fw == "ok" ? Coral : (fw == "stale" ? RedC : Muted);
|
||||
|
||||
bool running = Core.ClaudeRunning();
|
||||
if (!running) { lblCl.Text = "не запущен"; lblCl.ForeColor = Muted; }
|
||||
else { lblCl.Text = "запущен"; lblCl.ForeColor = Ink; }
|
||||
|
||||
if (checkReach)
|
||||
{
|
||||
bool ok; string ep;
|
||||
if (Mode == "vless") ok = Vless.ServerReachable(txtField.Text, out ep);
|
||||
else { string h; int p; if (Core.TryParseProxy(txtField.Text, out h, out p)) { ep = h + ":" + p; ok = Core.TcpReachable(h, p, 1500); } else { ep = "некорректный адрес"; ok = false; } }
|
||||
rDot.ForeColor = ok ? OkC : RedC;
|
||||
lblReach.Text = (ok ? "сервер доступен " : "не отвечает ") + ep;
|
||||
}
|
||||
if (Mode == "vless") UpdateXrayStatus();
|
||||
}
|
||||
catch (Exception ex) { Log("ошибка обновления: " + ex.Message); }
|
||||
}
|
||||
|
||||
// ---- orchestration ----
|
||||
bool Preflight()
|
||||
{
|
||||
if (Mode == "vless")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(txtField.Text)) { MessageBox.Show("Вставь VLESS-ссылку из 3x-ui.", "Нужна ссылка", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; }
|
||||
try { string h, u, r; int p; Vless.Parse(txtField.Text, out h, out p, out u, out r); }
|
||||
catch (Exception ex) { MessageBox.Show(ex.Message, "Некорректная ссылка", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; }
|
||||
if (!Core.XrayPresent())
|
||||
{
|
||||
SetBusy(true);
|
||||
try { Core.EnsureXray(); UpdateXrayStatus(); }
|
||||
catch (Exception ex) { Log("ОШИБКА Xray: " + ex.Message); MessageBox.Show(ex.Message, "Ошибка Xray", MessageBoxButtons.OK, MessageBoxIcon.Error); SetBusy(false); return false; }
|
||||
SetBusy(false);
|
||||
}
|
||||
string ep; if (!Vless.ServerReachable(txtField.Text, out ep))
|
||||
if (MessageBox.Show("Сервер " + ep + " не отвечает. Всё равно продолжить?", "Сервер недоступен", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
string h; int p;
|
||||
if (!Core.TryParseProxy(txtField.Text, out h, out p)) { MessageBox.Show("Некорректный адрес прокси.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; }
|
||||
if (!Core.TcpReachable(h, p, 1500))
|
||||
if (MessageBox.Show("Прокси " + h + ":" + p + " не отвечает. Всё равно включить?", "Прокси недоступен", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return false;
|
||||
}
|
||||
CaptureConfig();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DoEnable(bool restart)
|
||||
{
|
||||
var cfg = Core.LoadConfig();
|
||||
string proxyArg;
|
||||
if (cfg.Mode == "vless") { Core.EnsureXray(); Core.StartXray(cfg.Vless, cfg.LocalPort); proxyArg = "socks5://127.0.0.1:" + cfg.LocalPort; }
|
||||
else proxyArg = Core.ProxyArg(cfg.Proxy);
|
||||
|
||||
if (cfg.FailClosed)
|
||||
if (!Core.ElevateSelf("fwapply")) { if (cfg.Mode == "vless") Core.StopXray(); throw new Exception("Установка firewall отменена в UAC."); }
|
||||
|
||||
if (restart) { Core.StopClaude(); string a = "--proxy-server=" + proxyArg; Core.LaunchClaude(a); }
|
||||
}
|
||||
|
||||
void DoDisable(bool restart)
|
||||
{
|
||||
string exe = null; try { exe = Core.GetClaudeApp().Exe; } catch { }
|
||||
if (Core.FirewallState(exe) != "absent")
|
||||
if (!Core.ElevateSelf("fwremove")) throw new Exception("Снятие firewall отменено в UAC (лок остался).");
|
||||
Core.StopXray();
|
||||
if (restart) { Core.StopClaude(); Core.LaunchClaude(""); }
|
||||
}
|
||||
|
||||
void ToggleSwitch()
|
||||
{
|
||||
if (busy) return;
|
||||
bool restart = chkRestart.Checked;
|
||||
try
|
||||
{
|
||||
if (!switchOn)
|
||||
{
|
||||
if (!Preflight()) return;
|
||||
bool running = Core.ClaudeRunning();
|
||||
bool doRestart = false;
|
||||
if (running) { if (restart) { if (MessageBox.Show("Claude запущен. Перезапустить его через прокси? (текущее окно закроется)", "Перезапуск Claude", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK) return; doRestart = true; } }
|
||||
else doRestart = true;
|
||||
SetBusy(true); Log("включаю..."); DoEnable(doRestart);
|
||||
Log("ВКЛ. " + (!running ? "Claude запущен через прокси." : (doRestart ? "Claude перезапущен." : "лок установлен; перезапусти Claude, чтобы применить.")));
|
||||
}
|
||||
else
|
||||
{
|
||||
bool restartOff = restart;
|
||||
if (restartOff && MessageBox.Show("Выключение перезапустит Claude напрямую (текущее окно закроется). Продолжить?", "Перезапуск Claude", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK) restartOff = false;
|
||||
SetBusy(true); Log("выключаю прокси..."); DoDisable(restartOff); Log("прокси ВЫКЛ. firewall-лок снят.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Log("ОШИБКА: " + ex.Message); MessageBox.Show(ex.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); }
|
||||
finally { SetBusy(false); UpdateUi(true); }
|
||||
}
|
||||
|
||||
void LaunchProxied()
|
||||
{
|
||||
if (busy) return;
|
||||
try
|
||||
{
|
||||
if (!Preflight()) return;
|
||||
if (Core.ClaudeRunning() && MessageBox.Show("Claude уже запущен. Перезапустить его через прокси? (текущее окно закроется)", "Перезапуск Claude", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK) return;
|
||||
SetBusy(true); Log("запускаю Claude через прокси..."); DoEnable(true); Log("Claude запущен через прокси.");
|
||||
}
|
||||
catch (Exception ex) { Log("ОШИБКА: " + ex.Message); MessageBox.Show(ex.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); }
|
||||
finally { SetBusy(false); UpdateUi(true); }
|
||||
}
|
||||
|
||||
void OnVerify()
|
||||
{
|
||||
try
|
||||
{
|
||||
var r = Verify.Run(Core.LoadConfig());
|
||||
foreach (var l in r.Lines) Log(l);
|
||||
string stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
|
||||
string p = Verify.WriteLog(r, stamp);
|
||||
if (p.Length > 0) Log("лог: " + p);
|
||||
MessageBox.Show(r.Verdict, "Результат проверки", MessageBoxButtons.OK,
|
||||
r.Ok ? MessageBoxIcon.Information : (r.Verdict.StartsWith("ПРОВАЛ") ? MessageBoxIcon.Error : MessageBoxIcon.Warning));
|
||||
}
|
||||
catch (Exception ex) { MessageBox.Show(ex.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); }
|
||||
}
|
||||
|
||||
void OnSave()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Mode == "vless") { string h, u, rm; int p; Vless.Parse(txtField.Text, out h, out p, out u, out rm); CaptureConfig(); Log("VLESS сохранён: " + h + ":" + p + " [" + rm + "]"); }
|
||||
else { string h; int p; if (!Core.TryParseProxy(txtField.Text, out h, out p)) throw new Exception("Некорректный адрес."); CaptureConfig(); Log("прокси сохранён: " + Core.ProxyArg(txtField.Text)); }
|
||||
if (switchOn) Log("ВНИМАНИЕ: режим активен со старой настройкой — переключи рубильник, чтобы применить.");
|
||||
UpdateUi(true);
|
||||
}
|
||||
catch (Exception ex) { MessageBox.Show(ex.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); }
|
||||
}
|
||||
|
||||
void OnXray()
|
||||
{
|
||||
if (busy) return;
|
||||
try
|
||||
{
|
||||
SetBusy(true); Core.StopXray();
|
||||
try { if (Core.XrayPresent()) System.IO.File.Delete(Core.XrayExe); } catch { }
|
||||
Log("готовлю Xray-core..."); Core.EnsureXray(); UpdateXrayStatus(); Log("Xray готов.");
|
||||
}
|
||||
catch (Exception ex) { Log("ОШИБКА: " + ex.Message); MessageBox.Show(ex.Message, "Ошибка Xray", MessageBoxButtons.OK, MessageBoxIcon.Error); }
|
||||
finally { SetBusy(false); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<!-- asInvoker: run de-elevated so the Claude we launch is the user's normal instance -->
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0"
|
||||
processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user