mirror of
https://github.com/MaxiFan/TunnelX.git
synced 2026-05-17 21:14:37 +03:00
315 lines
11 KiB
C#
315 lines
11 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Windows;
|
|
using AppTunnel.Services;
|
|
using Application = System.Windows.Application;
|
|
using MessageBox = System.Windows.MessageBox;
|
|
|
|
namespace AppTunnel;
|
|
|
|
public partial class App : Application
|
|
{
|
|
private const string SingleInstanceMutexName = @"Global\TunnelX.SingleInstance";
|
|
private const string BringToFrontEventName = @"Global\TunnelX.BringToFront";
|
|
|
|
private Mutex? _singleInstanceMutex;
|
|
private EventWaitHandle? _bringToFrontEvent;
|
|
private RegisteredWaitHandle? _bringToFrontRegistration;
|
|
|
|
/// <summary>
|
|
/// Persistent data directory: %LOCALAPPDATA%\TunnelX\
|
|
/// All user data (profiles, config, native DLLs) is stored here so that
|
|
/// the application can run from any read-only or temporary location and
|
|
/// multiple instances share the same data folder.
|
|
/// </summary>
|
|
public static readonly string AppDataDir = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"TunnelX");
|
|
|
|
protected override void OnStartup(StartupEventArgs e)
|
|
{
|
|
if (!TryAcquireSingleInstance())
|
|
{
|
|
SignalExistingInstanceToShow();
|
|
Shutdown(0);
|
|
return;
|
|
}
|
|
|
|
// Ensure the data directory exists before anything else.
|
|
try
|
|
{
|
|
Directory.CreateDirectory(AppDataDir);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Without the data directory we cannot extract native libraries
|
|
// or persist any user data — there is nothing useful the app can
|
|
// do, so report a clear error and exit instead of crashing later
|
|
// with a confusing P/Invoke failure.
|
|
MessageBox.Show(
|
|
$"TunnelX cannot create its data directory:\n{AppDataDir}\n\n{ex.Message}\n\nPlease check that you have write permission to %LOCALAPPDATA%.",
|
|
"TunnelX — خطای راهاندازی",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
Shutdown(1);
|
|
return;
|
|
}
|
|
|
|
// Extract WinDivert/wintun native files from embedded resources into
|
|
// AppDataDir. Must happen BEFORE the NativeLibrary resolver is
|
|
// registered and before any DllImport call is made.
|
|
EnsureNativeLibsExtracted();
|
|
|
|
// Register a resolver so that DllImport("WinDivert.dll") and
|
|
// DllImport("wintun.dll") load from AppDataDir rather than relying on
|
|
// the default search order (which would only find them next to the exe).
|
|
NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), (name, asm, searchPath) =>
|
|
{
|
|
var candidate = Path.Combine(AppDataDir, name);
|
|
if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out var handle))
|
|
return handle;
|
|
// Fall back to default resolution (regular build side-by-side DLLs).
|
|
return IntPtr.Zero;
|
|
});
|
|
|
|
base.OnStartup(e);
|
|
|
|
var mainWindow = new MainWindow();
|
|
MainWindow = mainWindow;
|
|
mainWindow.Show();
|
|
|
|
// Clean up any stale VPN connection left by a previous crash/kill.
|
|
CleanupStaleVpn();
|
|
|
|
// Safety net: if the process is killed, try to disconnect VPN.
|
|
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
|
|
{
|
|
try { CleanupStaleVpn(); } catch { }
|
|
};
|
|
|
|
// Global exception handlers for debugging
|
|
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
|
|
{
|
|
var ex = args.ExceptionObject as Exception;
|
|
Logger.Error("Unhandled exception in AppDomain", ex);
|
|
try { CleanupStaleVpn(); } catch { }
|
|
MessageBox.Show(
|
|
$"Critical error occurred:\n{ex?.Message}\n\nCheck Debug Log for details.",
|
|
"TunnelX - Fatal Error",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
};
|
|
|
|
DispatcherUnhandledException += (sender, args) =>
|
|
{
|
|
Logger.Error("Unhandled exception in Dispatcher", args.Exception);
|
|
MessageBox.Show(
|
|
$"UI error occurred:\n{args.Exception.Message}\n\nCheck Debug Log for details.",
|
|
"TunnelX - Error",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
args.Handled = true; // Prevent crash
|
|
};
|
|
|
|
Logger.Info("TunnelX application started");
|
|
}
|
|
|
|
protected override void OnExit(ExitEventArgs e)
|
|
{
|
|
Logger.Info("TunnelX application exiting");
|
|
|
|
try { _bringToFrontRegistration?.Unregister(null); } catch { }
|
|
try { _bringToFrontEvent?.Dispose(); } catch { }
|
|
try
|
|
{
|
|
_singleInstanceMutex?.ReleaseMutex();
|
|
_singleInstanceMutex?.Dispose();
|
|
}
|
|
catch { }
|
|
|
|
base.OnExit(e);
|
|
}
|
|
|
|
private bool TryAcquireSingleInstance()
|
|
{
|
|
try
|
|
{
|
|
_singleInstanceMutex = new Mutex(true, SingleInstanceMutexName, out bool createdNew);
|
|
if (!createdNew)
|
|
return false;
|
|
|
|
_bringToFrontEvent = new EventWaitHandle(false, EventResetMode.AutoReset, BringToFrontEventName);
|
|
_bringToFrontRegistration = ThreadPool.RegisterWaitForSingleObject(
|
|
_bringToFrontEvent,
|
|
(_, _) => Dispatcher.BeginInvoke(new Action(BringMainWindowToFront)),
|
|
null,
|
|
Timeout.Infinite,
|
|
false);
|
|
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static void SignalExistingInstanceToShow()
|
|
{
|
|
try
|
|
{
|
|
using var evt = EventWaitHandle.OpenExisting(BringToFrontEventName);
|
|
evt.Set();
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private void BringMainWindowToFront()
|
|
{
|
|
if (MainWindow is MainWindow win)
|
|
{
|
|
win.BringToForeground();
|
|
return;
|
|
}
|
|
|
|
if (Application.Current.MainWindow is MainWindow fallback)
|
|
fallback.BringToForeground();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disconnects and removes the VPN connection if it exists.
|
|
/// Called on startup (to clean up after crash) and on fatal errors.
|
|
/// Also removes duplicates like "TunnelX 2", "TunnelX 3" that Windows
|
|
/// sometimes creates when the original wasn't fully removed.
|
|
/// </summary>
|
|
private static void CleanupStaleVpn()
|
|
{
|
|
// ── 0. Kill any stray sing-box process left over from a crash ──────────
|
|
try
|
|
{
|
|
var killSingBox = new ProcessStartInfo
|
|
{
|
|
FileName = "taskkill",
|
|
Arguments = "/F /IM sing-box.exe",
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
};
|
|
using var kp = Process.Start(killSingBox);
|
|
kp?.WaitForExit(3000);
|
|
}
|
|
catch { }
|
|
|
|
const string vpnName = "TunnelX";
|
|
try
|
|
{
|
|
// Disconnect if active
|
|
var disconnectPsi = new ProcessStartInfo
|
|
{
|
|
FileName = "rasdial",
|
|
Arguments = $"\"{vpnName}\" /disconnect",
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
};
|
|
using var dp = Process.Start(disconnectPsi);
|
|
dp?.WaitForExit(5000);
|
|
|
|
// Find and remove all VPN connections matching "TunnelX" or "TunnelX N"
|
|
var findPsi = new ProcessStartInfo
|
|
{
|
|
FileName = "powershell",
|
|
Arguments = "-NoProfile -Command \"Get-VpnConnection -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^TunnelX' } | Select-Object -ExpandProperty Name\"",
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
};
|
|
using var fp = Process.Start(findPsi);
|
|
var output = fp?.StandardOutput.ReadToEnd() ?? "";
|
|
fp?.WaitForExit(8000);
|
|
|
|
var names = output.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Where(n => n.Length > 0).ToList();
|
|
|
|
foreach (var name in names)
|
|
{
|
|
// Disconnect each variant
|
|
var dPsi = new ProcessStartInfo
|
|
{
|
|
FileName = "rasdial",
|
|
Arguments = $"\"{name}\" /disconnect",
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
};
|
|
using var ddp = Process.Start(dPsi);
|
|
ddp?.WaitForExit(3000);
|
|
|
|
// Remove VPN profile
|
|
var safeName = name.Replace("'", "''");
|
|
var rmPsi = new ProcessStartInfo
|
|
{
|
|
FileName = "powershell",
|
|
Arguments = $"-NoProfile -Command \"Remove-VpnConnection -Name '{safeName}' -Force -ErrorAction SilentlyContinue\"",
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
};
|
|
using var rp = Process.Start(rmPsi);
|
|
rp?.WaitForExit(5000);
|
|
}
|
|
|
|
if (names.Count > 0)
|
|
Logger.Info($"[CLEANUP] Removed {names.Count} stale VPN profile(s): {string.Join(", ", names)}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Warning($"[CLEANUP] Stale VPN cleanup failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts WinDivert.dll, WinDivert64.sys and wintun.dll from embedded
|
|
/// assembly resources into <see cref="AppDataDir"/>.
|
|
/// For regular (non-single-file) builds the DLLs live next to the exe and
|
|
/// are not embedded, so GetManifestResourceStream returns null — no-op.
|
|
/// Each file is re-extracted only when it is missing or has a different
|
|
/// size (i.e. a new version was just deployed).
|
|
/// </summary>
|
|
private static void EnsureNativeLibsExtracted()
|
|
{
|
|
var asm = Assembly.GetExecutingAssembly();
|
|
|
|
foreach (var name in new[] { "WinDivert.dll", "WinDivert64.sys", "wintun.dll" })
|
|
{
|
|
try
|
|
{
|
|
using var stream = asm.GetManifestResourceStream(name);
|
|
if (stream == null) continue; // not embedded → regular build, skip
|
|
|
|
var destPath = Path.Combine(AppDataDir, name);
|
|
|
|
// Skip if already extracted with the same size.
|
|
if (File.Exists(destPath) && new FileInfo(destPath).Length == stream.Length)
|
|
continue;
|
|
|
|
using var fs = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
stream.CopyTo(fs);
|
|
Logger.Info($"[NATIVE] Extracted {name} → {destPath}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Warning($"[NATIVE] Could not extract {name}: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|