mirror of
https://github.com/MaxiFan/TunnelX.git
synced 2026-05-18 07:34:36 +03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52d970f49e | |||
| 588daa1332 | |||
| b2974cdc95 |
@@ -23,9 +23,9 @@
|
||||
<NeutralLanguage>fa-IR</NeutralLanguage>
|
||||
|
||||
<!-- Version Management -->
|
||||
<Version>1.2.22</Version>
|
||||
<AssemblyVersion>1.2.22.0</AssemblyVersion>
|
||||
<FileVersion>1.2.22.0</FileVersion>
|
||||
<Version>1.2.23</Version>
|
||||
<AssemblyVersion>1.2.23.0</AssemblyVersion>
|
||||
<FileVersion>1.2.23.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media;
|
||||
using AppTunnel.Models;
|
||||
using AppTunnel.ViewModels;
|
||||
using AppTunnel.Helpers;
|
||||
using AppTunnel.Services;
|
||||
@@ -16,6 +17,8 @@ public partial class MainWindow : Window
|
||||
private CancellationTokenSource _loadCts = new();
|
||||
private System.Windows.Forms.NotifyIcon? _trayIcon;
|
||||
private bool _isRealExit;
|
||||
private ConnectionState _lastNotifiedConnectionState = ConnectionState.Disconnected;
|
||||
private bool _updateNotificationShown;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -69,8 +72,25 @@ public partial class MainWindow : Window
|
||||
statusItem.Text = $"وضعیت: {_viewModel.StatusText}";
|
||||
});
|
||||
}
|
||||
else if (args.PropertyName == nameof(MainViewModel.ConnectionState))
|
||||
{
|
||||
Dispatcher.BeginInvoke(NotifyConnectionStateChanged);
|
||||
}
|
||||
else if (args.PropertyName == nameof(MainViewModel.IsUpdateAvailable))
|
||||
{
|
||||
Dispatcher.BeginInvoke(NotifyUpdateAvailable);
|
||||
}
|
||||
};
|
||||
|
||||
var updateItem = new System.Windows.Forms.ToolStripMenuItem("بررسی بروزرسانی");
|
||||
updateItem.Click += (_, _) =>
|
||||
{
|
||||
BringToForeground();
|
||||
if (_viewModel.CheckForUpdatesCommand.CanExecute(null))
|
||||
_viewModel.CheckForUpdatesCommand.Execute(null);
|
||||
};
|
||||
menu.Items.Add(updateItem);
|
||||
|
||||
menu.Items.Add(new System.Windows.Forms.ToolStripSeparator());
|
||||
|
||||
var exitItem = new System.Windows.Forms.ToolStripMenuItem("خروج از برنامه");
|
||||
@@ -136,6 +156,49 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyConnectionStateChanged()
|
||||
{
|
||||
if (_trayIcon == null) return;
|
||||
|
||||
var state = _viewModel.ConnectionState;
|
||||
if (state == _lastNotifiedConnectionState) return;
|
||||
_lastNotifiedConnectionState = state;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ConnectionState.Connected:
|
||||
ShowTrayNotification("TunnelX متصل شد", _viewModel.StatusText,
|
||||
System.Windows.Forms.ToolTipIcon.Info);
|
||||
break;
|
||||
case ConnectionState.Disconnected:
|
||||
ShowTrayNotification("TunnelX قطع شد", _viewModel.StatusText,
|
||||
System.Windows.Forms.ToolTipIcon.Info);
|
||||
break;
|
||||
case ConnectionState.Error:
|
||||
ShowTrayNotification("خطا در اتصال TunnelX", _viewModel.StatusText,
|
||||
System.Windows.Forms.ToolTipIcon.Warning);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyUpdateAvailable()
|
||||
{
|
||||
if (_trayIcon == null || _updateNotificationShown || !_viewModel.IsUpdateAvailable)
|
||||
return;
|
||||
|
||||
_updateNotificationShown = true;
|
||||
ShowTrayNotification("بروزرسانی TunnelX آماده است",
|
||||
_viewModel.UpdateStatusText,
|
||||
System.Windows.Forms.ToolTipIcon.Info);
|
||||
}
|
||||
|
||||
private void ShowTrayNotification(string title, string message, System.Windows.Forms.ToolTipIcon icon)
|
||||
{
|
||||
if (_trayIcon == null) return;
|
||||
_trayIcon.Visible = true;
|
||||
_trayIcon.ShowBalloonTip(3500, title, message, icon);
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Force window to foreground — borderless/transparent windows sometimes
|
||||
|
||||
@@ -5,6 +5,7 @@ public static class AppInfo
|
||||
public const string AppName = "TunnelX";
|
||||
public const string CreatorName = "MaxFan";
|
||||
public const string GitHubUrl = "https://github.com/MaxiFan/TunnelX";
|
||||
public const string LatestReleaseUrl = GitHubUrl + "/releases/latest";
|
||||
public const string LicenseName = "GPL-3.0-or-later";
|
||||
public const string PayPalEmail = "gallafan@gmail.com";
|
||||
public const string PayPalDonateUrl = "https://www.paypal.com/donate/?business=gallafan%40gmail.com¤cy_code=USD";
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AppTunnel.Services;
|
||||
|
||||
public sealed record GitHubReleaseInfo(
|
||||
Version Version,
|
||||
string TagName,
|
||||
string Name,
|
||||
string Url,
|
||||
bool IsPrerelease);
|
||||
|
||||
public static class GitHubReleaseChecker
|
||||
{
|
||||
private const string LatestReleaseApi =
|
||||
"https://api.github.com/repos/MaxiFan/TunnelX/releases/latest";
|
||||
|
||||
public static async Task<GitHubReleaseInfo?> GetLatestReleaseAsync(CancellationToken ct)
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd("TunnelX");
|
||||
http.Timeout = TimeSpan.FromSeconds(8);
|
||||
|
||||
using var response = await http.GetAsync(LatestReleaseApi, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(ct);
|
||||
using var json = await JsonDocument.ParseAsync(stream, cancellationToken: ct);
|
||||
var root = json.RootElement;
|
||||
|
||||
var tag = root.TryGetProperty("tag_name", out var tagElement)
|
||||
? tagElement.GetString() ?? ""
|
||||
: "";
|
||||
if (!TryParseVersion(tag, out var version))
|
||||
return null;
|
||||
|
||||
var name = root.TryGetProperty("name", out var nameElement)
|
||||
? nameElement.GetString() ?? tag
|
||||
: tag;
|
||||
var url = root.TryGetProperty("html_url", out var urlElement)
|
||||
? urlElement.GetString() ?? AppInfo.LatestReleaseUrl
|
||||
: AppInfo.LatestReleaseUrl;
|
||||
var prerelease = root.TryGetProperty("prerelease", out var preElement) &&
|
||||
preElement.ValueKind == JsonValueKind.True;
|
||||
|
||||
return new GitHubReleaseInfo(version, tag, name, url, prerelease);
|
||||
}
|
||||
|
||||
public static bool TryParseVersion(string value, out Version version)
|
||||
{
|
||||
value = (value ?? "").Trim().TrimStart('v', 'V');
|
||||
return Version.TryParse(value, out version!);
|
||||
}
|
||||
}
|
||||
@@ -293,9 +293,11 @@ public partial class TrafficRouterService : IDisposable
|
||||
public long LeakCount => Interlocked.Read(ref _statLeakConfirmed);
|
||||
/// <summary>
|
||||
/// Number of attempted leaks blocked locally by leak-guard.
|
||||
/// Diagnostic-only signal for policy-transition races.
|
||||
/// Diagnostic-only signal; these packets did not escape the machine.
|
||||
/// </summary>
|
||||
public long LeakBlockedCount => Interlocked.Read(ref _statLeakBlocked);
|
||||
public long LeakBlockedRecoveredCount => Interlocked.Read(ref _statLeakBlockedRecovered);
|
||||
public long LeakBlockedSuppressedCount => Interlocked.Read(ref _statLeakBlockedSuppressed);
|
||||
public long Ipv6BlockedCount => Interlocked.Read(ref _statFlowIPv6Blocked);
|
||||
public long DnsRedirectCount => Interlocked.Read(ref _redirectCount);
|
||||
public long ActiveRouteCount => _addedRoutes.Count;
|
||||
@@ -540,13 +542,13 @@ public partial class TrafficRouterService : IDisposable
|
||||
long netOutFail = Interlocked.Read(ref _statNetOutSendFailed);
|
||||
string mode = _fullRouteEnabled ? "full-route" : "split";
|
||||
string leakState = leakConfirmed > 0 ? "LEAK-DETECTED" :
|
||||
(leakBlocked > 0 ? "BLOCKING-ATTEMPTS" : "OK");
|
||||
(leakBlocked > 0 ? "PROTECTED" : "OK");
|
||||
Logger.Info(
|
||||
$"[STATS] mode={mode} health={leakState} " +
|
||||
$"flows={flowEst}/{flowDel} targetHit={flowHit} excluded={flowExcl} ipv6Drop={ipv6Blocked} " +
|
||||
$"routes={Interlocked.Read(ref _statRoutesAdded)}({Interlocked.Read(ref _statRoutesFailed)}fail)/{_addedRoutes.Count}active " +
|
||||
$"rewriteOut={netOutRw} rewriteIn={netInRw} rewriteFail={netOutFail} nat={_natTable.Count} " +
|
||||
$"leakConfirmed={leakConfirmed} leakBlocked={leakBlocked} recovered={leakBlockedRecovered} suppressed={leakBlockedSuppressed} " +
|
||||
$"leakConfirmed={leakConfirmed} protectedBlocked={leakBlocked} recovered={leakBlockedRecovered} suppressed={leakBlockedSuppressed} " +
|
||||
$"targets={_targetExecutables.Count} blockedApps={_blockedExecutables.Count}");
|
||||
|
||||
// Loop health check — warn if any background loop has exited unexpectedly
|
||||
|
||||
@@ -235,11 +235,11 @@ public partial class TrafficRouterService
|
||||
|
||||
leakLogCount++;
|
||||
if (recovered)
|
||||
Logger.Warning($"[LEAK-BLOCKED] Packet with VPN srcIP exiting PHYSICAL ifIdx={addrBuf.IfIdx} → dst={dst} (proto={buffer[9]}) — blocked locally, route restored for retransmit via VPN");
|
||||
Logger.Info($"[LEAK-PROTECTED] Packet with VPN srcIP exiting PHYSICAL ifIdx={addrBuf.IfIdx} → dst={dst} (proto={buffer[9]}) — blocked locally, route restored for retransmit via VPN");
|
||||
else if (graceSuppressed)
|
||||
Logger.Info($"[LEAK-BLOCKED-TRANSITION] Packet with VPN srcIP exiting PHYSICAL ifIdx={addrBuf.IfIdx} → dst={dst} (proto={buffer[9]}) — blocked during policy transition grace");
|
||||
Logger.Info($"[LEAK-PROTECTED-TRANSITION] Packet with VPN srcIP exiting PHYSICAL ifIdx={addrBuf.IfIdx} → dst={dst} (proto={buffer[9]}) — blocked during policy transition grace");
|
||||
else
|
||||
Logger.Warning($"[LEAK-BLOCKED] Packet with VPN srcIP exiting PHYSICAL ifIdx={addrBuf.IfIdx} → dst={dst} (proto={buffer[9]}) — blocked by split policy, route not restored");
|
||||
Logger.Info($"[LEAK-PROTECTED] Packet with VPN srcIP exiting PHYSICAL ifIdx={addrBuf.IfIdx} → dst={dst} (proto={buffer[9]}) — blocked by split policy, route not restored");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
OpenGitHubCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.GitHubUrl));
|
||||
OpenDonateCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.PayPalDonateUrl));
|
||||
CopyDonationInfoCommand = new RelayCommand(_ => CopyDonationInfoToClipboard());
|
||||
CheckForUpdatesCommand = new RelayCommand(_ => _ = CheckForUpdatesAsync(false), _ => !IsCheckingForUpdates);
|
||||
OpenLatestReleaseCommand = new RelayCommand(_ => OpenExternalLink(LatestReleaseUrl), _ => !string.IsNullOrWhiteSpace(LatestReleaseUrl));
|
||||
|
||||
_trafficRouter.TrafficUpdated += OnTrafficUpdated;
|
||||
|
||||
@@ -88,6 +90,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
LoadExcludes();
|
||||
LoadIncludes();
|
||||
LoadHistory();
|
||||
_ = CheckForUpdatesAsync(true);
|
||||
}
|
||||
|
||||
#region Properties
|
||||
@@ -251,6 +254,59 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
public string DonatePayPalText => $"پیپل: {AppInfo.PayPalEmail}";
|
||||
public string CryptoDonationText => AppInfo.CryptoDonationText;
|
||||
|
||||
private bool _isCheckingForUpdates;
|
||||
public bool IsCheckingForUpdates
|
||||
{
|
||||
get => _isCheckingForUpdates;
|
||||
set
|
||||
{
|
||||
if (_isCheckingForUpdates == value) return;
|
||||
_isCheckingForUpdates = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(UpdateButtonText));
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isUpdateAvailable;
|
||||
public bool IsUpdateAvailable
|
||||
{
|
||||
get => _isUpdateAvailable;
|
||||
set
|
||||
{
|
||||
if (_isUpdateAvailable == value) return;
|
||||
_isUpdateAvailable = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _updateStatusText = "برای بررسی نسخه جدید، دکمه بررسی بروزرسانی را بزنید.";
|
||||
public string UpdateStatusText
|
||||
{
|
||||
get => _updateStatusText;
|
||||
set
|
||||
{
|
||||
if (_updateStatusText == value) return;
|
||||
_updateStatusText = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _latestReleaseUrl = AppInfo.LatestReleaseUrl;
|
||||
public string LatestReleaseUrl
|
||||
{
|
||||
get => _latestReleaseUrl;
|
||||
set
|
||||
{
|
||||
if (_latestReleaseUrl == value) return;
|
||||
_latestReleaseUrl = value;
|
||||
OnPropertyChanged();
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
}
|
||||
|
||||
public string UpdateButtonText => IsCheckingForUpdates ? "در حال بررسی..." : "بررسی بروزرسانی";
|
||||
|
||||
public string ConnectButtonText => _connectionState switch
|
||||
{
|
||||
ConnectionState.Disconnected => "🔌 اتصال",
|
||||
@@ -403,20 +459,20 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
? (_trafficRouter.LeakCount == 0
|
||||
? (_trafficRouter.LeakBlockedCount == 0
|
||||
? "Leak: OK"
|
||||
: $"Leak: OK (blocked {_trafficRouter.LeakBlockedCount})")
|
||||
: $"Leak: Protected {_trafficRouter.LeakBlockedCount}")
|
||||
: $"Leak: {_trafficRouter.LeakCount}")
|
||||
: "Leak: -";
|
||||
public string HeaderLeakColor => !IsConnected
|
||||
? "#6CCB5F"
|
||||
: _trafficRouter.LeakCount > 0
|
||||
? "#E05252"
|
||||
: (_trafficRouter.LeakBlockedCount > 0 ? "#E07820" : "#6CCB5F");
|
||||
: "#6CCB5F";
|
||||
|
||||
public string HealthLeakText => IsConnected
|
||||
? (_trafficRouter.LeakCount == 0
|
||||
? (_trafficRouter.LeakBlockedCount == 0
|
||||
? "0 leak"
|
||||
: $"0 leak / {_trafficRouter.LeakBlockedCount} blocked")
|
||||
: $"0 leak / {_trafficRouter.LeakBlockedCount} protected")
|
||||
: $"{_trafficRouter.LeakCount} leak")
|
||||
: "-";
|
||||
public string HealthDnsText => IsConnected
|
||||
@@ -619,6 +675,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
public ICommand OpenGitHubCommand { get; }
|
||||
public ICommand OpenDonateCommand { get; }
|
||||
public ICommand CopyDonationInfoCommand { get; }
|
||||
public ICommand CheckForUpdatesCommand { get; }
|
||||
public ICommand OpenLatestReleaseCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -658,6 +716,61 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckForUpdatesAsync(bool silent)
|
||||
{
|
||||
if (IsCheckingForUpdates) return;
|
||||
|
||||
try
|
||||
{
|
||||
IsCheckingForUpdates = true;
|
||||
if (!silent)
|
||||
UpdateStatusText = "در حال بررسی آخرین نسخه در GitHub...";
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var latest = await GitHubReleaseChecker.GetLatestReleaseAsync(cts.Token);
|
||||
if (latest == null)
|
||||
{
|
||||
if (!silent)
|
||||
UpdateStatusText = "بررسی نسخه جدید ناموفق بود. اتصال اینترنت یا GitHub را بررسی کنید.";
|
||||
Logger.Warning("[UPDATE] Latest release check failed");
|
||||
return;
|
||||
}
|
||||
|
||||
LatestReleaseUrl = latest.Url;
|
||||
var currentVersion = System.Reflection.Assembly.GetExecutingAssembly()
|
||||
.GetName().Version ?? new Version(0, 0, 0);
|
||||
var current = new Version(currentVersion.Major, currentVersion.Minor, currentVersion.Build);
|
||||
|
||||
if (latest.Version > current)
|
||||
{
|
||||
IsUpdateAvailable = true;
|
||||
UpdateStatusText = $"نسخه جدید آماده است: {latest.TagName} - برای دانلود از GitHub باز کنید.";
|
||||
Logger.Info($"[UPDATE] New version available: current={current} latest={latest.TagName}");
|
||||
return;
|
||||
}
|
||||
|
||||
IsUpdateAvailable = false;
|
||||
UpdateStatusText = $"TunnelX بهروز است. نسخه فعلی: {AppInfo.VersionText}";
|
||||
Logger.Info($"[UPDATE] App is up to date: current={current} latest={latest.TagName}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (!silent)
|
||||
UpdateStatusText = "بررسی بروزرسانی به زمان مجاز نرسید.";
|
||||
Logger.Warning("[UPDATE] Latest release check timed out");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!silent)
|
||||
UpdateStatusText = $"بررسی بروزرسانی ناموفق بود: {ex.Message}";
|
||||
Logger.Warning($"[UPDATE] Latest release check failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsCheckingForUpdates = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void PasteConfigFromClipboard()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -102,6 +102,41 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Updates -->
|
||||
<Border Style="{StaticResource CardPanel}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="بروزرسانی"/>
|
||||
<TextBlock Text="{Binding UpdateStatusText}"
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
LineHeight="18"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Vertical"
|
||||
VerticalAlignment="Center"
|
||||
MinWidth="150">
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
Content="{Binding UpdateButtonText}"
|
||||
Command="{Binding CheckForUpdatesCommand}"
|
||||
Padding="14,8"/>
|
||||
<Button Style="{StaticResource PrimaryButton}"
|
||||
Content="باز کردن صفحه انتشار"
|
||||
Command="{Binding OpenLatestReleaseCommand}"
|
||||
Padding="14,8"
|
||||
Margin="0,8,0,0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Quick Path -->
|
||||
<Border Style="{StaticResource CardPanel}">
|
||||
<StackPanel>
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.2.23
|
||||
|
||||
- Added GitHub release checking from the Help tab.
|
||||
- Added automatic tray notification when a newer release is available.
|
||||
- Added tray notifications for connection, disconnection, and connection errors.
|
||||
- Added a tray menu action for checking updates.
|
||||
- Moved remaining future VPN-manager improvements into the public roadmap.
|
||||
|
||||
## 1.2.22
|
||||
|
||||
- Fixed Help page data binding so GitHub and donation buttons work.
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ dotnet publish AppTunnel\AppTunnel.csproj -c Release -r win-x64 --self-contained
|
||||
Rename the final executable with the app version:
|
||||
|
||||
```powershell
|
||||
TunnelX-v1.2.22-standalone-compressed.exe
|
||||
TunnelX-v1.2.23-standalone-compressed.exe
|
||||
```
|
||||
|
||||
## 32-bit Windows
|
||||
|
||||
+35
-1
@@ -16,7 +16,7 @@ Candidate checks:
|
||||
- WinDivert and Wintun native component availability
|
||||
- extraction folder write permissions
|
||||
- route and packet interception readiness
|
||||
- GitHub release/update status
|
||||
- deeper release asset verification for future automatic update flows
|
||||
|
||||
Potential actions:
|
||||
|
||||
@@ -28,3 +28,37 @@ Potential actions:
|
||||
- avoid silent driver or system-level changes
|
||||
|
||||
For public releases, TunnelX should continue to prefer self-contained standalone EXE builds so end users do not need to install the .NET Runtime separately.
|
||||
|
||||
### Profile management
|
||||
|
||||
- import and export connection profiles
|
||||
- clone profiles with clearer naming
|
||||
- per-profile health and last-test status
|
||||
- presets for common workflows such as browser-only, messaging, development, and full VPN
|
||||
|
||||
### Split tunnel rule clarity
|
||||
|
||||
- explicit rule priority between apps, include destinations, and exclude destinations
|
||||
- wildcard and domain-suffix previews
|
||||
- conflict detection when the same destination is both included and excluded
|
||||
- show which rule caused a destination to be routed or bypassed
|
||||
|
||||
### Health dashboard
|
||||
|
||||
- user-facing states: safe, protected, needs attention, and broken
|
||||
- short explanation for the latest important network event
|
||||
- suggested action next to DNS, IPv6, route, and leak status
|
||||
|
||||
### Kill switch
|
||||
|
||||
- optional per-app kill switch when the tunnel core or TUN bridge drops
|
||||
- keep selected apps blocked until the tunnel is restored or the user disconnects
|
||||
|
||||
### Traffic accounting clarity
|
||||
|
||||
- keep tunnel, direct, per-app, DNS, and history counters aligned to one accounting model
|
||||
- expose whether counters are interface-based, rewritten-flow-based, or app-attributed
|
||||
|
||||
### Installer and prerequisite checks
|
||||
|
||||
- optional installer/bootstrapper for shortcuts, uninstall support, prerequisite checks, and future update flow
|
||||
|
||||
Reference in New Issue
Block a user