Fix OpenVPN reconnect routing

Update the router when OpenVPN reconnects with a new runtime endpoint, and keep the release notes and README status in sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MaxFan
2026-05-17 15:53:14 +03:30
parent 83ea7560f4
commit a686edd027
5 changed files with 180 additions and 26 deletions
@@ -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();
}
}
+121 -22
View File
@@ -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(() =>
+19
View File
@@ -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
-2
View File
@@ -6,8 +6,6 @@
<span dir="ltr">TunnelX</span> یک نرم‌افزار آزاد و رایگان برای ویندوز است که توسط **<span dir="ltr">MaxFan</span>** ساخته شده و برای مدیریت تونل، وی‌پی‌ان و <span dir="ltr">Split Tunneling</span> استفاده می‌شود. این برنامه می‌تواند ترافیک برنامه‌های انتخاب‌شده، مقصدهای مشخص، یا کل سیستم را از تونل عبور دهد و هم‌زمان مسیر عادی شبکه را برای مقصدهای محلی یا مستثنی‌شده حفظ کند.
> وضعیت پروژه: پیش‌انتشار. قبل از انتشار عمومی فایل اجرایی، نکات ساخت و انتشار را در <span dir="ltr">`docs/BUILD.md`</span> بررسی کنید.
## کاربرد برنامه
<span dir="ltr">TunnelX</span> برای زمانی ساخته شده که کاربر نمی‌خواهد تمام ترافیک سیستم از وی‌پی‌ان عبور کند. با این برنامه می‌توان فقط برنامه‌هایی مثل مرورگر، تلگرام، ابزارهای توسعه یا برنامه‌های مشخص دیگر را وارد تونل کرد و بقیه ترافیک سیستم را روی اینترنت عادی نگه داشت. همچنین در صورت نیاز، حالت <span dir="ltr">Full-route</span> برای عبور کل سیستم از تونل در دسترس است.
-2
View File
@@ -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