diff --git a/AppTunnel/Services/OpenVpnTunnelProvider.cs b/AppTunnel/Services/OpenVpnTunnelProvider.cs index 44561b6..421671f 100644 --- a/AppTunnel/Services/OpenVpnTunnelProvider.cs +++ b/AppTunnel/Services/OpenVpnTunnelProvider.cs @@ -184,6 +184,8 @@ public class OpenVpnTunnelProvider : ITunnelProvider public bool IsInterfaceUp() { + TryUpdateConnectedStatusFromCapturedState(); + if (_vpnInterfaceIndex < 0) return false; try { @@ -198,6 +200,41 @@ public class OpenVpnTunnelProvider : ITunnelProvider return false; } + private bool TryUpdateConnectedStatusFromCapturedState() + { + if (Status.State != ConnectionState.Connected) + return false; + if (string.IsNullOrWhiteSpace(_assignedLocalIp) || + string.IsNullOrWhiteSpace(_routeGatewayIp) || + string.IsNullOrWhiteSpace(_connectedRemoteIp) || + _connectedRemotePort <= 0) + return false; + + var interfaceIndex = FindOpenVpnInterfaceIndex(_assignedLocalIp); + if (interfaceIndex <= 0) + return false; + + var changed = + Status.VpnInterfaceIndex != interfaceIndex || + !string.Equals(Status.VpnLocalIp, _assignedLocalIp, StringComparison.OrdinalIgnoreCase) || + !string.Equals(Status.VpnGatewayIp, _routeGatewayIp, StringComparison.OrdinalIgnoreCase) || + !string.Equals(Status.VpnServerIp, _connectedRemoteIp, StringComparison.OrdinalIgnoreCase) || + Status.VpnServerPort != _connectedRemotePort; + + if (!changed) + return true; + + _vpnInterfaceIndex = interfaceIndex; + Status.VpnInterfaceIndex = interfaceIndex; + Status.VpnLocalIp = _assignedLocalIp; + Status.VpnGatewayIp = _routeGatewayIp; + Status.VpnServerIp = _connectedRemoteIp; + Status.VpnServerPort = _connectedRemotePort; + Status.Message = "OpenVPN متصل شد (Split Tunnel)"; + Logger.Warning($"[OpenVPN] Runtime endpoint changed. LocalIP={Status.VpnLocalIp} Gateway={Status.VpnGatewayIp} Remote={Status.VpnServerIp}:{Status.VpnServerPort} IF={Status.VpnInterfaceIndex}"); + return true; + } + private async Task KillProcessAsync() { var processId = _process?.Id; @@ -505,6 +542,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider { _routeGatewayIp = gateway; Logger.Info($"[OpenVPN] Captured route-gateway {gateway}"); + TryUpdateConnectedStatusFromCapturedState(); } } @@ -544,6 +582,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider _connectedRemoteIp = host; _connectedRemotePort = port; Logger.Info($"[OpenVPN] Captured connected remote {host}:{port}"); + TryUpdateConnectedStatusFromCapturedState(); } private void TryCaptureAssignedLocalIp(string line) @@ -564,6 +603,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider { _assignedLocalIp = localIp; Logger.Info($"[OpenVPN] Captured assigned local IP {localIp}"); + TryUpdateConnectedStatusFromCapturedState(); } } diff --git a/AppTunnel/ViewModels/MainViewModel.Connection.cs b/AppTunnel/ViewModels/MainViewModel.Connection.cs index 8e0c0b6..4355121 100644 --- a/AppTunnel/ViewModels/MainViewModel.Connection.cs +++ b/AppTunnel/ViewModels/MainViewModel.Connection.cs @@ -141,32 +141,13 @@ public partial class MainViewModel StatusText = _vpnService.Status.Message; VpnIp = _vpnService.Status.VpnLocalIp; VpnAdapterName = ResolveInterfaceName(_vpnService.Status.VpnInterfaceIndex); + _currentVpnInterfaceIndex = _vpnService.Status.VpnInterfaceIndex; + _currentVpnGatewayIp = _vpnService.Status.VpnGatewayIp; _connectionStartTime = DateTime.Now; LastActiveProfileId = _selectedProfile?.Id; RaiseHealthStatusChanged(); - // Start traffic routing for enabled apps - var enabledApps = TunnelApps.Where(a => a.IsEnabled).ToList(); - _trafficRouter.ClearTargetApps(); - foreach (var app in enabledApps) - { - app.BytesSent = 0; - app.BytesReceived = 0; - _trafficRouter.AddTargetApp(app.ExecutableName); - } - - // Load user's exclude list (domains/IPs to bypass tunnel) - _trafficRouter.SetExcludedDestinations(ExcludedDestinations); - _trafficRouter.SetIncludedDestinations(IncludedDestinations); - _trafficRouter.Socks5Port = MixedProxyPort; - _trafficRouter.EnableDnsOptimization = IsDnsOptimizationEnabled; - _trafficRouter.EnableGameMode = IsGameModeEnabled; - - _trafficRouter.Start( - _vpnService.Status.VpnInterfaceIndex, - _vpnService.Status.VpnLocalIp, - _vpnService.Status.VpnServerIp, - _vpnService.Status.VpnGatewayIp); // actual proxy/VPN server host, resolved by TrafficRouter + StartTrafficRouterForCurrentStatus(resetAppCounters: true); _vpnHealthCheckCounter = 0; _timer.Start(); @@ -207,12 +188,41 @@ public partial class MainViewModel VpnIp = ""; VpnAdapterName = ""; + _currentVpnInterfaceIndex = -1; + _currentVpnGatewayIp = ""; _isFullRouteEnabled = false; OnPropertyChanged(nameof(IsFullRouteEnabled)); OnPropertyChanged(nameof(FullRouteStatusText)); RaiseHealthStatusChanged(); } + private void StartTrafficRouterForCurrentStatus(bool resetAppCounters) + { + var enabledApps = TunnelApps.Where(a => a.IsEnabled).ToList(); + _trafficRouter.ClearTargetApps(); + foreach (var app in enabledApps) + { + if (resetAppCounters) + { + app.BytesSent = 0; + app.BytesReceived = 0; + } + _trafficRouter.AddTargetApp(app.ExecutableName); + } + + _trafficRouter.SetExcludedDestinations(ExcludedDestinations); + _trafficRouter.SetIncludedDestinations(IncludedDestinations); + _trafficRouter.Socks5Port = MixedProxyPort; + _trafficRouter.EnableDnsOptimization = IsDnsOptimizationEnabled; + _trafficRouter.EnableGameMode = IsGameModeEnabled; + + _trafficRouter.Start( + _vpnService.Status.VpnInterfaceIndex, + _vpnService.Status.VpnLocalIp, + _vpnService.Status.VpnServerIp, + _vpnService.Status.VpnGatewayIp); + } + private async Task DisconnectAsync() { IsBusy = true; @@ -233,6 +243,8 @@ public partial class MainViewModel StatusText = "قطع شد"; VpnIp = ""; VpnAdapterName = ""; + _currentVpnInterfaceIndex = -1; + _currentVpnGatewayIp = ""; _isFullRouteEnabled = false; OnPropertyChanged(nameof(IsFullRouteEnabled)); OnPropertyChanged(nameof(FullRouteStatusText)); @@ -283,6 +295,8 @@ public partial class MainViewModel StatusText = "اتصال VPN به‌طور غیرمنتظره قطع شد"; VpnIp = ""; VpnAdapterName = ""; + _currentVpnInterfaceIndex = -1; + _currentVpnGatewayIp = ""; _isFullRouteEnabled = false; OnPropertyChanged(nameof(IsFullRouteEnabled)); OnPropertyChanged(nameof(FullRouteStatusText)); @@ -314,11 +328,20 @@ public partial class MainViewModel } private int _vpnHealthCheckCounter; + private bool _isRefreshingOpenVpnRouter; + private int _currentVpnInterfaceIndex = -1; + private string _currentVpnGatewayIp = ""; private void UpdateTimerTick() { if (!IsConnected) return; + if (CurrentTunnelType == TunnelType.OpenVpn && OpenVpnRuntimeEndpointChanged()) + { + _ = RefreshOpenVpnRouterAsync(); + return; + } + // Check VPN interface health every 5 seconds if (++_vpnHealthCheckCounter >= 5) { @@ -358,6 +381,82 @@ public partial class MainViewModel RaiseHealthStatusChanged(); } + private bool OpenVpnRuntimeEndpointChanged() + { + _vpnService.IsInterfaceUp(); // lets the OpenVPN provider publish post-reconnect IP/gateway changes. + var status = _vpnService.Status; + if (string.IsNullOrWhiteSpace(status.VpnLocalIp) || + string.IsNullOrWhiteSpace(status.VpnGatewayIp) || + status.VpnInterfaceIndex <= 0) + return false; + + return !string.Equals(status.VpnLocalIp, VpnIp, StringComparison.OrdinalIgnoreCase) || + status.VpnInterfaceIndex != _currentVpnInterfaceIndex || + !string.Equals(status.VpnGatewayIp, _currentVpnGatewayIp, StringComparison.OrdinalIgnoreCase); + } + + private async Task RefreshOpenVpnRouterAsync() + { + if (_isRefreshingOpenVpnRouter) return; + if (!IsConnected || CurrentTunnelType != TunnelType.OpenVpn) return; + + try + { + _isRefreshingOpenVpnRouter = true; + var status = _vpnService.Status; + if (string.IsNullOrWhiteSpace(status.VpnLocalIp) || + string.IsNullOrWhiteSpace(status.VpnGatewayIp) || + status.VpnInterfaceIndex <= 0) + return; + + if (string.Equals(status.VpnLocalIp, VpnIp, StringComparison.OrdinalIgnoreCase) && + status.VpnInterfaceIndex == _currentVpnInterfaceIndex && + string.Equals(status.VpnGatewayIp, _currentVpnGatewayIp, StringComparison.OrdinalIgnoreCase)) + return; + + var wasFullRoute = IsFullRouteEnabled; + Logger.Warning($"[OpenVPN] Runtime endpoint changed; restarting TrafficRouter. OldIP={VpnIp} NewIP={status.VpnLocalIp} Gateway={status.VpnGatewayIp} IF={status.VpnInterfaceIndex}"); + StatusText = "OpenVPN دوباره متصل شد؛ مسیرهای TunnelX در حال بروزرسانی است..."; + + _timer.Stop(); + _pingCts?.Cancel(); + IsPinging = false; + + await _trafficRouter.StopAsync(); + + VpnIp = status.VpnLocalIp; + VpnAdapterName = ResolveInterfaceName(status.VpnInterfaceIndex); + _currentVpnInterfaceIndex = status.VpnInterfaceIndex; + _currentVpnGatewayIp = status.VpnGatewayIp; + _isFullRouteEnabled = false; + OnPropertyChanged(nameof(IsFullRouteEnabled)); + OnPropertyChanged(nameof(FullRouteStatusText)); + + StartTrafficRouterForCurrentStatus(resetAppCounters: false); + if (wasFullRoute) + { + _isFullRouteEnabled = _trafficRouter.SetFullRouteEnabled(true); + OnPropertyChanged(nameof(IsFullRouteEnabled)); + OnPropertyChanged(nameof(FullRouteStatusText)); + } + + _vpnHealthCheckCounter = 0; + StatusText = "OpenVPN دوباره متصل شد و مسیرها بروزرسانی شدند"; + RaiseHealthStatusChanged(); + } + catch (Exception ex) + { + Logger.Error("[OpenVPN] TrafficRouter refresh after reconnect failed", ex); + await HandleVpnDroppedAsync(); + } + finally + { + _isRefreshingOpenVpnRouter = false; + if (IsConnected) + _timer.Start(); + } + } + private void OnTrafficUpdated(string exeName, long sent, long received) { Application.Current?.Dispatcher.BeginInvoke(() => diff --git a/CHANGELOG.md b/CHANGELOG.md index 788d359..30f4547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,18 @@ ## Unreleased +### English + +- Fixed OpenVPN internal reconnect handling by detecting runtime tunnel IP, gateway, interface, or remote endpoint changes and restarting TunnelX packet routing with the new values. + +### فارسی + +- مشکل reconnect داخلی OpenVPN اصلاح شد؛ اگر هنگام اتصال طولانی IP تونل، gateway، interface یا سرور مقصد عوض شود، TunnelX مسیر‌دهی ترافیک را با مقادیر جدید دوباره راه‌اندازی می‌کند. + ## 1.2.26 - 2026-05-17 +### English + - Added OpenVPN Community support as an external tunnel provider for split tunneling. - Added `.ovpn` file selection, OpenVPN username/password fields, install detection, and clearer Persian guidance in the connection and help screens. - Added split-compatible OpenVPN config preparation with route/DNS push filtering, credential file handling without UTF-8 BOM, remote candidate filtering, and faster retry behavior. @@ -11,6 +21,15 @@ - Added OpenVPN stale-process cleanup for TunnelX-started OpenVPN processes and prevented stale TAP adapters from being treated as a fresh connection. - Improved server testing and post-connect ping behavior for OpenVPN profiles. +### فارسی + +- پشتیبانی از OpenVPN Community به‌عنوان ارائه‌دهنده خارجی تونل برای Split Tunneling اضافه شد. +- انتخاب فایل `.ovpn`، فیلدهای نام کاربری و رمز عبور OpenVPN، تشخیص نصب بودن OpenVPN Community و راهنمای فارسی واضح‌تر در صفحه اتصال و راهنما اضافه شد. +- آماده‌سازی کانفیگ OpenVPN سازگار با Split Tunnel اضافه شد؛ شامل نادیده گرفتن route/DNSهای push شده، ذخیره فایل credential بدون UTF-8 BOM، فیلتر کردن remoteهای نامعتبر و retry سریع‌تر. +- مسیر‌دهی Split Tunnel در OpenVPN با ثبت remote واقعی متصل‌شده، IP اختصاص داده‌شده به تونل و route gateway قبل از شروع packet routing اصلاح شد. +- پاک‌سازی پردازش‌های قدیمی OpenVPN که توسط TunnelX اجرا شده‌اند اضافه شد و از شناسایی آداپترهای TAP خراب یا قدیمی به‌عنوان اتصال جدید جلوگیری شد. +- تست سرور و پینگ بعد از اتصال برای پروفایل‌های OpenVPN بهبود پیدا کرد. + ## 1.2.25 - 2026-05-16 - Merge pull request #13 from BlacKSnowDot0/pr-clean diff --git a/README.fa.md b/README.fa.md index f1dc651..fb030f5 100644 --- a/README.fa.md +++ b/README.fa.md @@ -6,8 +6,6 @@ TunnelX یک نرم‌افزار آزاد و رایگان برای ویندوز است که توسط **MaxFan** ساخته شده و برای مدیریت تونل، وی‌پی‌ان و Split Tunneling استفاده می‌شود. این برنامه می‌تواند ترافیک برنامه‌های انتخاب‌شده، مقصدهای مشخص، یا کل سیستم را از تونل عبور دهد و هم‌زمان مسیر عادی شبکه را برای مقصدهای محلی یا مستثنی‌شده حفظ کند. -> وضعیت پروژه: پیش‌انتشار. قبل از انتشار عمومی فایل اجرایی، نکات ساخت و انتشار را در `docs/BUILD.md` بررسی کنید. - ## کاربرد برنامه TunnelX برای زمانی ساخته شده که کاربر نمی‌خواهد تمام ترافیک سیستم از وی‌پی‌ان عبور کند. با این برنامه می‌توان فقط برنامه‌هایی مثل مرورگر، تلگرام، ابزارهای توسعه یا برنامه‌های مشخص دیگر را وارد تونل کرد و بقیه ترافیک سیستم را روی اینترنت عادی نگه داشت. همچنین در صورت نیاز، حالت Full-route برای عبور کل سیستم از تونل در دسترس است. diff --git a/README.md b/README.md index 9ebbc47..5eb5bb2 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ TunnelX is a free and open-source Windows split-tunneling client built by **MaxFan**. It routes selected apps, selected destinations, or the whole system through supported tunnel cores while keeping local and excluded destinations on the normal network path. -> Status: pre-release. Review the release notes in `docs/BUILD.md` before publishing a public artifact. - ## Features - App-based split tunneling for selected Windows processes