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
+393
View File
@@ -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; } }); }
}
}
+391
View File
@@ -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); }
}
}
}
+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());
}
}
}
+22
View File
@@ -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>