diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a908ffb2..069889fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# v4.5.520 +### Client +* Feature: Allow to select servers by country if the server supports it +* Feature: Auto pause and resume when the server is not reachable +* Update: User IncludeIpRanges and ExcludeIpRanges in settings instead of CustomIpRanges +* Update: Try to fix accesskey when it is not valid by missing the padding characters +* Update: Countries IP ranges +* Fix: Some wrong message in disconnect + +### Server +* Improve: Optimize server reconfiguration at runtime +* Feature: Support selecting server location when controlled by HttpAccessManager +* Feature: Enable hot restart for FileAccessManager +* Deprecate: ServerProtocol Version 2 (451) is deprecated and no longer supported + # v4.4.506 ### Client * Update: Android: Show Notification & QuickLaunch Request after connect diff --git a/Pub/PubVersion.json b/Pub/PubVersion.json index 54dbf9c7a..49685a544 100644 --- a/Pub/PubVersion.json +++ b/Pub/PubVersion.json @@ -1,6 +1,6 @@ { - "Version": "4.4.506", - "BumpTime": "2024-05-11T09:07:36.0577031Z", + "Version": "4.5.520", + "BumpTime": "2024-05-29T06:25:48.3640756Z", "Prerelease": false, "DeprecatedVersion": "4.0.00" } diff --git a/Pub/PublishApps.ps1 b/Pub/PublishApps.ps1 index db3d1a92e..0ddb90afe 100644 --- a/Pub/PublishApps.ps1 +++ b/Pub/PublishApps.ps1 @@ -3,6 +3,7 @@ param( [Parameter(Mandatory=$true)][object]$nugets, [Parameter(Mandatory=$true)][object]$winClient, [Parameter(Mandatory=$true)][object]$android, + [Parameter(Mandatory=$true)][object]$maui, [Parameter(Mandatory=$true)][object]$server, [Parameter(Mandatory=$true)][object]$distribute, [Parameter(Mandatory=$true)][object]$samples @@ -15,6 +16,7 @@ $distribute = $distribute -eq "1"; $winClient = $winClient -eq "1"; $server = $server -eq "1"; $samples = $samples -eq "1"; +$maui = $maui -eq "1"; . "$PSScriptRoot/Core/Common.ps1" -bump $bump @@ -41,11 +43,15 @@ Remove-Item "$packagesRootDir/ReleaseNote.txt" -ErrorAction Ignore; & "$solutionDir/VpnHood.Client.App.Android.GooglePlay/_publish.ps1"; & "$solutionDir/VpnHood.Client.App.Android.GooglePlay.Core/_publish.ps1"; & "$solutionDir/VpnHood.Client.App.Win.Common/_publish.ps1"; -& "$solutionDir/VpnHood.Client.App.Maui.Common/_publish.ps1"; - & "$solutionDir/VpnHood.Server/_publish.ps1"; & "$solutionDir/VpnHood.Server.Access/_publish.ps1"; +# publish client +if ($maui) +{ + & "$solutionDir/VpnHood.Client.App.Maui.Common/_publish.ps1"; +} + # publish client if ($winClient) { diff --git a/Tests/VpnHood.Test/TestAccessManager.cs b/Tests/VpnHood.Test/TestAccessManager.cs index 3c5a85ddc..4fdc5255a 100644 --- a/Tests/VpnHood.Test/TestAccessManager.cs +++ b/Tests/VpnHood.Test/TestAccessManager.cs @@ -15,6 +15,17 @@ public class TestAccessManager : IAccessManager private readonly object _lockeObject = new(); private readonly HttpAccessManager _httpAccessManager; public int SessionGetCounter { get; private set; } + public DateTime? LastConfigureTime { get; private set; } + public ServerInfo? LastServerInfo { get; private set; } + public ServerStatus? LastServerStatus { get; private set; } + + public TestEmbedIoAccessManager EmbedIoAccessManager { get; } + public IAccessManager BaseAccessManager { get; } + + public bool IsMaintenanceMode => _httpAccessManager.IsMaintenanceMode; + public IPEndPoint? RedirectHostEndPoint { get; set; } + public Dictionary ServerLocations { get; set; } = new(); + public TestAccessManager(IAccessManager baseAccessManager) { @@ -27,15 +38,6 @@ public TestAccessManager(IAccessManager baseAccessManager) }; } - public DateTime? LastConfigureTime { get; private set; } - public ServerInfo? LastServerInfo { get; private set; } - public ServerStatus? LastServerStatus { get; private set; } - - public TestEmbedIoAccessManager EmbedIoAccessManager { get; } - public IAccessManager BaseAccessManager { get; } - - public bool IsMaintenanceMode => _httpAccessManager.IsMaintenanceMode; - public async Task Server_UpdateStatus(ServerStatus serverStatus) { var ret = await _httpAccessManager.Server_UpdateStatus(serverStatus); @@ -59,9 +61,32 @@ public Task Session_Get(ulong sessionId, IPEndPoint hostEndPo return _httpAccessManager.Session_Get(sessionId, hostEndPoint, clientIp); } - public Task Session_Create(SessionRequestEx sessionRequestEx) + public async Task Session_Create(SessionRequestEx sessionRequestEx) { - return _httpAccessManager.Session_Create(sessionRequestEx); + var ret = await _httpAccessManager.Session_Create(sessionRequestEx); + + if (!sessionRequestEx.AllowRedirect) + return ret; + + if (RedirectHostEndPoint != null && + !sessionRequestEx.HostEndPoint.Equals(RedirectHostEndPoint)) + { + ret.RedirectHostEndPoint = RedirectHostEndPoint; + ret.ErrorCode = SessionErrorCode.RedirectHost; + } + + // manage region + if (sessionRequestEx.ServerLocation != null) + { + var redirectEndPoint = ServerLocations[sessionRequestEx.ServerLocation]; + if (!sessionRequestEx.HostEndPoint.Equals(redirectEndPoint)) + { + ret.RedirectHostEndPoint = ServerLocations[sessionRequestEx.ServerLocation]; + ret.ErrorCode = SessionErrorCode.RedirectHost; + } + } + + return ret; } public Task Session_AddUsage(ulong sessionId, Traffic traffic, string? adData) diff --git a/Tests/VpnHood.Test/TestConstants.cs b/Tests/VpnHood.Test/TestConstants.cs index ce4df71af..67cc7d024 100644 --- a/Tests/VpnHood.Test/TestConstants.cs +++ b/Tests/VpnHood.Test/TestConstants.cs @@ -6,8 +6,8 @@ namespace VpnHood.Test; internal class TestConstants { public const int DefaultTimeout = 30000; - public static Uri HttpsUri1 => new("https://www.quad9.net/"); - public static Uri HttpsUri2 => new("https://www.google.com/"); + public static Uri HttpsUri1 => new("https://ipv4.jamieweb.net/"); //make sure always return same ips + public static Uri HttpsUri2 => new("https://ip4.me/"); //make sure always return same ips public static IPEndPoint NsEndPoint1 => IPEndPoint.Parse("1.1.1.1:53"); public static IPEndPoint NsEndPoint2 => IPEndPoint.Parse("1.0.0.1:53"); public static IPEndPoint TcpEndPoint1 => IPEndPoint.Parse("198.18.0.1:80"); diff --git a/Tests/VpnHood.Test/TestEmbedIoAccessManager.cs b/Tests/VpnHood.Test/TestEmbedIoAccessManager.cs index 113c49a24..19629cbb7 100644 --- a/Tests/VpnHood.Test/TestEmbedIoAccessManager.cs +++ b/Tests/VpnHood.Test/TestEmbedIoAccessManager.cs @@ -4,13 +4,16 @@ using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; +using Microsoft.Extensions.Logging; using Swan.Logging; +using VpnHood.Common.Logging; using VpnHood.Common.Messaging; using VpnHood.Common.Utils; using VpnHood.Server.Access; using VpnHood.Server.Access.Configurations; using VpnHood.Server.Access.Managers; using VpnHood.Server.Access.Messaging; +using VpnHood.Tunneling; // ReSharper disable UnusedMember.Local @@ -31,16 +34,18 @@ public TestEmbedIoAccessManager(IAccessManager fileFileAccessManager, bool autoS BaseUri = new Uri($"http://{VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback)}"); _webServer = CreateServer(BaseUri); if (autoStart) + { _webServer.Start(); + VhLogger.Instance.LogInformation(GeneralEventId.Test, $"{VhLogger.FormatType(this)} is listening to {BaseUri}"); + } } public Uri BaseUri { get; } - public IPEndPoint? RedirectHostEndPoint { get; set; } public HttpException? HttpException { get; set; } - public Dictionary Regions { get; set; } = new(); public void Dispose() { + Stop(); _webServer.Dispose(); GC.SuppressFinalize(this); } @@ -60,6 +65,7 @@ private WebServer CreateServer(Uri url) public void Stop() { + VhLogger.Instance.LogInformation(GeneralEventId.Test, $"{VhLogger.FormatType(this)} has stopped listening to {BaseUri}"); _webServer.Dispose(); } @@ -111,28 +117,6 @@ public async Task Session_Create([QueryField] Guid serverId) _ = serverId; var sessionRequestEx = await GetRequestDataAsync(); var res = await AccessManager.Session_Create(sessionRequestEx); - - if (!sessionRequestEx.AllowRedirect) - return res; - - if (embedIoAccessManager.RedirectHostEndPoint != null && - !sessionRequestEx.HostEndPoint.Equals(embedIoAccessManager.RedirectHostEndPoint)) - { - res.RedirectHostEndPoint = embedIoAccessManager.RedirectHostEndPoint; - res.ErrorCode = SessionErrorCode.RedirectHost; - } - - // manage region - if (sessionRequestEx.RegionId != null) - { - var redirectEndPoint = embedIoAccessManager.Regions[sessionRequestEx.RegionId]; - if (!sessionRequestEx.HostEndPoint.Equals(redirectEndPoint)) - { - res.RedirectHostEndPoint = embedIoAccessManager.Regions[sessionRequestEx.RegionId]; - res.ErrorCode = SessionErrorCode.RedirectHost; - } - } - return res; } diff --git a/Tests/VpnHood.Test/TestHelper.cs b/Tests/VpnHood.Test/TestHelper.cs index 8531efe65..bdb04f21c 100644 --- a/Tests/VpnHood.Test/TestHelper.cs +++ b/Tests/VpnHood.Test/TestHelper.cs @@ -1,10 +1,10 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using VpnHood.Client; using VpnHood.Client.App; -using VpnHood.Client.App.Abstractions; using VpnHood.Client.Device; using VpnHood.Client.Diagnosing; using VpnHood.Common; @@ -20,7 +20,6 @@ using VpnHood.Server.Access.Messaging; using VpnHood.Test.Factory; using VpnHood.Tunneling; -using VpnHood.Tunneling.Factory; using ProtocolType = PacketDotNet.ProtocolType; namespace VpnHood.Test; @@ -31,6 +30,8 @@ internal static class TestHelper public class TestAppUiContext : IUiContext; public static TestWebServer WebServer { get; private set; } = default!; public static TestNetFilter NetFilter { get; private set; } = default!; + private static bool LogVerbose => true; + private static int _accessItemIndex; @@ -49,14 +50,22 @@ internal static void Cleanup() } } - public static Task WaitForClientStateAsync(VpnHoodApp app, AppConnectionState connectionSate, int timeout = 5000) + public static Task WaitForAppState(VpnHoodApp app, AppConnectionState connectionSate, int timeout = 5000) { return VhTestUtil.AssertEqualsWait(connectionSate, () => app.State.ConnectionState, "App state didn't reach the expected value.", timeout); } - public static Task WaitForClientStateAsync(VpnHoodClient client, ClientState clientState, int timeout = 6000) + public static Task WaitForClientState(VpnHoodClient client, ClientState clientState, int timeout = 6000, bool useUpdateStatus = false) { - return VhTestUtil.AssertEqualsWait(clientState, () => client.State, "Client state didn't reach the expected value.", timeout); + return VhTestUtil.AssertEqualsWait(clientState, + async () => + { + if (useUpdateStatus) + try { await client.UpdateSessionStatus(); } catch { /*ignore*/ } + return client.State; + }, + "Client state didn't reach the expected value.", + timeout); } private static Task SendPing(Ping? ping = null, IPAddress? ipAddress = null, int timeout = TestConstants.DefaultTimeout) @@ -144,7 +153,8 @@ public static async Task Test_Https(HttpClient? httpClient = default, Uri? { if (throwError) { - Assert.IsTrue(await SendHttpGet(httpClient, uri, timeout), "Https get doesn't work!"); + VhLogger.Instance.LogInformation(GeneralEventId.Test, $"Fetching a test uri. {uri}", uri); + Assert.IsTrue(await SendHttpGet(httpClient, uri, timeout), $"Could not fetch the test uri: {uri}"); return true; } @@ -207,19 +217,26 @@ public static string CreateAccessManagerWorkingDir() return Path.Combine(WorkingPath, $"AccessManager_{Guid.NewGuid()}"); } - public static FileAccessManager CreateFileAccessManager(FileAccessManagerOptions? options = null, string? storagePath = null) + public static FileAccessManager CreateFileAccessManager(FileAccessManagerOptions? options = null, string? storagePath = null, + string? serverLocation = null) { storagePath ??= CreateAccessManagerWorkingDir(); + if (!string.IsNullOrEmpty(serverLocation)) + { + Directory.CreateDirectory(storagePath); + File.WriteAllText(Path.Combine(storagePath, "server_location"), serverLocation); + } + options ??= CreateFileAccessManagerOptions(); return new FileAccessManager(storagePath, options); } - public static FileAccessManagerOptions CreateFileAccessManagerOptions() + public static FileAccessManagerOptions CreateFileAccessManagerOptions(IPEndPoint[]? tcpEndPoints = null) { var options = new FileAccessManagerOptions { PublicEndPoints = null, // use TcpEndPoints - TcpEndPoints = [VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback)], + TcpEndPoints = tcpEndPoints ?? [VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback)], UdpEndPoints = [new IPEndPoint(IPAddress.Loopback, 0)], TrackingOptions = new TrackingOptions { @@ -233,22 +250,23 @@ public static FileAccessManagerOptions CreateFileAccessManagerOptions() SyncCacheSize = 50, SyncInterval = TimeSpan.FromMilliseconds(100) }, - LogAnonymizer = false + LogAnonymizer = false, + UseExternalLocationService = false }; return options; } - public static VpnHoodServer CreateServer(IAccessManager? accessManager = null, bool autoStart = true, TimeSpan? configureInterval = null) + public static Task CreateServer(IAccessManager? accessManager = null, bool autoStart = true, TimeSpan? configureInterval = null) { return CreateServer(accessManager, null, autoStart, configureInterval); } - public static VpnHoodServer CreateServer(FileAccessManagerOptions? options, bool autoStart = true, TimeSpan? configureInterval = null) + public static Task CreateServer(FileAccessManagerOptions? options, bool autoStart = true, TimeSpan? configureInterval = null) { return CreateServer(null, options, autoStart, configureInterval); } - private static VpnHoodServer CreateServer(IAccessManager? accessManager, FileAccessManagerOptions? fileAccessManagerOptions, bool autoStart, + private static async Task CreateServer(IAccessManager? accessManager, FileAccessManagerOptions? fileAccessManagerOptions, bool autoStart, TimeSpan? configureInterval = null) { if (accessManager != null && fileAccessManagerOptions != null) @@ -276,7 +294,7 @@ private static VpnHoodServer CreateServer(IAccessManager? accessManager, FileAcc var server = new VpnHoodServer(accessManager, serverOptions); if (autoStart) { - server.Start().Wait(); + await server.Start(); Assert.AreEqual(ServerState.Ready, server.State); } @@ -338,44 +356,15 @@ public static async Task CreateClient(Token token, return client; } - public static VpnHoodConnect CreateClientConnect(Token token, - IPacketCapture? packetCapture = default, - TestDeviceOptions? deviceOptions = default, - Guid? clientId = default, - bool autoConnect = true, - ClientOptions? clientOptions = default, - ConnectOptions? connectOptions = default) - { - clientOptions ??= new ClientOptions(); - packetCapture ??= CreatePacketCapture(deviceOptions); - clientId ??= Guid.NewGuid(); - if (clientOptions.SessionTimeout == new ClientOptions().SessionTimeout) - clientOptions.SessionTimeout = TimeSpan.FromSeconds(2); //overwrite default timeout - clientOptions.SocketFactory = new SocketFactory(); - clientOptions.PacketCaptureIncludeIpRanges = TestIpAddresses.Select(x => new IpRange(x)).ToArray(); - clientOptions.IncludeLocalNetwork = true; - - var clientConnect = new VpnHoodConnect( - packetCapture, - clientId.Value, - token, - clientOptions, - connectOptions); - - // test starting the client - if (autoConnect) - clientConnect.Connect().Wait(); - - return clientConnect; - } - public static AppOptions CreateClientAppOptions() { var appOptions = new AppOptions { StorageFolderPath = Path.Combine(WorkingPath, "AppData_" + Guid.NewGuid()), SessionTimeout = TimeSpan.FromSeconds(2), - LoadCountryIpGroups = false + UseIpGroupManager = false, + UseExternalLocationService = false, + LogVerbose = LogVerbose }; return appOptions; } @@ -390,9 +379,8 @@ public static VpnHoodApp CreateClientApp(TestDeviceOptions? deviceOptions = defa clientApp.Diagnoser.HttpTimeout = 2000; clientApp.Diagnoser.NsTimeout = 2000; clientApp.UserSettings.PacketCaptureIncludeIpRanges = TestIpAddresses.Select(x => new IpRange(x)).ToArray(); - clientApp.TcpTimeout = TimeSpan.FromSeconds(2); clientApp.UserSettings.Logging.LogAnonymous = false; - clientApp.UserSettings.Logging.LogVerbose = true; + clientApp.TcpTimeout = TimeSpan.FromSeconds(2); clientApp.UiContext = new TestAppUiContext(); return clientApp; @@ -404,7 +392,13 @@ public static SessionRequestEx CreateSessionRequestEx(Token token, Guid? clientI return new SessionRequestEx { TokenId = token.TokenId, - ClientInfo = new ClientInfo { ClientId = clientId.Value }, + ClientInfo = new ClientInfo + { + ClientId = clientId.Value, + UserAgent = "Test", + ClientVersion = "1.0.0", + ProtocolVersion = 4 + }, HostEndPoint = token.ServerToken.HostEndPoints!.First(), EncryptedClientId = VhUtil.EncryptClientId(clientId.Value, token.Secret), ClientIp = null, diff --git a/Tests/VpnHood.Test/TestNullPacketCapture.cs b/Tests/VpnHood.Test/TestNullPacketCapture.cs new file mode 100644 index 000000000..3218b4760 --- /dev/null +++ b/Tests/VpnHood.Test/TestNullPacketCapture.cs @@ -0,0 +1,70 @@ +using System.Net; +using System.Net.Sockets; +using PacketDotNet; +using VpnHood.Client.Device; +using VpnHood.Common.Net; + +namespace VpnHood.Test; + +internal class TestNullPacketCapture : IPacketCapture +{ + public event EventHandler? PacketReceivedFromInbound; + public event EventHandler? Stopped; + public bool Started { get; set; } + public bool IsDnsServersSupported { get; set; } + public IPAddress[]? DnsServers { get; set; } + public bool CanExcludeApps { get; set; } = true; + public bool CanIncludeApps { get; set; } = true; + public string[]? ExcludeApps { get; set; } + public string[]? IncludeApps { get; set; } + public IpNetwork[]? IncludeNetworks { get; set; } + public bool IsMtuSupported { get; set; } = true; + public int Mtu { get; set; } + public bool IsAddIpV6AddressSupported { get; set; } = true; + public bool AddIpV6Address { get; set; } = true; + public bool CanProtectSocket { get; set; } = true; + public bool CanSendPacketToOutbound { get; set; } = false; + public void StartCapture() + { + Started = true; + _ = PacketReceivedFromInbound; //prevent not used warning + } + + public void StopCapture() + { + Started = false; + Stopped?.Invoke(this, EventArgs.Empty); + } + + public void ProtectSocket(Socket socket) + { + // nothing + + } + + public void SendPacketToInbound(IPPacket ipPacket) + { + // nothing + } + + public void SendPacketToInbound(IEnumerable packets) + { + // nothing + } + + public void SendPacketToOutbound(IPPacket ipPacket) + { + // nothing + } + + public void SendPacketToOutbound(IEnumerable ipPackets) + { + // nothing + } + + public void Dispose() + { + StopCapture(); + } + +} \ No newline at end of file diff --git a/Tests/VpnHood.Test/Tests/AccessTest.cs b/Tests/VpnHood.Test/Tests/AccessTest.cs index 35745ae68..da750acca 100644 --- a/Tests/VpnHood.Test/Tests/AccessTest.cs +++ b/Tests/VpnHood.Test/Tests/AccessTest.cs @@ -21,7 +21,7 @@ public async Task Foo() [TestMethod] public async Task Server_reject_invalid_requests() { - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); // ************ // *** TEST ***: request with invalid tokenId @@ -55,7 +55,7 @@ public async Task Server_reject_invalid_requests() [TestMethod] public async Task Server_reject_expired_access_hello() { - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); // create an expired token var token = TestHelper.CreateAccessToken(server, expirationTime: DateTime.Now.AddDays(-1)); @@ -70,7 +70,7 @@ public async Task Server_reject_expired_access_hello() public async Task Server_reject_expired_access_at_runtime() { var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create a short expiring token var accessToken = TestHelper.CreateAccessToken(server, expirationTime: DateTime.Now.AddSeconds(1)); @@ -90,7 +90,7 @@ await VhTestUtil.AssertEqualsWait(ClientState.Disposed, async () => [TestMethod] public async Task Server_reject_trafficOverflow_access() { - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); // create a fast expiring token var accessToken = TestHelper.CreateAccessToken(server, maxTrafficByteCount: 50); @@ -146,53 +146,44 @@ public async Task Server_reject_trafficOverflow_access() [TestMethod] public async Task Server_maxClient_suppress_other_sessions() { - using var packetCapture = TestHelper.CreatePacketCapture(); - // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server, 2); // create default token with 2 client count - await using var client1 = await TestHelper.CreateClient(packetCapture: packetCapture, token: token, - clientId: Guid.NewGuid(), clientOptions: new ClientOptions { AutoDisposePacketCapture = false }); + await using var client1 = await TestHelper.CreateClient(packetCapture: new TestNullPacketCapture(), token: token, + clientId: Guid.NewGuid()); // suppress by yourself - await using var client2 = await TestHelper.CreateClient(packetCapture: packetCapture, token: token, - clientId: client1.ClientId, clientOptions: new ClientOptions { AutoDisposePacketCapture = false }); + await using var client2 = await TestHelper.CreateClient(packetCapture: new TestNullPacketCapture(), token: token, + clientId: client1.ClientId); Assert.AreEqual(SessionSuppressType.YourSelf, client2.SessionStatus.SuppressedTo); Assert.AreEqual(SessionSuppressType.None, client2.SessionStatus.SuppressedBy); // wait for finishing client1 VhLogger.Instance.LogTrace(GeneralEventId.Test, "Test: Waiting for client1 disposal."); - await VhTestUtil.AssertEqualsWait(ClientState.Disposed, async () => - { - await TestHelper.Test_Https(throwError: false, timeout: 2000); - return client1.State; - }, "Client1 has not been stopped yet."); + await TestHelper.WaitForClientState(client1, ClientState.Disposed, useUpdateStatus: true); Assert.AreEqual(SessionSuppressType.None, client1.SessionStatus.SuppressedTo); Assert.AreEqual(SessionSuppressType.YourSelf, client1.SessionStatus.SuppressedBy); // suppress by other (MaxTokenClient is 2) VhLogger.Instance.LogTrace(GeneralEventId.Test, "Test: Creating client3."); - await using var client3 = await TestHelper.CreateClient(packetCapture: packetCapture, token: token, - clientId: Guid.NewGuid(), clientOptions: new ClientOptions { AutoDisposePacketCapture = false }); + await using var client3 = await TestHelper.CreateClient(packetCapture: new TestNullPacketCapture(), token: token, + clientId: Guid.NewGuid()); - await using var client4 = await TestHelper.CreateClient(packetCapture: packetCapture, token: token, - clientId: Guid.NewGuid(), clientOptions: new ClientOptions { AutoDisposePacketCapture = false }); + await using var client4 = await TestHelper.CreateClient(packetCapture: new TestNullPacketCapture(), token: token, + clientId: Guid.NewGuid()); // create a client with another token var accessTokenX = TestHelper.CreateAccessToken(server); - await using var clientX = await TestHelper.CreateClient(packetCapture: packetCapture, clientId: Guid.NewGuid(), - token: accessTokenX, clientOptions: new ClientOptions { AutoDisposePacketCapture = false }); + await using var clientX = await TestHelper.CreateClient(packetCapture: new TestNullPacketCapture(), clientId: Guid.NewGuid(), + token: accessTokenX); // wait for finishing client2 VhLogger.Instance.LogTrace(GeneralEventId.Test, "Test: Waiting for client2 disposal."); - await VhTestUtil.AssertEqualsWait(ClientState.Disposed, async () => - { - await TestHelper.Test_Https(throwError: false, timeout: 2000); - return client2.State; - }); + await TestHelper.WaitForClientState(client2, ClientState.Disposed, useUpdateStatus: true); + Assert.AreEqual(SessionSuppressType.YourSelf, client2.SessionStatus.SuppressedTo); Assert.AreEqual(SessionSuppressType.Other, client2.SessionStatus.SuppressedBy); Assert.AreEqual(SessionSuppressType.None, client3.SessionStatus.SuppressedBy); @@ -208,20 +199,19 @@ public async Task Server_maxClient_should_not_suppress_when_zero() { // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server, 0); // client1 - using var packetCapture = TestHelper.CreatePacketCapture(); - await using var client1 = await TestHelper.CreateClient(packetCapture: packetCapture, token: token, - clientId: Guid.NewGuid(), clientOptions: new ClientOptions { AutoDisposePacketCapture = false }); + await using var client1 = await TestHelper.CreateClient(packetCapture: new TestNullPacketCapture(), token: token, + clientId: Guid.NewGuid()); - await using var client2 = await TestHelper.CreateClient(packetCapture: packetCapture, token: token, - clientId: Guid.NewGuid(), clientOptions: new ClientOptions { AutoDisposePacketCapture = false }); + await using var client2 = await TestHelper.CreateClient(packetCapture: new TestNullPacketCapture(), token: token, + clientId: Guid.NewGuid()); // suppress by yourself - await using var client3 = await TestHelper.CreateClient(packetCapture: packetCapture, token: token, - clientId: Guid.NewGuid(), clientOptions: new ClientOptions { AutoDisposePacketCapture = false }); + await using var client3 = await TestHelper.CreateClient(packetCapture: new TestNullPacketCapture(), token: token, + clientId: Guid.NewGuid()); Assert.AreEqual(SessionSuppressType.None, client3.SessionStatus.SuppressedTo); Assert.AreEqual(SessionSuppressType.None, client3.SessionStatus.SuppressedBy); diff --git a/Tests/VpnHood.Test/Tests/AdTest.cs b/Tests/VpnHood.Test/Tests/AdTest.cs index cd7330cfe..6367085d8 100644 --- a/Tests/VpnHood.Test/Tests/AdTest.cs +++ b/Tests/VpnHood.Test/Tests/AdTest.cs @@ -61,10 +61,10 @@ public async Task flexible_ad_should_not_close_session_if_load_ad_failed() using var fileAccessManager = new AdAccessManager(TestHelper.CreateAccessManagerWorkingDir(), TestHelper.CreateFileAccessManagerOptions()); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // create access item - var accessItem = fileAccessManager.AccessItem_Create(adShow: AdShow.Flexible); + var accessItem = fileAccessManager.AccessItem_Create(adRequirement: AdRequirement.Flexible); accessItem.Token.ToAccessKey(); // create client app @@ -86,10 +86,10 @@ public async Task flexible_ad_should_close_session_if_display_ad_failed() using var fileAccessManager = new AdAccessManager(TestHelper.CreateAccessManagerWorkingDir(), TestHelper.CreateFileAccessManagerOptions()); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // create access item - var accessItem = fileAccessManager.AccessItem_Create(adShow: AdShow.Flexible); + var accessItem = fileAccessManager.AccessItem_Create(adRequirement: AdRequirement.Flexible); accessItem.Token.ToAccessKey(); // create client app @@ -102,7 +102,7 @@ public async Task flexible_ad_should_close_session_if_display_ad_failed() // connect var clientProfile = app.ClientProfileService.ImportAccessKey(accessItem.Token.ToAccessKey()); await Assert.ThrowsExceptionAsync(() => app.Connect(clientProfile.ClientProfileId)); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.None); + await TestHelper.WaitForAppState(app, AppConnectionState.None); } [TestMethod] @@ -112,10 +112,10 @@ public async Task Session_must_be_closed_after_few_minutes_if_ad_is_not_accepted using var fileAccessManager = new AdAccessManager(TestHelper.CreateAccessManagerWorkingDir(), TestHelper.CreateFileAccessManagerOptions()); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // create access item - var accessItem = fileAccessManager.AccessItem_Create(adShow: AdShow.Required); + var accessItem = fileAccessManager.AccessItem_Create(adRequirement: AdRequirement.Required); accessItem.Token.ToAccessKey(); // create client app @@ -128,7 +128,7 @@ public async Task Session_must_be_closed_after_few_minutes_if_ad_is_not_accepted // connect var clientProfile = app.ClientProfileService.ImportAccessKey(accessItem.Token.ToAccessKey()); await Assert.ThrowsExceptionAsync(() => app.Connect(clientProfile.ClientProfileId)); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.None); + await TestHelper.WaitForAppState(app, AppConnectionState.None); } [TestMethod] @@ -138,10 +138,10 @@ public async Task Session_expiration_must_increase_by_ad() using var fileAccessManager = new AdAccessManager(TestHelper.CreateAccessManagerWorkingDir(), TestHelper.CreateFileAccessManagerOptions()); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // create access item - var accessItem = fileAccessManager.AccessItem_Create(adShow: AdShow.Required); + var accessItem = fileAccessManager.AccessItem_Create(adRequirement: AdRequirement.Required); accessItem.Token.ToAccessKey(); // create client app @@ -164,10 +164,10 @@ public async Task Session_exception_should_be_short_if_ad_is_not_accepted() using var fileAccessManager = new AdAccessManager(TestHelper.CreateAccessManagerWorkingDir(), TestHelper.CreateFileAccessManagerOptions()); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // create access item - var accessItem = fileAccessManager.AccessItem_Create(adShow: AdShow.Required); + var accessItem = fileAccessManager.AccessItem_Create(adRequirement: AdRequirement.Required); accessItem.Token.ToAccessKey(); fileAccessManager.RejectAllAds = true; // server will reject all ads diff --git a/Tests/VpnHood.Test/Tests/CheckNewVersionTest.cs b/Tests/VpnHood.Test/Tests/CheckNewVersionTest.cs index 0fa444fda..d59b503fe 100644 --- a/Tests/VpnHood.Test/Tests/CheckNewVersionTest.cs +++ b/Tests/VpnHood.Test/Tests/CheckNewVersionTest.cs @@ -122,7 +122,7 @@ public async Task Current_is_old_before_connect_to_vpn() public async Task Current_is_old_after_connect_to_vpn() { // create server and token - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // Set invalid file version to raise error @@ -140,7 +140,7 @@ public async Task Current_is_old_after_connect_to_vpn() // set new version SetNewRelease(new Version(CurrentAppVersion.Major, CurrentAppVersion.Minor, CurrentAppVersion.Build + 1), DateTime.UtcNow, TimeSpan.Zero); await app.Connect(clientProfile.ClientProfileId); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); await VhTestUtil.AssertEqualsWait(VersionStatus.Old, () => app.State.VersionStatus); } } \ No newline at end of file diff --git a/Tests/VpnHood.Test/Tests/ClientAppTest.cs b/Tests/VpnHood.Test/Tests/ClientAppTest.cs index 4a5727bf8..352268e37 100644 --- a/Tests/VpnHood.Test/Tests/ClientAppTest.cs +++ b/Tests/VpnHood.Test/Tests/ClientAppTest.cs @@ -51,15 +51,15 @@ private Token CreateToken() public async Task BuiltIn_AccessKeys_initialization() { var appOptions = TestHelper.CreateClientAppOptions(); - var tokens = new[] {CreateToken(), CreateToken()}; - appOptions.AccessKeys = tokens.Select(x=>x.ToAccessKey()).ToArray(); - + var tokens = new[] { CreateToken(), CreateToken() }; + appOptions.AccessKeys = tokens.Select(x => x.ToAccessKey()).ToArray(); + await using var app1 = TestHelper.CreateClientApp(appOptions: appOptions); var clientProfiles = app1.ClientProfileService.List(); Assert.AreEqual(tokens.Length, clientProfiles.Length); Assert.AreEqual(tokens[0].TokenId, clientProfiles[0].Token.TokenId); Assert.AreEqual(tokens[1].TokenId, clientProfiles[1].Token.TokenId); - Assert.AreEqual(tokens[0].TokenId, clientProfiles.Single(x=>x.ClientProfileId ==app1.Features.BuiltInClientProfileId).Token.TokenId); + Assert.AreEqual(tokens[0].TokenId, clientProfiles.Single(x => x.ClientProfileId == app1.Features.BuiltInClientProfileId).Token.TokenId); // BuiltIn token should not be removed foreach (var clientProfile in clientProfiles) @@ -87,13 +87,93 @@ public async Task Load_country_ip_groups() // ************ // *** TEST ***: var appOptions = TestHelper.CreateClientAppOptions(); - appOptions.LoadCountryIpGroups = true; + appOptions.UseIpGroupManager = true; await using var app2 = TestHelper.CreateClientApp(appOptions: appOptions); var ipGroups2 = await app2.GetIpGroups(); Assert.IsTrue(ipGroups2.Any(x => x.IpGroupId == "us"), "Countries has not been extracted."); } + [TestMethod] + public async Task ClientProfiles_default_ServerLocation() + { + await using var app = TestHelper.CreateClientApp(); + + // test two region in a same country + var token = CreateToken(); + token.ServerToken.ServerLocations = ["us/regin2", "us/california"]; + var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + app.UserSettings.ClientProfileId = clientProfile.ClientProfileId; + app.UserSettings.ServerLocation = "us/*"; + app.Settings.Save(); + Assert.AreEqual("us/*", app.State.ClientServerLocationInfo?.ServerLocation); + Assert.AreEqual(null, app.UserSettings.ServerLocation); + + // test three regin + token.ServerToken.ServerLocations = ["us/regin2", "us/california", "fr/paris"]; + clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + app.UserSettings.ClientProfileId = clientProfile.ClientProfileId; + app.Settings.Save(); + Assert.AreEqual("*/*", app.State.ClientServerLocationInfo?.ServerLocation); + Assert.AreEqual(null, app.UserSettings.ServerLocation); + } + + + [TestMethod] + public async Task ClientProfiles_ServerLocations() + { + await using var app1 = TestHelper.CreateClientApp(); + + // test two region in a same country + var token = CreateToken(); + token.ServerToken.ServerLocations = ["us", "us/california"]; + var clientProfile = app1.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + var clientProfileInfo = clientProfile.ToInfo(); + var serverLocations = clientProfileInfo.ServerLocationInfos.Select(x => x.ServerLocation).ToArray(); + var i = 0; + Assert.AreEqual("us/*", serverLocations[i++]); + Assert.AreEqual("us/california", serverLocations[i++]); + Assert.IsFalse(clientProfileInfo.ServerLocationInfos[0].IsNestedCountry); + Assert.IsTrue(clientProfileInfo.ServerLocationInfos[0].IsDefault); + Assert.IsTrue(clientProfileInfo.ServerLocationInfos[1].IsNestedCountry); + Assert.IsFalse(clientProfileInfo.ServerLocationInfos[1].IsDefault); + _ = i; + + // test multiple countries + token = CreateToken(); + token.ServerToken.ServerLocations = ["us", "us/california", "uk"]; + clientProfile = app1.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + clientProfileInfo = clientProfile.ToInfo(); + serverLocations = clientProfileInfo.ServerLocationInfos.Select(x => x.ServerLocation).ToArray(); + i = 0; + Assert.AreEqual("*/*", serverLocations[i++]); + Assert.AreEqual("uk/*", serverLocations[i++]); + Assert.AreEqual("us/*", serverLocations[i++]); + Assert.AreEqual("us/california", serverLocations[i++]); + Assert.IsFalse(clientProfileInfo.ServerLocationInfos[0].IsNestedCountry); + Assert.IsTrue(clientProfileInfo.ServerLocationInfos[0].IsDefault); + _ = i; + + // test multiple countries + token = CreateToken(); + token.ServerToken.ServerLocations = ["us/virgina", "us/california", "uk/england", "uk/region2", "uk/england"]; + clientProfile = app1.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + clientProfileInfo = clientProfile.ToInfo(); + serverLocations = clientProfileInfo.ServerLocationInfos.Select(x => x.ServerLocation).ToArray(); + i = 0; + Assert.AreEqual("*/*", serverLocations[i++]); + Assert.AreEqual("uk/*", serverLocations[i++]); + Assert.AreEqual("uk/england", serverLocations[i++]); + Assert.AreEqual("uk/region2", serverLocations[i++]); + Assert.AreEqual("us/*", serverLocations[i++]); + Assert.AreEqual("us/california", serverLocations[i++]); + Assert.AreEqual("us/virgina", serverLocations[i++]); + Assert.IsFalse(clientProfileInfo.ServerLocationInfos[0].IsNestedCountry); + Assert.IsFalse(clientProfileInfo.ServerLocationInfos[1].IsNestedCountry); + Assert.IsTrue(clientProfileInfo.ServerLocationInfos[2].IsNestedCountry); + Assert.IsTrue(clientProfileInfo.ServerLocationInfos[3].IsNestedCountry); + _ = i; + } [TestMethod] public async Task ClientProfiles_CRUD() @@ -103,7 +183,7 @@ public async Task ClientProfiles_CRUD() // ************ // *** TEST ***: AddAccessKey should add a clientProfile var token1 = CreateToken(); - token1.ServerToken.Regions = [new HostRegion{RegionId = "r1", CountryCode = "US"}, new HostRegion { RegionId = "r2", CountryCode = "US" }]; + token1.ServerToken.ServerLocations = ["us", "us/california"]; var clientProfile1 = app.ClientProfileService.ImportAccessKey(token1.ToAccessKey()); Assert.IsNotNull(app.ClientProfileService.FindByTokenId(token1.TokenId), "ClientProfile is not added"); Assert.AreEqual(token1.TokenId, clientProfile1.Token.TokenId, "invalid tokenId has been assigned to clientProfile"); @@ -129,19 +209,7 @@ public async Task ClientProfiles_CRUD() // ReSharper disable once AccessToDisposedClosure app.ClientProfileService.Update(Guid.NewGuid(), new ClientProfileUpdateParams { - ClientProfileName = "Hi" - }); - - }); - - // ************ - // *** TEST ***: Update throw NotExistsException exception if regionId does not exist - Assert.ThrowsException(() => - { - // ReSharper disable once AccessToDisposedClosure - app.ClientProfileService.Update(Guid.NewGuid(), new ClientProfileUpdateParams - { - RegionId = Guid.NewGuid().ToString() + ClientProfileName = "Hi" }); }); @@ -151,10 +219,10 @@ public async Task ClientProfiles_CRUD() app.ClientProfileService.Update(clientProfile1.ClientProfileId, new ClientProfileUpdateParams { ClientProfileName = "Hi2", - RegionId = "r2" + IsFavorite = true }); Assert.AreEqual("Hi2", app.ClientProfileService.Get(clientProfile1.ClientProfileId).ClientProfileName); - Assert.AreEqual("r2", app.ClientProfileService.Get(clientProfile1.ClientProfileId).RegionId); + Assert.IsTrue(app.ClientProfileService.Get(clientProfile1.ClientProfileId).IsFavorite); // ************ // *** TEST ***: RemoveClientProfile @@ -165,19 +233,19 @@ public async Task ClientProfiles_CRUD() [TestMethod] public async Task Save_load_clientProfiles() { - await using var app = TestHelper.CreateClientApp(); + await using var app1 = TestHelper.CreateClientApp(); var token1 = CreateToken(); - var clientProfile1 = app.ClientProfileService.ImportAccessKey(token1.ToAccessKey()); + var clientProfile1 = app1.ClientProfileService.ImportAccessKey(token1.ToAccessKey()); var token2 = CreateToken(); - var clientProfile2 = app.ClientProfileService.ImportAccessKey(token2.ToAccessKey()); + var clientProfile2 = app1.ClientProfileService.ImportAccessKey(token2.ToAccessKey()); - var clientProfiles = app.ClientProfileService.List(); - await app.DisposeAsync(); + var clientProfiles = app1.ClientProfileService.List(); + await app1.DisposeAsync(); var appOptions = TestHelper.CreateClientAppOptions(); - appOptions.StorageFolderPath = app.StorageFolderPath; + appOptions.StorageFolderPath = app1.StorageFolderPath; await using var app2 = TestHelper.CreateClientApp(appOptions: appOptions); Assert.AreEqual(clientProfiles.Length, app2.ClientProfileService.List().Length, "ClientProfiles count are not same!"); @@ -191,7 +259,7 @@ public async Task Save_load_clientProfiles() public async Task State_Diagnose_info() { // create server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // create app @@ -200,11 +268,11 @@ public async Task State_Diagnose_info() // ************ // Test: With diagnose - _ = app.Connect(clientProfile1.ClientProfileId, diagnose: true); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected, 10000); + await app.Connect(clientProfile1.ClientProfileId, diagnose: true); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected, 10000); app.ClearLastError(); // should not affect await app.Disconnect(true); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.None); + await TestHelper.WaitForAppState(app, AppConnectionState.None); Assert.IsTrue(app.State.LogExists); Assert.IsTrue(app.State.HasDiagnoseStarted); @@ -219,52 +287,81 @@ public async Task State_Diagnose_info() // ************ // Test: Without diagnose - // ReSharper disable once RedundantAssignment - _ = app.Connect(clientProfile1.ClientProfileId); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected); + await app.Connect(clientProfile1.ClientProfileId); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); await app.Disconnect(true); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.None); + await TestHelper.WaitForAppState(app, AppConnectionState.None); - Assert.IsFalse(app.State.LogExists); + Assert.IsTrue(app.State.IsIdle); Assert.IsFalse(app.State.HasDiagnoseStarted); Assert.IsTrue(app.State.HasDisconnectedByUser); - Assert.IsTrue(app.State.HasProblemDetected); //no data - Assert.IsTrue(app.State.IsIdle); + Assert.IsFalse(app.State.LogExists); } [TestMethod] public async Task State_Error_InConnecting() { // create server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); token.ServerToken.HostEndPoints = [IPEndPoint.Parse("10.10.10.99:443")]; // create app await using var app = TestHelper.CreateClientApp(); var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + await Assert.ThrowsExceptionAsync(() => app.Connect(clientProfile.ClientProfileId)); - try - { - await app.Connect(clientProfile.ClientProfileId); - } - catch - { - // ignored - } - - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.None); + await TestHelper.WaitForAppState(app, AppConnectionState.None); Assert.IsFalse(app.State.LogExists); Assert.IsFalse(app.State.HasDiagnoseStarted); Assert.IsTrue(app.State.HasProblemDetected); Assert.IsNotNull(app.State.LastError); } + + [TestMethod] + public async Task State_Waiting() + { + // create Access Manager and token + using var fileAccessManager = TestHelper.CreateFileAccessManager(); + using var testAccessManager = new TestAccessManager(fileAccessManager); + var token = TestHelper.CreateAccessToken(fileAccessManager); + + // create server + await using var server1 = await TestHelper.CreateServer(testAccessManager); + + // create app & connect + var appOptions = TestHelper.CreateClientAppOptions(); + appOptions.SessionTimeout = TimeSpan.FromSeconds(20); + appOptions.ReconnectTimeout = TimeSpan.FromSeconds(1); + appOptions.AutoWaitTimeout = TimeSpan.FromSeconds(2); + await using var app = TestHelper.CreateClientApp(appOptions: appOptions); + var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + await app.Connect(clientProfile.ClientProfileId); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); + + // dispose server and wait for waiting state + await server1.DisposeAsync(); + await VhTestUtil.AssertEqualsWait(AppConnectionState.Waiting, async () => + { + await TestHelper.Test_Https(throwError: false, timeout: 100); + return app.State.ConnectionState; + }); + + // start a new server & waiting for connected state + await using var server2 = await TestHelper.CreateServer(testAccessManager); + await VhTestUtil.AssertEqualsWait(AppConnectionState.Connected, async () => + { + await TestHelper.Test_Https(throwError: false, timeout: 100); + return app.State.ConnectionState; + }); + } + [TestMethod] public async Task Set_DnsServer_to_packetCapture() { // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // create app @@ -272,7 +369,7 @@ public async Task Set_DnsServer_to_packetCapture() Assert.IsTrue(packetCapture.DnsServers == null || packetCapture.DnsServers.Length == 0); await using var client = await TestHelper.CreateClient(token, packetCapture); - await TestHelper.WaitForClientStateAsync(client, ClientState.Connected); + await TestHelper.WaitForClientState(client, ClientState.Connected); Assert.IsTrue(packetCapture.DnsServers is { Length: > 0 }); } @@ -288,7 +385,7 @@ public async Task IpFilters(bool usePassthru, bool isDnsServerSupported) !isDnsServerSupported; //dns will work as normal UDP when DnsServerSupported, otherwise it should be redirected // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // create app @@ -301,7 +398,7 @@ public async Task IpFilters(bool usePassthru, bool isDnsServerSupported) await using var app = TestHelper.CreateClientApp(deviceOptions: deviceOptions); var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); - var ipList = (await Dns.GetHostAddressesAsync(TestConstants.HttpsUri1.Host)) + var customIps = (await Dns.GetHostAddressesAsync(TestConstants.HttpsUri1.Host)) .Select(x => new IpRange(x)) .Concat(new[] { @@ -309,15 +406,15 @@ public async Task IpFilters(bool usePassthru, bool isDnsServerSupported) new IpRange(TestConstants.NsEndPoint1.Address), new IpRange(TestConstants.UdpV4EndPoint1.Address), new IpRange(TestConstants.UdpV6EndPoint1.Address) - }); + }) + .ToArray(); // ************ // *** TEST ***: Test Include ip filter - app.UserSettings.CustomIpRanges = ipList.ToArray(); - app.UserSettings.IpGroupFilters = ["custom"]; - app.UserSettings.IpGroupFiltersMode = FilterMode.Include; + app.UserSettings.IncludeIpRanges = customIps; + app.UserSettings.ExcludeIpRanges = null; await app.Connect(clientProfile.ClientProfileId); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); await TestHelper.Test_Ping(ipAddress: TestConstants.PingV4Address1); await IpFilters_TestInclude(app, testPing: usePassthru, testUdp: true, testDns: testDns); @@ -325,11 +422,13 @@ public async Task IpFilters(bool usePassthru, bool isDnsServerSupported) // ************ // *** TEST ***: Test Exclude ip filters - app.UserSettings.IpGroupFiltersMode = FilterMode.Exclude; + app.UserSettings.IncludeIpRanges = null; + app.UserSettings.ExcludeIpRanges = customIps; await app.Connect(clientProfile.ClientProfileId); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); await IpFilters_TestExclude(app, testPing: usePassthru, testUdp: true, testDns: testDns); + await app.Disconnect(); } public static async Task IpFilters_TestInclude(VpnHoodApp app, bool testUdp, bool testPing, bool testDns) @@ -474,7 +573,7 @@ public static async Task IpFilters_TestExclude(VpnHoodApp app, bool testUdp, boo public async Task State_Connected_Disconnected_successfully() { // create server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // create app @@ -482,7 +581,7 @@ public async Task State_Connected_Disconnected_successfully() var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); _ = app.Connect(clientProfile.ClientProfileId); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); // get data through tunnel await TestHelper.Test_Https(); @@ -495,7 +594,7 @@ public async Task State_Connected_Disconnected_successfully() // test disconnect await app.Disconnect(); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.None); + await TestHelper.WaitForAppState(app, AppConnectionState.None); } [TestMethod] @@ -513,13 +612,13 @@ public async Task update_server_token_url_from_server() fileAccessManager.ClearCache(); // create server and app - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); await using var app = TestHelper.CreateClientApp(); var clientProfile1 = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); // wait for connect await app.Connect(clientProfile1.ClientProfileId); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); Assert.AreEqual(fileAccessManager.ServerConfig.ServerTokenUrl, app.ClientProfileService.GetToken(token.TokenId).ServerToken.Url); CollectionAssert.AreEqual(fileAccessManager.ServerConfig.ServerSecret, app.ClientProfileService.GetToken(token.TokenId).ServerToken.Secret); @@ -539,7 +638,7 @@ public async Task update_server_token_from_server_token_url() fileAccessManagerOptions1.ServerTokenUrl = $"http://{endPoint}/accesskey"; using var fileAccessManager1 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions1); using var testAccessManager1 = new TestAccessManager(fileAccessManager1); - await using var server1 = TestHelper.CreateServer(testAccessManager1); + await using var server1 = await TestHelper.CreateServer(testAccessManager1); var token1 = TestHelper.CreateAccessToken(server1); await server1.DisposeAsync(); @@ -548,7 +647,7 @@ public async Task update_server_token_from_server_token_url() fileAccessManagerOptions1.TcpEndPoints = [VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback, tcpEndPoint.Port + 1)]; var fileAccessManager2 = TestHelper.CreateFileAccessManager(storagePath: fileAccessManager1.StoragePath, options: fileAccessManagerOptions1); using var testAccessManager2 = new TestAccessManager(fileAccessManager2); - await using var server2 = TestHelper.CreateServer(testAccessManager2); + await using var server2 = await TestHelper.CreateServer(testAccessManager2); var token2 = TestHelper.CreateAccessToken(server2); //update web server enc_server_token @@ -574,8 +673,8 @@ public async Task update_server_token_from_server_token_url() [TestMethod] public async Task Change_server_while_connected() { - await using var server1 = TestHelper.CreateServer(); - await using var server2 = TestHelper.CreateServer(); + await using var server1 = await TestHelper.CreateServer(); + await using var server2 = await TestHelper.CreateServer(); var token1 = TestHelper.CreateAccessToken(server1); var token2 = TestHelper.CreateAccessToken(server2); @@ -586,10 +685,10 @@ public async Task Change_server_while_connected() var clientProfile2 = app.ClientProfileService.ImportAccessKey(token2.ToAccessKey()); await app.Connect(clientProfile1.ClientProfileId); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); await app.Connect(clientProfile2.ClientProfileId); - await TestHelper.WaitForClientStateAsync(app, AppConnectionState.Connected); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); Assert.AreEqual(AppConnectionState.Connected, app.State.ConnectionState, "Client connection has not been changed!"); diff --git a/Tests/VpnHood.Test/Tests/ClientServerTest.cs b/Tests/VpnHood.Test/Tests/ClientServerTest.cs index 7e1fb46ac..ddbc49a13 100644 --- a/Tests/VpnHood.Test/Tests/ClientServerTest.cs +++ b/Tests/VpnHood.Test/Tests/ClientServerTest.cs @@ -10,7 +10,6 @@ using VpnHood.Common.Messaging; using VpnHood.Common.Utils; using VpnHood.Server; -using VpnHood.Server.Access.Managers.File; using VpnHood.Tunneling; // ReSharper disable DisposeOnUsingVariable @@ -27,17 +26,18 @@ public async Task Redirect_Server() var fileAccessManagerOptions1 = TestHelper.CreateFileAccessManagerOptions(); using var fileAccessManager1 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions1); using var testAccessManager1 = new TestAccessManager(fileAccessManager1); - await using var server1 = TestHelper.CreateServer(testAccessManager1); + await using var server1 = await TestHelper.CreateServer(testAccessManager1); // Create Server 2 var serverEndPoint2 = VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback); - var fileAccessManagerOptions2 = new FileAccessManagerOptions { TcpEndPoints = [serverEndPoint2] }; + var fileAccessManagerOptions2 = TestHelper.CreateFileAccessManagerOptions(); + fileAccessManagerOptions2.TcpEndPoints = [serverEndPoint2]; using var fileAccessManager2 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions2, fileAccessManager1.StoragePath); using var testAccessManager2 = new TestAccessManager(fileAccessManager2); - await using var server2 = TestHelper.CreateServer(testAccessManager2); + await using var server2 = await TestHelper.CreateServer(testAccessManager2); // redirect server1 to server2 - testAccessManager1.EmbedIoAccessManager.RedirectHostEndPoint = serverEndPoint2; + testAccessManager1.RedirectHostEndPoint = serverEndPoint2; // Create Client var token1 = TestHelper.CreateAccessToken(fileAccessManager1); @@ -48,34 +48,53 @@ public async Task Redirect_Server() } [TestMethod] - public async Task Redirect_Server_For_Region() + public async Task Redirect_Server_By_ServerLocation() { // Create Server 1 - var fileAccessManagerOptions1 = TestHelper.CreateFileAccessManagerOptions(); - using var fileAccessManager1 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions1); + var serverEndPoint1 = VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback); + var fileAccessManagerOptions1 = TestHelper.CreateFileAccessManagerOptions(tcpEndPoints: [serverEndPoint1]); + using var fileAccessManager1 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions1, serverLocation: "us/california"); using var testAccessManager1 = new TestAccessManager(fileAccessManager1); - await using var server1 = TestHelper.CreateServer(testAccessManager1); + await using var server1 = await TestHelper.CreateServer(testAccessManager1); // Create Server 2 var serverEndPoint2 = VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback); - var fileAccessManagerOptions2 = new FileAccessManagerOptions { TcpEndPoints = [serverEndPoint2] }; - using var fileAccessManager2 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions2, fileAccessManager1.StoragePath); + var fileAccessManagerOptions2 = TestHelper.CreateFileAccessManagerOptions(tcpEndPoints: [serverEndPoint2]); + using var fileAccessManager2 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions2, fileAccessManager1.StoragePath, serverLocation: "uk/london"); using var testAccessManager2 = new TestAccessManager(fileAccessManager2); - await using var server2 = TestHelper.CreateServer(testAccessManager2); + await using var server2 = await TestHelper.CreateServer(testAccessManager2); // redirect server1 to server2 - testAccessManager1.EmbedIoAccessManager.Regions.Add("r1", serverEndPoint2); + testAccessManager1.ServerLocations.Add("us/california", serverEndPoint1); + testAccessManager1.ServerLocations.Add("uk/london", serverEndPoint2); // Create Client var token1 = TestHelper.CreateAccessToken(fileAccessManager1); var clientOptions = TestHelper.CreateClientOptions(); - clientOptions.RegionId = "r1"; - await using var client = await TestHelper.CreateClient(token1, clientOptions: clientOptions); - await TestHelper.Test_Https(); + clientOptions.ServerLocation = "uk/london"; + await using var client = await TestHelper.CreateClient(token1, clientOptions: clientOptions, packetCapture: new TestNullPacketCapture()); Assert.AreEqual(serverEndPoint2, client.HostTcpEndPoint); + Assert.AreEqual("uk/london", client.Stat.ServerLocationInfo?.ServerLocation); + } + + [TestMethod] + public async Task Client_must_update_ServerLocation_from_access_manager() + { + // Create Server + var fileAccessManagerOptions1 = TestHelper.CreateFileAccessManagerOptions(); + using var fileAccessManager1 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions1, serverLocation: "us/california"); + using var testAccessManager1 = new TestAccessManager(fileAccessManager1); + + await using var server1 = await TestHelper.CreateServer(testAccessManager1); + + // create client + var token1 = TestHelper.CreateAccessToken(fileAccessManager1); + await using var client = await TestHelper.CreateClient(token1, packetCapture: new TestNullPacketCapture()); + Assert.AreEqual("us/california", client.Stat.ServerLocationInfo?.ServerLocation); } + [TestMethod] public async Task TcpChannel() { @@ -87,7 +106,7 @@ public async Task TcpChannel() using var fileAccessManager = TestHelper.CreateFileAccessManager(fileAccessManagerOptions); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); var token = TestHelper.CreateAccessToken(server); // Create Client @@ -110,7 +129,7 @@ public async Task TcpChannel() public async Task UdpPackets_Drop() { // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); await using var client = await TestHelper.CreateClient(token, clientOptions: new ClientOptions @@ -139,7 +158,7 @@ public async Task MaxDatagramChannels() fileAccessManagerOptions.SessionOptions.MaxDatagramChannelCount = 3; // Create Server - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); var token = TestHelper.CreateAccessToken(server); // -------- @@ -187,7 +206,7 @@ public async Task MaxDatagramChannels() public async Task UnsupportedClient() { // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // Create Client @@ -198,7 +217,7 @@ public async Task UnsupportedClient() public async Task DatagramChannel_Stream() { // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // Create Client @@ -221,7 +240,7 @@ public async Task DatagramChannel_Udp() VhLogger.IsDiagnoseMode = true; // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // Create Client @@ -244,7 +263,7 @@ public async Task UdpChannel() VhLogger.IsDiagnoseMode = true; // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // Create Client @@ -276,7 +295,7 @@ public async Task UdpChannel_custom_udp_port() .Select(x => VhUtil.GetFreeUdpEndPoint(x.Address)).ToArray(); // Create Server - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); var token = TestHelper.CreateAccessToken(server); // Create Client @@ -369,20 +388,20 @@ await httpClient.GetStringAsync($"http://{TestConstants.NsEndPoint1}", [TestMethod] public async Task Client_must_dispose_after_device_closed() { - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); using var packetCapture = TestHelper.CreatePacketCapture(); await using var client = await TestHelper.CreateClient(token, packetCapture); packetCapture.StopCapture(); - await TestHelper.WaitForClientStateAsync(client, ClientState.Disposed); + await TestHelper.WaitForClientState(client, ClientState.Disposed); } [TestMethod] public async Task Client_must_dispose_after_server_stopped() { - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // create client @@ -410,7 +429,7 @@ public async Task Client_must_dispose_after_server_stopped() /* ignored */ } - await TestHelper.WaitForClientStateAsync(client, ClientState.Disposed); + await TestHelper.WaitForClientState(client, ClientState.Disposed); await client.DisposeAsync(); } @@ -424,7 +443,7 @@ public async Task Datagram_channel_after_client_reconnection() using var fileAccessManager = TestHelper.CreateFileAccessManager(); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); var token = TestHelper.CreateAccessToken(server); // create client @@ -448,7 +467,7 @@ public async Task Datagram_channel_after_client_reconnection() public async Task Reset_tcp_connection_immediately_after_vpn_connected() { // create server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); using TcpClient tcpClient = new(TestConstants.HttpsUri1.Host, 443); @@ -461,6 +480,7 @@ public async Task Reset_tcp_connection_immediately_after_vpn_connected() { stream.WriteByte(1); stream.ReadByte(); + Assert.Fail("Exception expected!"); } catch (Exception ex) when (ex.InnerException is SocketException { SocketErrorCode: SocketError.ConnectionReset }) @@ -476,7 +496,7 @@ public async Task Disconnect_if_session_expired() using var testAccessManager = new TestAccessManager(fileAccessManager); // create server - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); var token = TestHelper.CreateAccessToken(server); // connect @@ -501,7 +521,7 @@ await VhTestUtil.AssertEqualsWait(false, // ignored } - await TestHelper.WaitForClientStateAsync(client, ClientState.Disposed); + await TestHelper.WaitForClientState(client, ClientState.Disposed); } [TestMethod] @@ -512,7 +532,7 @@ public async Task Configure_Maintenance_Server() // -------- using var fileAccessManager = TestHelper.CreateFileAccessManager(); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); Assert.IsFalse(server.AccessManager.IsMaintenanceMode); Assert.AreEqual(Environment.Version, fileAccessManager.ServerInfo?.EnvironmentVersion); @@ -524,14 +544,14 @@ public async Task Configure_Maintenance_Server() // Check: AccessManager is off at start // ------------ testAccessManager.EmbedIoAccessManager.Stop(); - await using var server2 = TestHelper.CreateServer(testAccessManager, false); + await using var server2 = await TestHelper.CreateServer(testAccessManager, false); await server2.Start(); // ---------- // Check: MaintenanceMode is expected // ---------- var token = TestHelper.CreateAccessToken(fileAccessManager); - await using var client = await TestHelper.CreateClient(token, autoConnect: false); + await using var client = await TestHelper.CreateClient(token, autoConnect: false, packetCapture: new TestNullPacketCapture()); await Assert.ThrowsExceptionAsync(() => client.Connect()); Assert.AreEqual(SessionErrorCode.Maintenance, client.SessionStatus.ErrorCode); @@ -541,86 +561,42 @@ public async Task Configure_Maintenance_Server() // Check: Connect after Maintenance is done // ---------- testAccessManager.EmbedIoAccessManager.Start(); - await using var client2 = await TestHelper.CreateClient(token); - await TestHelper.WaitForClientStateAsync(client2, ClientState.Connected); + await using var client2 = await TestHelper.CreateClient(token, packetCapture: new TestNullPacketCapture()); + await TestHelper.WaitForClientState(client2, ClientState.Connected); // ---------- // Check: Go Maintenance mode after server started by stopping the server // ---------- testAccessManager.EmbedIoAccessManager.Stop(); - await using var client3 = await TestHelper.CreateClient(token, autoConnect: false); + await using var client3 = await TestHelper.CreateClient(token, autoConnect: false, packetCapture: new TestNullPacketCapture()); await Assert.ThrowsExceptionAsync(() => client3.Connect()); - await TestHelper.WaitForClientStateAsync(client3, ClientState.Disposed); + await TestHelper.WaitForClientState(client3, ClientState.Disposed); Assert.AreEqual(SessionErrorCode.Maintenance, client3.SessionStatus.ErrorCode); // ---------- // Check: Connect after Maintenance is done // ---------- testAccessManager.EmbedIoAccessManager.Start(); - await using var client4 = await TestHelper.CreateClient(token); - await TestHelper.WaitForClientStateAsync(client4, ClientState.Connected); + await using var client4 = await TestHelper.CreateClient(token, packetCapture: new TestNullPacketCapture()); + await TestHelper.WaitForClientState(client4, ClientState.Connected); // ---------- // Check: Go Maintenance mode by replying 404 from access-server // ---------- testAccessManager.EmbedIoAccessManager.HttpException = HttpException.Forbidden(); - await using var client5 = await TestHelper.CreateClient(token, autoConnect: false); + await using var client5 = await TestHelper.CreateClient(token, autoConnect: false, packetCapture: new TestNullPacketCapture()); await Assert.ThrowsExceptionAsync(() => client5.Connect()); - await TestHelper.WaitForClientStateAsync(client5, ClientState.Disposed); + await TestHelper.WaitForClientState(client5, ClientState.Disposed); Assert.AreEqual(SessionErrorCode.Maintenance, client5.SessionStatus.ErrorCode); // ---------- // Check: Connect after Maintenance is done // ---------- testAccessManager.EmbedIoAccessManager.HttpException = null; - await using var client6 = await TestHelper.CreateClient(token); - await TestHelper.WaitForClientStateAsync(client6, ClientState.Connected); - } - - [TestMethod] - public async Task AutoReconnect() - { - using var httpClient = new HttpClient(); - - // create server - using var fileAccessManager = TestHelper.CreateFileAccessManager(); - using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); - - // create client - var token = TestHelper.CreateAccessToken(server); - - // ----------- - // Check: Reconnect after disconnection (1st time) - // ----------- - await using var clientConnect = TestHelper.CreateClientConnect(token, - connectOptions: new ConnectOptions { MaxReconnectCount = 1, ReconnectDelay = TimeSpan.Zero }); - Assert.AreEqual(ClientState.Connected, clientConnect.Client.State); // checkpoint - await TestHelper.Test_Https(); //let transfer something - - fileAccessManager.SessionController.Sessions.TryRemove(clientConnect.Client.SessionId, out _); - server.SessionManager.Sessions.TryRemove(clientConnect.Client.SessionId, out _); - await VhTestUtil.AssertEqualsWait(ClientState.Connected, async () => - { - await TestHelper.Test_Https(throwError: false); - return clientConnect.Client.State; - }, timeout: 30_000); - Assert.AreEqual(1, clientConnect.AttemptCount); - await TestTunnel(server, clientConnect.Client); - - // ----------- - // Check: dispose after second try (2nd time) - // ----------- - Assert.AreEqual(ClientState.Connected, clientConnect.Client.State); // checkpoint - await server.SessionManager.CloseSession(clientConnect.Client.SessionId); - await VhTestUtil.AssertEqualsWait(ClientState.Disposed, async () => - { - await TestHelper.Test_Https(throwError: false); - return clientConnect.Client.State; - }, timeout: 30_000); - Assert.AreEqual(1, clientConnect.AttemptCount); + await using var client6 = await TestHelper.CreateClient(token, packetCapture: new TestNullPacketCapture()); + await TestHelper.WaitForClientState(client6, ClientState.Connected); } #if DEBUG @@ -628,26 +604,15 @@ await VhTestUtil.AssertEqualsWait(ClientState.Disposed, async () => public async Task Disconnect_for_unsupported_client() { // create server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // create client await using var client = await TestHelper.CreateClient(token, autoConnect: false, clientOptions: new ClientOptions { ProtocolVersion = 1 }); - try - { - _ = client.Connect(); - await TestHelper.WaitForClientStateAsync(client, ClientState.Disposed); - } - catch - { - // ignored - } - finally - { - Assert.AreEqual(SessionErrorCode.UnsupportedClient, client.SessionStatus.ErrorCode); - } + await Assert.ThrowsExceptionAsync(() => client.Connect()); + Assert.AreEqual(SessionErrorCode.UnsupportedClient, client.SessionStatus.ErrorCode); } #endif @@ -657,7 +622,7 @@ public async Task Server_limit_by_Max_TcpConnectWait() // create access server var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); fileAccessManagerOptions.SessionOptions.MaxTcpConnectWaitCount = 2; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); @@ -680,7 +645,7 @@ public async Task Server_limit_by_Max_TcpChannel() // create access server var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); fileAccessManagerOptions.SessionOptions.MaxTcpChannelCount = 2; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); @@ -708,7 +673,7 @@ public async Task Server_limit_by_Max_TcpChannel() public async Task Reusing_ChunkStream() { // Create Server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); // Create Client @@ -778,7 +743,7 @@ public async Task IsUdpChannelSupported_must_be_false_when_server_return_udp_por // Create Server var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); fileAccessManagerOptions.UdpEndPoints = []; - await using var server = TestHelper.CreateServer(options: fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(options: fileAccessManagerOptions); var token = TestHelper.CreateAccessToken(server); // Create Client diff --git a/Tests/VpnHood.Test/Tests/DiagnoserTest.cs b/Tests/VpnHood.Test/Tests/DiagnoserTest.cs index 15c4eda8f..1d0897237 100644 --- a/Tests/VpnHood.Test/Tests/DiagnoserTest.cs +++ b/Tests/VpnHood.Test/Tests/DiagnoserTest.cs @@ -10,7 +10,7 @@ public class DiagnoserTest : TestBase public async Task NormalConnect_NoInternet() { // create server - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); token.ServerToken.HostEndPoints = [TestConstants.InvalidEp]; diff --git a/Tests/VpnHood.Test/Tests/DnsConfigurationTest.cs b/Tests/VpnHood.Test/Tests/DnsConfigurationTest.cs index 2f9e37b7d..1ab989e38 100644 --- a/Tests/VpnHood.Test/Tests/DnsConfigurationTest.cs +++ b/Tests/VpnHood.Test/Tests/DnsConfigurationTest.cs @@ -14,7 +14,7 @@ public async Task Server_specify_dns_servers() // create server var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); fileAccessManagerOptions.DnsServers = [IPAddress.Parse("1.1.1.1"), IPAddress.Parse("1.1.1.2")]; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); @@ -30,7 +30,7 @@ public async Task Client_specify_dns_servers() // create server var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); fileAccessManagerOptions.DnsServers = [IPAddress.Parse("1.1.1.1"), IPAddress.Parse("1.1.1.2")]; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); @@ -54,7 +54,7 @@ public async Task Server_override_dns_servers() { ExcludeIpRanges = clientDnsServers.Select(x => new IpRange(x)).ToArray() }; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); @@ -78,7 +78,7 @@ public async Task Server_should_not_block_own_dns_servers() { IncludeIpRanges = [IpRange.Parse("10.10.10.10-10.10.10.11")] }; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); diff --git a/Tests/VpnHood.Test/Tests/FileAccessServerTest.cs b/Tests/VpnHood.Test/Tests/FileAccessServerTest.cs index 75bdae624..5895f3f78 100644 --- a/Tests/VpnHood.Test/Tests/FileAccessServerTest.cs +++ b/Tests/VpnHood.Test/Tests/FileAccessServerTest.cs @@ -16,7 +16,7 @@ public async Task Create_access_token_with_valid_domain() var fileAccessManager = TestHelper.CreateFileAccessManager(options); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); var accessItem = fileAccessManager.AccessItem_Create(); Assert.AreEqual(fileAccessManager.ServerConfig.TcpEndPointsValue.First().Port, accessItem.Token.ServerToken.HostPort); @@ -28,11 +28,9 @@ public async Task Create_access_token_with_valid_domain() public async Task Crud() { var storagePath = Path.Combine(TestHelper.WorkingPath, Guid.NewGuid().ToString()); - var fileAccessManagerOptions = new FileAccessManagerOptions - { - TcpEndPoints = [new IPEndPoint(IPAddress.Any, 8000)], - PublicEndPoints = [IPEndPoint.Parse("127.0.0.1:8000")] - }; + var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); + fileAccessManagerOptions.TcpEndPoints = [new IPEndPoint(IPAddress.Any, 8000)]; + fileAccessManagerOptions.PublicEndPoints = [IPEndPoint.Parse("127.0.0.1:8000")]; var accessManager1 = new FileAccessManager(storagePath, fileAccessManagerOptions); //add two tokens diff --git a/Tests/VpnHood.Test/Tests/NetProtectTest.cs b/Tests/VpnHood.Test/Tests/NetProtectTest.cs index 66cdabe6a..a5762297f 100644 --- a/Tests/VpnHood.Test/Tests/NetProtectTest.cs +++ b/Tests/VpnHood.Test/Tests/NetProtectTest.cs @@ -12,7 +12,7 @@ public async Task MaxTcpWaitConnect_reject() var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); fileAccessManagerOptions.SessionOptions.TcpConnectTimeout = TimeSpan.FromSeconds(1); fileAccessManagerOptions.SessionOptions.MaxTcpConnectWaitCount = 0; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); @@ -36,7 +36,7 @@ public async Task MaxTcpWaitConnect_accept() var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); fileAccessManagerOptions.SessionOptions.TcpConnectTimeout = TimeSpan.FromSeconds(1); fileAccessManagerOptions.SessionOptions.MaxTcpConnectWaitCount = 1; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); diff --git a/Tests/VpnHood.Test/Tests/NetScanTest.cs b/Tests/VpnHood.Test/Tests/NetScanTest.cs index 9c59607eb..43a757c60 100644 --- a/Tests/VpnHood.Test/Tests/NetScanTest.cs +++ b/Tests/VpnHood.Test/Tests/NetScanTest.cs @@ -17,7 +17,7 @@ public async Task Reject_by_server() var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); fileAccessManagerOptions.SessionOptions.NetScanTimeout = TimeSpan.FromSeconds(100); fileAccessManagerOptions.SessionOptions.NetScanLimit = 1; - await using var server = TestHelper.CreateServer(fileAccessManagerOptions); + await using var server = await TestHelper.CreateServer(fileAccessManagerOptions); // create client var token = TestHelper.CreateAccessToken(server); diff --git a/Tests/VpnHood.Test/Tests/ServerNetFilterConfigTest.cs b/Tests/VpnHood.Test/Tests/ServerNetFilterConfigTest.cs index b316348da..9f3111983 100644 --- a/Tests/VpnHood.Test/Tests/ServerNetFilterConfigTest.cs +++ b/Tests/VpnHood.Test/Tests/ServerNetFilterConfigTest.cs @@ -15,7 +15,7 @@ public async Task PacketCapture_Include() serverOptions.NetFilterOptions.PacketCaptureIncludeIpRanges = [IpRange.Parse("230.0.0.100-230.0.0.250")]; // Create Server - await using var server = TestHelper.CreateServer(serverOptions); + await using var server = await TestHelper.CreateServer(serverOptions); var token = TestHelper.CreateAccessToken(server); // create client @@ -47,7 +47,7 @@ public async Task PacketCapture_Exclude() serverOptions.NetFilterOptions.PacketCaptureExcludeIpRanges = [IpRange.Parse("230.0.0.100-230.0.0.250")]; // Create Server - await using var server = TestHelper.CreateServer(serverOptions); + await using var server = await TestHelper.CreateServer(serverOptions); var token = TestHelper.CreateAccessToken(server); // create client @@ -82,7 +82,7 @@ public async Task PacketCapture_Include_Exclude_LocalNetwork() serverOptions.NetFilterOptions.PacketCaptureExcludeIpRanges = [IpRange.Parse("230.0.0.100 - 230.0.0.250")]; // Create Server - await using var server = TestHelper.CreateServer(serverOptions); + await using var server = await TestHelper.CreateServer(serverOptions); var token = TestHelper.CreateAccessToken(server); // create client @@ -109,7 +109,7 @@ public async Task IpRange_Include_Exclude() serverOptions.NetFilterOptions.ExcludeIpRanges = [IpRange.Parse("230.0.0.100 - 230.0.0.250")]; // Create Server - await using var server = TestHelper.CreateServer(serverOptions); + await using var server = await TestHelper.CreateServer(serverOptions); var token = TestHelper.CreateAccessToken(server); // create client diff --git a/Tests/VpnHood.Test/Tests/ServerTest.cs b/Tests/VpnHood.Test/Tests/ServerTest.cs index 596c9f6e3..9c3c8d67e 100644 --- a/Tests/VpnHood.Test/Tests/ServerTest.cs +++ b/Tests/VpnHood.Test/Tests/ServerTest.cs @@ -9,7 +9,6 @@ using VpnHood.Common.Net; using VpnHood.Common.Utils; using VpnHood.Server.Access.Configurations; -using VpnHood.Server.Access.Managers.File; using VpnHood.Tunneling; namespace VpnHood.Test.Tests; @@ -22,7 +21,7 @@ public async Task Configure() { using var fileAccessManager = TestHelper.CreateFileAccessManager(); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); Assert.IsNotNull(testAccessManager.LastServerInfo); Assert.IsTrue(testAccessManager.LastServerInfo.FreeUdpPortV4 > 0); @@ -41,7 +40,7 @@ public async Task Auto_sync_sessions_by_interval() serverOptions.SessionOptions.SyncInterval = TimeSpan.FromMicroseconds(200); var fileAccessManager = TestHelper.CreateFileAccessManager(serverOptions); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // Create client var token = TestHelper.CreateAccessToken(server); @@ -66,12 +65,12 @@ public async Task Reconfigure_Listeners() using var fileAccessManager = TestHelper.CreateFileAccessManager(); fileAccessManager.ServerConfig.UpdateStatusInterval = TimeSpan.FromMilliseconds(300); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // change tcp end points var newTcpEndPoint = VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback); VhLogger.Instance.LogTrace(GeneralEventId.Test, - "Test: Changing access server UdpEndPoint. TcpEndPoint: {TcpEndPoint}", newTcpEndPoint); + "Test: Changing access server TcpEndPoint. TcpEndPoint: {TcpEndPoint}", newTcpEndPoint); fileAccessManager.ServerConfig.TcpEndPoints = [newTcpEndPoint]; fileAccessManager.ServerConfig.ConfigCode = Guid.NewGuid().ToString(); await VhTestUtil.AssertEqualsWait(fileAccessManager.ServerConfig.ConfigCode, @@ -87,7 +86,8 @@ await VhTestUtil.AssertEqualsWait(fileAccessManager.ServerConfig.ConfigCode, fileAccessManager.ServerConfig.UdpEndPoints = [newUdpEndPoint]; fileAccessManager.ServerConfig.ConfigCode = Guid.NewGuid().ToString(); await VhTestUtil.AssertEqualsWait(fileAccessManager.ServerConfig.ConfigCode, - () => testAccessManager.LastServerStatus!.ConfigCode); + () => testAccessManager.LastServerStatus!.ConfigCode); + Assert.AreNotEqual( VhUtil.GetFreeUdpEndPoint(IPAddress.Loopback, fileAccessManager.ServerConfig.UdpEndPoints[0].Port), fileAccessManager.ServerConfig.UdpEndPoints[0]); @@ -97,7 +97,7 @@ await VhTestUtil.AssertEqualsWait(fileAccessManager.ServerConfig.ConfigCode, public async Task Reconfigure() { var serverEndPoint = VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback); - var fileAccessManagerOptions = new FileAccessManagerOptions { TcpEndPoints = [serverEndPoint] }; + var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(tcpEndPoints: [serverEndPoint]); using var fileAccessManager = TestHelper.CreateFileAccessManager(fileAccessManagerOptions); var serverConfig = fileAccessManager.ServerConfig; serverConfig.UpdateStatusInterval = TimeSpan.FromMilliseconds(500); @@ -114,7 +114,7 @@ public async Task Reconfigure() using var testAccessManager = new TestAccessManager(fileAccessManager); var dateTime = DateTime.Now; - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); Assert.IsTrue(testAccessManager.LastConfigureTime > dateTime); dateTime = DateTime.Now; @@ -147,7 +147,7 @@ public async Task Close_session_by_client_disconnect() // create server using var fileAccessManager = TestHelper.CreateFileAccessManager(); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // create client var token = TestHelper.CreateAccessToken(server); @@ -155,7 +155,7 @@ public async Task Close_session_by_client_disconnect() Assert.IsTrue(fileAccessManager.SessionController.Sessions.TryGetValue(client.SessionId, out var session)); await client.DisposeAsync(); - await TestHelper.WaitForClientStateAsync(client, ClientState.Disposed); + await TestHelper.WaitForClientState(client, ClientState.Disposed); await VhTestUtil.AssertEqualsWait(false, () => session.IsAlive); } @@ -163,19 +163,24 @@ public async Task Close_session_by_client_disconnect() public async Task Restore_session_after_restarting_server() { // create server - using var fileAccessManager = TestHelper.CreateFileAccessManager(); - using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + var fileAccessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); + using var fileAccessManager1 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions); + using var testAccessManager1 = new TestAccessManager(fileAccessManager1); + await using var server1 = await TestHelper.CreateServer(testAccessManager1); // create client - var token = TestHelper.CreateAccessToken(server); + var token = TestHelper.CreateAccessToken(server1); await using var client = await TestHelper.CreateClient(token); Assert.AreEqual(ClientState.Connected, client.State); await TestHelper.Test_Https(); - // restart server - await server.DisposeAsync(); - await using var server2 = TestHelper.CreateServer(testAccessManager); + // restart server and access manager + await server1.DisposeAsync(); + testAccessManager1.Dispose(); + fileAccessManager1.Dispose(); + using var fileAccessManager2 = TestHelper.CreateFileAccessManager(fileAccessManagerOptions, fileAccessManager1.StoragePath); + using var testAccessManager2 = new TestAccessManager(fileAccessManager2); + await using var server2 = await TestHelper.CreateServer(testAccessManager2); VhLogger.Instance.LogInformation("Test: Sending another HTTP Request..."); await TestHelper.Test_Https(); @@ -188,14 +193,14 @@ public async Task Recover_should_call_access_server_only_once() { using var fileAccessManager = TestHelper.CreateFileAccessManager(); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // Create Client var token1 = TestHelper.CreateAccessToken(fileAccessManager); await using var client = await TestHelper.CreateClient(token1); await server.DisposeAsync(); - await using var server2 = TestHelper.CreateServer(testAccessManager); + await using var server2 = await TestHelper.CreateServer(testAccessManager); await Task.WhenAll( TestHelper.Test_Https(timeout: 10000, throwError: false), TestHelper.Test_Https(timeout: 10000, throwError: false), @@ -213,7 +218,7 @@ await Task.WhenAll( [TestMethod] public async Task Unauthorized_response_is_expected_for_unknown_request() { - await using var server = TestHelper.CreateServer(); + await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); var handler = new HttpClientHandler(); @@ -233,7 +238,7 @@ public async Task Server_should_close_session_if_it_does_not_exist_in_access_ser accessManagerOptions.SessionOptions.SyncCacheSize = 1000000; using var fileAccessManager = TestHelper.CreateFileAccessManager(accessManagerOptions); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // create client var token = TestHelper.CreateAccessToken(server); @@ -316,7 +321,7 @@ public async Task DnsChallenge() fileAccessManager.ServerConfig.UpdateStatusInterval = TimeSpan.FromMilliseconds(300); using var testAccessManager = new TestAccessManager(fileAccessManager); - await using var server = TestHelper.CreateServer(testAccessManager); + await using var server = await TestHelper.CreateServer(testAccessManager); // set DnsChallenge var dnsChallenge = new DnsChallenge diff --git a/Tests/VpnHood.Test/Tests/UdpProxyTest.cs b/Tests/VpnHood.Test/Tests/UdpProxyTest.cs index 58132daa5..d302c601e 100644 --- a/Tests/VpnHood.Test/Tests/UdpProxyTest.cs +++ b/Tests/VpnHood.Test/Tests/UdpProxyTest.cs @@ -163,7 +163,7 @@ public async Task Max_UdpClients() var accessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); accessManagerOptions.SessionOptions.MaxUdpClientCount = maxUdpCount; - await using var server = TestHelper.CreateServer(accessManagerOptions); + await using var server = await TestHelper.CreateServer(accessManagerOptions); var token = TestHelper.CreateAccessToken(server); // Create Client diff --git a/Tests/VpnHood.Test/VpnHood.Test.csproj b/Tests/VpnHood.Test/VpnHood.Test.csproj index 461ae9a36..5bf9a86b1 100644 --- a/Tests/VpnHood.Test/VpnHood.Test.csproj +++ b/Tests/VpnHood.Test/VpnHood.Test.csproj @@ -25,9 +25,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/VpnHood.Client.App.Android.Common/VpnHood.Client.App.Android.Common.csproj b/VpnHood.Client.App.Android.Common/VpnHood.Client.App.Android.Common.csproj index 5d6286ac4..0479389fa 100644 --- a/VpnHood.Client.App.Android.Common/VpnHood.Client.App.Android.Common.csproj +++ b/VpnHood.Client.App.Android.Common/VpnHood.Client.App.Android.Common.csproj @@ -21,7 +21,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Android.Connect/VpnHood.Client.App.Android.Connect.csproj b/VpnHood.Client.App.Android.Connect/VpnHood.Client.App.Android.Connect.csproj index 64e444cf0..d62c8da7c 100644 --- a/VpnHood.Client.App.Android.Connect/VpnHood.Client.App.Android.Connect.csproj +++ b/VpnHood.Client.App.Android.Connect/VpnHood.Client.App.Android.Connect.csproj @@ -5,8 +5,8 @@ VpnHood.Client.App.Droid.Connect Exe com.vpnhood.connect.android - 502 - 4.4.502 + 510 + 4.5.510 23.0 @@ -30,7 +30,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.502 + 4.5.510 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Android.GooglePlay.Ads/VpnHood.Client.App.Android.GooglePlay.Ads.csproj b/VpnHood.Client.App.Android.GooglePlay.Ads/VpnHood.Client.App.Android.GooglePlay.Ads.csproj index 2eddb1006..1074f42c7 100644 --- a/VpnHood.Client.App.Android.GooglePlay.Ads/VpnHood.Client.App.Android.GooglePlay.Ads.csproj +++ b/VpnHood.Client.App.Android.GooglePlay.Ads/VpnHood.Client.App.Android.GooglePlay.Ads.csproj @@ -35,7 +35,7 @@ - + diff --git a/VpnHood.Client.App.Android.GooglePlay.Core/VpnHood.Client.App.Android.GooglePlay.Core.csproj b/VpnHood.Client.App.Android.GooglePlay.Core/VpnHood.Client.App.Android.GooglePlay.Core.csproj index 34a53bc48..95221c434 100644 --- a/VpnHood.Client.App.Android.GooglePlay.Core/VpnHood.Client.App.Android.GooglePlay.Core.csproj +++ b/VpnHood.Client.App.Android.GooglePlay.Core/VpnHood.Client.App.Android.GooglePlay.Core.csproj @@ -21,7 +21,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -35,7 +35,7 @@ - + diff --git a/VpnHood.Client.App.Android.GooglePlay/VpnHood.Client.App.Android.GooglePlay.csproj b/VpnHood.Client.App.Android.GooglePlay/VpnHood.Client.App.Android.GooglePlay.csproj index 187189c30..86024fe1d 100644 --- a/VpnHood.Client.App.Android.GooglePlay/VpnHood.Client.App.Android.GooglePlay.csproj +++ b/VpnHood.Client.App.Android.GooglePlay/VpnHood.Client.App.Android.GooglePlay.csproj @@ -21,7 +21,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -35,9 +35,9 @@ - - - + + + diff --git a/VpnHood.Client.App.Android/MainActivity.cs b/VpnHood.Client.App.Android/MainActivity.cs index d4f8ccd80..37f7cb8d9 100644 --- a/VpnHood.Client.App.Android/MainActivity.cs +++ b/VpnHood.Client.App.Android/MainActivity.cs @@ -40,7 +40,7 @@ protected override AndroidAppMainActivityHandler CreateMainActivityHandler() DefaultSpaPort = AssemblyInfo.DefaultSpaPort, ListenToAllIps = AssemblyInfo.ListenToAllIps, AccessKeySchemes = [AccessKeyScheme1, AccessKeyScheme2], - AccessKeyMimes = [AccessKeyMime1, AccessKeyMime2, AccessKeyMime3], + AccessKeyMimes = [AccessKeyMime1, AccessKeyMime2, AccessKeyMime3] }); } } diff --git a/VpnHood.Client.App.Android/Properties/AssemblyInfo.cs b/VpnHood.Client.App.Android/Properties/AssemblyInfo.cs index 9c5c82134..b9ac9c85c 100644 --- a/VpnHood.Client.App.Android/Properties/AssemblyInfo.cs +++ b/VpnHood.Client.App.Android/Properties/AssemblyInfo.cs @@ -2,6 +2,7 @@ // defined in this file are now automatically added during build and populated with // values defined in project properties. For details of which attributes are included // and how to customise this process see: https://aka.ms/assembly-info-properties + using VpnHood.Client.App.Abstractions; #if GOOGLE_PLAY using VpnHood.Client.App.Droid.GooglePlay; diff --git a/VpnHood.Client.App.Android/VpnHood.Client.App.Android.csproj b/VpnHood.Client.App.Android/VpnHood.Client.App.Android.csproj index b3adbecc6..2ba105366 100644 --- a/VpnHood.Client.App.Android/VpnHood.Client.App.Android.csproj +++ b/VpnHood.Client.App.Android/VpnHood.Client.App.Android.csproj @@ -5,8 +5,8 @@ VpnHood.Client.App.Droid Exe com.vpnhood.client.android.web - 506 - 4.4.506 + 520 + 4.5.520 23.0 @@ -30,7 +30,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Maui.Common/VpnHood.Client.App.Maui.Common.csproj b/VpnHood.Client.App.Maui.Common/VpnHood.Client.App.Maui.Common.csproj index 4adc66aa1..1b804243e 100644 --- a/VpnHood.Client.App.Maui.Common/VpnHood.Client.App.Maui.Common.csproj +++ b/VpnHood.Client.App.Maui.Common/VpnHood.Client.App.Maui.Common.csproj @@ -30,7 +30,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -44,9 +44,9 @@ - - - + + + diff --git a/VpnHood.Client.App.Resources/Resources/SPA.zip b/VpnHood.Client.App.Resources/Resources/SPA.zip index cce8df0a7..8bea67df0 100644 Binary files a/VpnHood.Client.App.Resources/Resources/SPA.zip and b/VpnHood.Client.App.Resources/Resources/SPA.zip differ diff --git a/VpnHood.Client.App.Resources/VpnHood.Client.App.Resources.csproj b/VpnHood.Client.App.Resources/VpnHood.Client.App.Resources.csproj index adaab76a4..0b9e57749 100644 --- a/VpnHood.Client.App.Resources/VpnHood.Client.App.Resources.csproj +++ b/VpnHood.Client.App.Resources/VpnHood.Client.App.Resources.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Store/VpnHood.Client.App.Store.csproj b/VpnHood.Client.App.Store/VpnHood.Client.App.Store.csproj index 1277b4800..0bbf8df80 100644 --- a/VpnHood.Client.App.Store/VpnHood.Client.App.Store.csproj +++ b/VpnHood.Client.App.Store/VpnHood.Client.App.Store.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -35,7 +35,7 @@ - + diff --git a/VpnHood.Client.App.Swagger/Api/VpnHood.Client.Api.ts b/VpnHood.Client.App.Swagger/Api/VpnHood.Client.Api.ts index 4ee8255a4..9414429c3 100644 --- a/VpnHood.Client.App.Swagger/Api/VpnHood.Client.Api.ts +++ b/VpnHood.Client.App.Swagger/Api/VpnHood.Client.Api.ts @@ -1,6 +1,6 @@ //---------------------- // -// Generated using the NSwag toolchain v14.0.1.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// Generated using the NSwag toolchain v14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) // //---------------------- @@ -473,10 +473,12 @@ export class AppClient { return Promise.resolve(null as any); } - connect(clientProfileId?: string | null | undefined, cancelToken?: CancelToken): Promise { + connect(clientProfileId?: string | null | undefined, serverLocation?: string | null | undefined, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/app/connect?"; if (clientProfileId !== undefined && clientProfileId !== null) url_ += "clientProfileId=" + encodeURIComponent("" + clientProfileId) + "&"; + if (serverLocation !== undefined && serverLocation !== null) + url_ += "serverLocation=" + encodeURIComponent("" + serverLocation) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: AxiosRequestConfig = { @@ -519,10 +521,12 @@ export class AppClient { return Promise.resolve(null as any); } - diagnose(clientProfileId?: string | null | undefined, cancelToken?: CancelToken): Promise { + diagnose(clientProfileId?: string | null | undefined, serverLocation?: string | null | undefined, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/app/diagnose?"; if (clientProfileId !== undefined && clientProfileId !== null) url_ += "clientProfileId=" + encodeURIComponent("" + clientProfileId) + "&"; + if (serverLocation !== undefined && serverLocation !== null) + url_ += "serverLocation=" + encodeURIComponent("" + serverLocation) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: AxiosRequestConfig = { @@ -1577,7 +1581,6 @@ export class AppSettings implements IAppSettings { configTime!: Date; userSettings!: UserSettings; clientId!: string; - lastCountryIpGroupId?: string | null; constructor(data?: IAppSettings) { if (data) { @@ -1599,7 +1602,6 @@ export class AppSettings implements IAppSettings { this.configTime = _data["configTime"] ? new Date(_data["configTime"].toString()) : null; this.userSettings = _data["userSettings"] ? UserSettings.fromJS(_data["userSettings"]) : new UserSettings(); this.clientId = _data["clientId"] !== undefined ? _data["clientId"] : null; - this.lastCountryIpGroupId = _data["lastCountryIpGroupId"] !== undefined ? _data["lastCountryIpGroupId"] : null; } } @@ -1618,7 +1620,6 @@ export class AppSettings implements IAppSettings { data["configTime"] = this.configTime ? this.configTime.toISOString() : null; data["userSettings"] = this.userSettings ? this.userSettings.toJSON() : null; data["clientId"] = this.clientId !== undefined ? this.clientId : null; - data["lastCountryIpGroupId"] = this.lastCountryIpGroupId !== undefined ? this.lastCountryIpGroupId : null; return data; } } @@ -1630,25 +1631,23 @@ export interface IAppSettings { configTime: Date; userSettings: UserSettings; clientId: string; - lastCountryIpGroupId?: string | null; } export class UserSettings implements IUserSettings { logging!: AppLogSettings; cultureCode?: string | null; clientProfileId?: string | null; - maxReconnectCount!: number; + serverLocation?: string | null; maxDatagramChannelCount!: number; tunnelClientCountry!: boolean; - ipGroupFilters?: string[] | null; - ipGroupFiltersMode!: FilterMode; - customIpRanges?: string[] | null; appFilters?: string[] | null; appFiltersMode!: FilterMode; useUdpChannel!: boolean; dropUdpPackets!: boolean; includeLocalNetwork!: boolean; - packetCaptureIncludeIpRanges!: string[]; + includeIpRanges?: string[] | null; + excludeIpRanges?: string[] | null; + packetCaptureIncludeIpRanges?: string[] | null; packetCaptureExcludeIpRanges?: string[] | null; allowAnonymousTracker!: boolean; dnsServers?: string[] | null; @@ -1664,7 +1663,6 @@ export class UserSettings implements IUserSettings { } if (!data) { this.logging = new AppLogSettings(); - this.packetCaptureIncludeIpRanges = []; } } @@ -1673,26 +1671,9 @@ export class UserSettings implements IUserSettings { this.logging = _data["logging"] ? AppLogSettings.fromJS(_data["logging"]) : new AppLogSettings(); this.cultureCode = _data["cultureCode"] !== undefined ? _data["cultureCode"] : null; this.clientProfileId = _data["clientProfileId"] !== undefined ? _data["clientProfileId"] : null; - this.maxReconnectCount = _data["maxReconnectCount"] !== undefined ? _data["maxReconnectCount"] : null; + this.serverLocation = _data["serverLocation"] !== undefined ? _data["serverLocation"] : null; this.maxDatagramChannelCount = _data["maxDatagramChannelCount"] !== undefined ? _data["maxDatagramChannelCount"] : null; this.tunnelClientCountry = _data["tunnelClientCountry"] !== undefined ? _data["tunnelClientCountry"] : null; - if (Array.isArray(_data["ipGroupFilters"])) { - this.ipGroupFilters = [] as any; - for (let item of _data["ipGroupFilters"]) - this.ipGroupFilters!.push(item); - } - else { - this.ipGroupFilters = null; - } - this.ipGroupFiltersMode = _data["ipGroupFiltersMode"] !== undefined ? _data["ipGroupFiltersMode"] : null; - if (Array.isArray(_data["customIpRanges"])) { - this.customIpRanges = [] as any; - for (let item of _data["customIpRanges"]) - this.customIpRanges!.push(item); - } - else { - this.customIpRanges = null; - } if (Array.isArray(_data["appFilters"])) { this.appFilters = [] as any; for (let item of _data["appFilters"]) @@ -1705,6 +1686,22 @@ export class UserSettings implements IUserSettings { this.useUdpChannel = _data["useUdpChannel"] !== undefined ? _data["useUdpChannel"] : null; this.dropUdpPackets = _data["dropUdpPackets"] !== undefined ? _data["dropUdpPackets"] : null; this.includeLocalNetwork = _data["includeLocalNetwork"] !== undefined ? _data["includeLocalNetwork"] : null; + if (Array.isArray(_data["includeIpRanges"])) { + this.includeIpRanges = [] as any; + for (let item of _data["includeIpRanges"]) + this.includeIpRanges!.push(item); + } + else { + this.includeIpRanges = null; + } + if (Array.isArray(_data["excludeIpRanges"])) { + this.excludeIpRanges = [] as any; + for (let item of _data["excludeIpRanges"]) + this.excludeIpRanges!.push(item); + } + else { + this.excludeIpRanges = null; + } if (Array.isArray(_data["packetCaptureIncludeIpRanges"])) { this.packetCaptureIncludeIpRanges = [] as any; for (let item of _data["packetCaptureIncludeIpRanges"]) @@ -1747,20 +1744,9 @@ export class UserSettings implements IUserSettings { data["logging"] = this.logging ? this.logging.toJSON() : null; data["cultureCode"] = this.cultureCode !== undefined ? this.cultureCode : null; data["clientProfileId"] = this.clientProfileId !== undefined ? this.clientProfileId : null; - data["maxReconnectCount"] = this.maxReconnectCount !== undefined ? this.maxReconnectCount : null; + data["serverLocation"] = this.serverLocation !== undefined ? this.serverLocation : null; data["maxDatagramChannelCount"] = this.maxDatagramChannelCount !== undefined ? this.maxDatagramChannelCount : null; data["tunnelClientCountry"] = this.tunnelClientCountry !== undefined ? this.tunnelClientCountry : null; - if (Array.isArray(this.ipGroupFilters)) { - data["ipGroupFilters"] = []; - for (let item of this.ipGroupFilters) - data["ipGroupFilters"].push(item); - } - data["ipGroupFiltersMode"] = this.ipGroupFiltersMode !== undefined ? this.ipGroupFiltersMode : null; - if (Array.isArray(this.customIpRanges)) { - data["customIpRanges"] = []; - for (let item of this.customIpRanges) - data["customIpRanges"].push(item); - } if (Array.isArray(this.appFilters)) { data["appFilters"] = []; for (let item of this.appFilters) @@ -1770,6 +1756,16 @@ export class UserSettings implements IUserSettings { data["useUdpChannel"] = this.useUdpChannel !== undefined ? this.useUdpChannel : null; data["dropUdpPackets"] = this.dropUdpPackets !== undefined ? this.dropUdpPackets : null; data["includeLocalNetwork"] = this.includeLocalNetwork !== undefined ? this.includeLocalNetwork : null; + if (Array.isArray(this.includeIpRanges)) { + data["includeIpRanges"] = []; + for (let item of this.includeIpRanges) + data["includeIpRanges"].push(item); + } + if (Array.isArray(this.excludeIpRanges)) { + data["excludeIpRanges"] = []; + for (let item of this.excludeIpRanges) + data["excludeIpRanges"].push(item); + } if (Array.isArray(this.packetCaptureIncludeIpRanges)) { data["packetCaptureIncludeIpRanges"] = []; for (let item of this.packetCaptureIncludeIpRanges) @@ -1796,18 +1792,17 @@ export interface IUserSettings { logging: AppLogSettings; cultureCode?: string | null; clientProfileId?: string | null; - maxReconnectCount: number; + serverLocation?: string | null; maxDatagramChannelCount: number; tunnelClientCountry: boolean; - ipGroupFilters?: string[] | null; - ipGroupFiltersMode: FilterMode; - customIpRanges?: string[] | null; appFilters?: string[] | null; appFiltersMode: FilterMode; useUdpChannel: boolean; dropUdpPackets: boolean; includeLocalNetwork: boolean; - packetCaptureIncludeIpRanges: string[]; + includeIpRanges?: string[] | null; + excludeIpRanges?: string[] | null; + packetCaptureIncludeIpRanges?: string[] | null; packetCaptureExcludeIpRanges?: string[] | null; allowAnonymousTracker: boolean; dnsServers?: string[] | null; @@ -1875,6 +1870,8 @@ export class AppState implements IAppState { connectionState!: AppConnectionState; lastError?: string | null; clientProfile?: ClientProfileBaseInfo | null; + clientServerLocationInfo?: ClientServerLocationInfo | null; + serverLocationInfo?: ServerLocationInfo | null; isIdle!: boolean; logExists!: boolean; hasDiagnoseStarted!: boolean; @@ -1884,13 +1881,15 @@ export class AppState implements IAppState { speed!: Traffic; sessionTraffic!: Traffic; accountTraffic!: Traffic; - clientIpGroup?: IpGroup | null; + clientCountryCode?: string | null; + clientCountryName?: string | null; isWaitingForAd!: boolean; versionStatus!: VersionStatus; lastPublishInfo?: PublishInfo | null; isUdpChannelSupported?: boolean | null; canDisconnect!: boolean; canConnect!: boolean; + canDiagnose!: boolean; currentUiCultureInfo!: UiCultureInfo; systemUiCultureInfo!: UiCultureInfo; purchaseState?: BillingPurchaseState | null; @@ -1918,6 +1917,8 @@ export class AppState implements IAppState { this.connectionState = _data["connectionState"] !== undefined ? _data["connectionState"] : null; this.lastError = _data["lastError"] !== undefined ? _data["lastError"] : null; this.clientProfile = _data["clientProfile"] ? ClientProfileBaseInfo.fromJS(_data["clientProfile"]) : null; + this.clientServerLocationInfo = _data["clientServerLocationInfo"] ? ClientServerLocationInfo.fromJS(_data["clientServerLocationInfo"]) : null; + this.serverLocationInfo = _data["serverLocationInfo"] ? ServerLocationInfo.fromJS(_data["serverLocationInfo"]) : null; this.isIdle = _data["isIdle"] !== undefined ? _data["isIdle"] : null; this.logExists = _data["logExists"] !== undefined ? _data["logExists"] : null; this.hasDiagnoseStarted = _data["hasDiagnoseStarted"] !== undefined ? _data["hasDiagnoseStarted"] : null; @@ -1927,13 +1928,15 @@ export class AppState implements IAppState { this.speed = _data["speed"] ? Traffic.fromJS(_data["speed"]) : new Traffic(); this.sessionTraffic = _data["sessionTraffic"] ? Traffic.fromJS(_data["sessionTraffic"]) : new Traffic(); this.accountTraffic = _data["accountTraffic"] ? Traffic.fromJS(_data["accountTraffic"]) : new Traffic(); - this.clientIpGroup = _data["clientIpGroup"] ? IpGroup.fromJS(_data["clientIpGroup"]) : null; + this.clientCountryCode = _data["clientCountryCode"] !== undefined ? _data["clientCountryCode"] : null; + this.clientCountryName = _data["clientCountryName"] !== undefined ? _data["clientCountryName"] : null; this.isWaitingForAd = _data["isWaitingForAd"] !== undefined ? _data["isWaitingForAd"] : null; this.versionStatus = _data["versionStatus"] !== undefined ? _data["versionStatus"] : null; this.lastPublishInfo = _data["lastPublishInfo"] ? PublishInfo.fromJS(_data["lastPublishInfo"]) : null; this.isUdpChannelSupported = _data["isUdpChannelSupported"] !== undefined ? _data["isUdpChannelSupported"] : null; this.canDisconnect = _data["canDisconnect"] !== undefined ? _data["canDisconnect"] : null; this.canConnect = _data["canConnect"] !== undefined ? _data["canConnect"] : null; + this.canDiagnose = _data["canDiagnose"] !== undefined ? _data["canDiagnose"] : null; this.currentUiCultureInfo = _data["currentUiCultureInfo"] ? UiCultureInfo.fromJS(_data["currentUiCultureInfo"]) : new UiCultureInfo(); this.systemUiCultureInfo = _data["systemUiCultureInfo"] ? UiCultureInfo.fromJS(_data["systemUiCultureInfo"]) : new UiCultureInfo(); this.purchaseState = _data["purchaseState"] !== undefined ? _data["purchaseState"] : null; @@ -1954,6 +1957,8 @@ export class AppState implements IAppState { data["connectionState"] = this.connectionState !== undefined ? this.connectionState : null; data["lastError"] = this.lastError !== undefined ? this.lastError : null; data["clientProfile"] = this.clientProfile ? this.clientProfile.toJSON() : null; + data["clientServerLocationInfo"] = this.clientServerLocationInfo ? this.clientServerLocationInfo.toJSON() : null; + data["serverLocationInfo"] = this.serverLocationInfo ? this.serverLocationInfo.toJSON() : null; data["isIdle"] = this.isIdle !== undefined ? this.isIdle : null; data["logExists"] = this.logExists !== undefined ? this.logExists : null; data["hasDiagnoseStarted"] = this.hasDiagnoseStarted !== undefined ? this.hasDiagnoseStarted : null; @@ -1963,13 +1968,15 @@ export class AppState implements IAppState { data["speed"] = this.speed ? this.speed.toJSON() : null; data["sessionTraffic"] = this.sessionTraffic ? this.sessionTraffic.toJSON() : null; data["accountTraffic"] = this.accountTraffic ? this.accountTraffic.toJSON() : null; - data["clientIpGroup"] = this.clientIpGroup ? this.clientIpGroup.toJSON() : null; + data["clientCountryCode"] = this.clientCountryCode !== undefined ? this.clientCountryCode : null; + data["clientCountryName"] = this.clientCountryName !== undefined ? this.clientCountryName : null; data["isWaitingForAd"] = this.isWaitingForAd !== undefined ? this.isWaitingForAd : null; data["versionStatus"] = this.versionStatus !== undefined ? this.versionStatus : null; data["lastPublishInfo"] = this.lastPublishInfo ? this.lastPublishInfo.toJSON() : null; data["isUdpChannelSupported"] = this.isUdpChannelSupported !== undefined ? this.isUdpChannelSupported : null; data["canDisconnect"] = this.canDisconnect !== undefined ? this.canDisconnect : null; data["canConnect"] = this.canConnect !== undefined ? this.canConnect : null; + data["canDiagnose"] = this.canDiagnose !== undefined ? this.canDiagnose : null; data["currentUiCultureInfo"] = this.currentUiCultureInfo ? this.currentUiCultureInfo.toJSON() : null; data["systemUiCultureInfo"] = this.systemUiCultureInfo ? this.systemUiCultureInfo.toJSON() : null; data["purchaseState"] = this.purchaseState !== undefined ? this.purchaseState : null; @@ -1983,6 +1990,8 @@ export interface IAppState { connectionState: AppConnectionState; lastError?: string | null; clientProfile?: ClientProfileBaseInfo | null; + clientServerLocationInfo?: ClientServerLocationInfo | null; + serverLocationInfo?: ServerLocationInfo | null; isIdle: boolean; logExists: boolean; hasDiagnoseStarted: boolean; @@ -1992,13 +2001,15 @@ export interface IAppState { speed: Traffic; sessionTraffic: Traffic; accountTraffic: Traffic; - clientIpGroup?: IpGroup | null; + clientCountryCode?: string | null; + clientCountryName?: string | null; isWaitingForAd: boolean; versionStatus: VersionStatus; lastPublishInfo?: PublishInfo | null; isUdpChannelSupported?: boolean | null; canDisconnect: boolean; canConnect: boolean; + canDiagnose: boolean; currentUiCultureInfo: UiCultureInfo; systemUiCultureInfo: UiCultureInfo; purchaseState?: BillingPurchaseState | null; @@ -2017,7 +2028,6 @@ export enum AppConnectionState { export class ClientProfileBaseInfo implements IClientProfileBaseInfo { clientProfileId!: string; clientProfileName!: string; - regionId?: string | null; supportId?: string | null; constructor(data?: IClientProfileBaseInfo) { @@ -2033,7 +2043,6 @@ export class ClientProfileBaseInfo implements IClientProfileBaseInfo { if (_data) { this.clientProfileId = _data["clientProfileId"] !== undefined ? _data["clientProfileId"] : null; this.clientProfileName = _data["clientProfileName"] !== undefined ? _data["clientProfileName"] : null; - this.regionId = _data["regionId"] !== undefined ? _data["regionId"] : null; this.supportId = _data["supportId"] !== undefined ? _data["supportId"] : null; } } @@ -2049,7 +2058,6 @@ export class ClientProfileBaseInfo implements IClientProfileBaseInfo { data = typeof data === 'object' ? data : {}; data["clientProfileId"] = this.clientProfileId !== undefined ? this.clientProfileId : null; data["clientProfileName"] = this.clientProfileName !== undefined ? this.clientProfileName : null; - data["regionId"] = this.regionId !== undefined ? this.regionId : null; data["supportId"] = this.supportId !== undefined ? this.supportId : null; return data; } @@ -2058,10 +2066,94 @@ export class ClientProfileBaseInfo implements IClientProfileBaseInfo { export interface IClientProfileBaseInfo { clientProfileId: string; clientProfileName: string; - regionId?: string | null; supportId?: string | null; } +export class ServerLocationInfo implements IServerLocationInfo { + countryCode!: string; + regionName!: string; + serverLocation!: string; + countryName!: string; + + constructor(data?: IServerLocationInfo) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.countryCode = _data["countryCode"] !== undefined ? _data["countryCode"] : null; + this.regionName = _data["regionName"] !== undefined ? _data["regionName"] : null; + this.serverLocation = _data["serverLocation"] !== undefined ? _data["serverLocation"] : null; + this.countryName = _data["countryName"] !== undefined ? _data["countryName"] : null; + } + } + + static fromJS(data: any): ServerLocationInfo { + data = typeof data === 'object' ? data : {}; + let result = new ServerLocationInfo(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["countryCode"] = this.countryCode !== undefined ? this.countryCode : null; + data["regionName"] = this.regionName !== undefined ? this.regionName : null; + data["serverLocation"] = this.serverLocation !== undefined ? this.serverLocation : null; + data["countryName"] = this.countryName !== undefined ? this.countryName : null; + return data; + } +} + +export interface IServerLocationInfo { + countryCode: string; + regionName: string; + serverLocation: string; + countryName: string; +} + +export class ClientServerLocationInfo extends ServerLocationInfo implements IClientServerLocationInfo { + isNestedCountry!: boolean; + isDefault!: boolean; + + constructor(data?: IClientServerLocationInfo) { + super(data); + } + + override init(_data?: any) { + super.init(_data); + if (_data) { + this.isNestedCountry = _data["isNestedCountry"] !== undefined ? _data["isNestedCountry"] : null; + this.isDefault = _data["isDefault"] !== undefined ? _data["isDefault"] : null; + } + } + + static override fromJS(data: any): ClientServerLocationInfo { + data = typeof data === 'object' ? data : {}; + let result = new ClientServerLocationInfo(); + result.init(data); + return result; + } + + override toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["isNestedCountry"] = this.isNestedCountry !== undefined ? this.isNestedCountry : null; + data["isDefault"] = this.isDefault !== undefined ? this.isDefault : null; + super.toJSON(data); + return data; + } +} + +export interface IClientServerLocationInfo extends IServerLocationInfo { + isNestedCountry: boolean; + isDefault: boolean; +} + export class SessionStatus implements ISessionStatus { errorCode!: SessionErrorCode; accessUsage?: AccessUsage | null; @@ -2240,46 +2332,6 @@ export enum SessionSuppressType { Other = "Other", } -export class IpGroup implements IIpGroup { - ipGroupId!: string; - ipGroupName!: string; - - constructor(data?: IIpGroup) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) - (this)[property] = (data)[property]; - } - } - } - - init(_data?: any) { - if (_data) { - this.ipGroupId = _data["ipGroupId"] !== undefined ? _data["ipGroupId"] : null; - this.ipGroupName = _data["ipGroupName"] !== undefined ? _data["ipGroupName"] : null; - } - } - - static fromJS(data: any): IpGroup { - data = typeof data === 'object' ? data : {}; - let result = new IpGroup(); - result.init(data); - return result; - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {}; - data["ipGroupId"] = this.ipGroupId !== undefined ? this.ipGroupId : null; - data["ipGroupName"] = this.ipGroupName !== undefined ? this.ipGroupName : null; - return data; - } -} - -export interface IIpGroup { - ipGroupId: string; - ipGroupName: string; -} - export enum VersionStatus { Unknown = "Unknown", Latest = "Latest", @@ -2401,13 +2453,13 @@ export class ClientProfileInfo extends ClientProfileBaseInfo implements IClientP tokenId!: string; hostNames!: string[]; isValidHostName!: boolean; - regions!: HostRegionInfo[]; + serverLocationInfos!: ClientServerLocationInfo[]; constructor(data?: IClientProfileInfo) { super(data); if (!data) { this.hostNames = []; - this.regions = []; + this.serverLocationInfos = []; } } @@ -2424,13 +2476,13 @@ export class ClientProfileInfo extends ClientProfileBaseInfo implements IClientP this.hostNames = null; } this.isValidHostName = _data["isValidHostName"] !== undefined ? _data["isValidHostName"] : null; - if (Array.isArray(_data["regions"])) { - this.regions = [] as any; - for (let item of _data["regions"]) - this.regions!.push(HostRegionInfo.fromJS(item)); + if (Array.isArray(_data["serverLocationInfos"])) { + this.serverLocationInfos = [] as any; + for (let item of _data["serverLocationInfos"]) + this.serverLocationInfos!.push(ClientServerLocationInfo.fromJS(item)); } else { - this.regions = null; + this.serverLocationInfos = null; } } } @@ -2451,10 +2503,10 @@ export class ClientProfileInfo extends ClientProfileBaseInfo implements IClientP data["hostNames"].push(item); } data["isValidHostName"] = this.isValidHostName !== undefined ? this.isValidHostName : null; - if (Array.isArray(this.regions)) { - data["regions"] = []; - for (let item of this.regions) - data["regions"].push(item.toJSON()); + if (Array.isArray(this.serverLocationInfos)) { + data["serverLocationInfos"] = []; + for (let item of this.serverLocationInfos) + data["serverLocationInfos"].push(item.toJSON()); } super.toJSON(data); return data; @@ -2465,51 +2517,7 @@ export interface IClientProfileInfo extends IClientProfileBaseInfo { tokenId: string; hostNames: string[]; isValidHostName: boolean; - regions: HostRegionInfo[]; -} - -export class HostRegionInfo implements IHostRegionInfo { - regionId!: string; - regionName!: string; - countryCode!: string; - - constructor(data?: IHostRegionInfo) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) - (this)[property] = (data)[property]; - } - } - } - - init(_data?: any) { - if (_data) { - this.regionId = _data["regionId"] !== undefined ? _data["regionId"] : null; - this.regionName = _data["regionName"] !== undefined ? _data["regionName"] : null; - this.countryCode = _data["countryCode"] !== undefined ? _data["countryCode"] : null; - } - } - - static fromJS(data: any): HostRegionInfo { - data = typeof data === 'object' ? data : {}; - let result = new HostRegionInfo(); - result.init(data); - return result; - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {}; - data["regionId"] = this.regionId !== undefined ? this.regionId : null; - data["regionName"] = this.regionName !== undefined ? this.regionName : null; - data["countryCode"] = this.countryCode !== undefined ? this.countryCode : null; - return data; - } -} - -export interface IHostRegionInfo { - regionId: string; - regionName: string; - countryCode: string; + serverLocationInfos: ClientServerLocationInfo[]; } export class ConfigParams implements IConfigParams { @@ -2694,9 +2702,49 @@ export interface IDeviceAppInfo { iconPng: string; } +export class IpGroup implements IIpGroup { + ipGroupId!: string; + ipGroupName!: string; + + constructor(data?: IIpGroup) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.ipGroupId = _data["ipGroupId"] !== undefined ? _data["ipGroupId"] : null; + this.ipGroupName = _data["ipGroupName"] !== undefined ? _data["ipGroupName"] : null; + } + } + + static fromJS(data: any): IpGroup { + data = typeof data === 'object' ? data : {}; + let result = new IpGroup(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["ipGroupId"] = this.ipGroupId !== undefined ? this.ipGroupId : null; + data["ipGroupName"] = this.ipGroupName !== undefined ? this.ipGroupName : null; + return data; + } +} + +export interface IIpGroup { + ipGroupId: string; + ipGroupName: string; +} + export class ClientProfileUpdateParams implements IClientProfileUpdateParams { clientProfileName?: PatchOfString | null; - regionId?: PatchOfString | null; + isFavorite?: PatchOfBoolean | null; constructor(data?: IClientProfileUpdateParams) { if (data) { @@ -2710,7 +2758,7 @@ export class ClientProfileUpdateParams implements IClientProfileUpdateParams { init(_data?: any) { if (_data) { this.clientProfileName = _data["clientProfileName"] ? PatchOfString.fromJS(_data["clientProfileName"]) : null; - this.regionId = _data["regionId"] ? PatchOfString.fromJS(_data["regionId"]) : null; + this.isFavorite = _data["isFavorite"] ? PatchOfBoolean.fromJS(_data["isFavorite"]) : null; } } @@ -2724,14 +2772,14 @@ export class ClientProfileUpdateParams implements IClientProfileUpdateParams { toJSON(data?: any) { data = typeof data === 'object' ? data : {}; data["clientProfileName"] = this.clientProfileName ? this.clientProfileName.toJSON() : null; - data["regionId"] = this.regionId ? this.regionId.toJSON() : null; + data["isFavorite"] = this.isFavorite ? this.isFavorite.toJSON() : null; return data; } } export interface IClientProfileUpdateParams { clientProfileName?: PatchOfString | null; - regionId?: PatchOfString | null; + isFavorite?: PatchOfBoolean | null; } export class PatchOfString implements IPatchOfString { @@ -2770,6 +2818,42 @@ export interface IPatchOfString { value?: string | null; } +export class PatchOfBoolean implements IPatchOfBoolean { + value!: boolean; + + constructor(data?: IPatchOfBoolean) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.value = _data["value"] !== undefined ? _data["value"] : null; + } + } + + static fromJS(data: any): PatchOfBoolean { + data = typeof data === 'object' ? data : {}; + let result = new PatchOfBoolean(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["value"] = this.value !== undefined ? this.value : null; + return data; + } +} + +export interface IPatchOfBoolean { + value: boolean; +} + export class SubscriptionPlan implements ISubscriptionPlan { subscriptionPlanId!: string; planPrice!: string; diff --git a/VpnHood.Client.App.Swagger/Controllers/AppController.cs b/VpnHood.Client.App.Swagger/Controllers/AppController.cs index ea3067900..1d6ef5401 100644 --- a/VpnHood.Client.App.Swagger/Controllers/AppController.cs +++ b/VpnHood.Client.App.Swagger/Controllers/AppController.cs @@ -30,13 +30,13 @@ public Task GetState() } [HttpPost("connect")] - public Task Connect(Guid? clientProfileId = null) + public Task Connect(Guid? clientProfileId = null, string? serverLocation = null) { throw new NotImplementedException(); } [HttpPost("diagnose")] - public Task Diagnose(Guid? clientProfileId = null) + public Task Diagnose(Guid? clientProfileId = null, string? serverLocation = null) { throw new NotImplementedException(); } diff --git a/VpnHood.Client.App.WebServer/Api/IAppController.cs b/VpnHood.Client.App.WebServer/Api/IAppController.cs index a74e60838..6a3f9e97f 100644 --- a/VpnHood.Client.App.WebServer/Api/IAppController.cs +++ b/VpnHood.Client.App.WebServer/Api/IAppController.cs @@ -9,8 +9,8 @@ public interface IAppController Task Configure(ConfigParams configParams); Task GetConfig(); Task GetState(); - Task Connect(Guid? clientProfileId = null); - Task Diagnose(Guid? clientProfileId = null); + Task Connect(Guid? clientProfileId = null, string? serverLocation = null); + Task Diagnose(Guid? clientProfileId = null, string? serverLocation = null); Task Disconnect(); Task AddAccessKey(string accessKey); Task UpdateClientProfile(Guid clientProfileId, ClientProfileUpdateParams updateParams); diff --git a/VpnHood.Client.App.WebServer/Controllers/AppController.cs b/VpnHood.Client.App.WebServer/Controllers/AppController.cs index 3239a2267..d49a3f5ad 100644 --- a/VpnHood.Client.App.WebServer/Controllers/AppController.cs +++ b/VpnHood.Client.App.WebServer/Controllers/AppController.cs @@ -57,16 +57,16 @@ public Task GetState() } [Route(HttpVerbs.Post, "/connect")] - public Task Connect([QueryField] Guid? clientProfileId = null) + public Task Connect([QueryField] Guid? clientProfileId = null, [QueryField] string? serverLocation = null) { - return App.Connect(clientProfileId, diagnose: false, + return App.Connect(clientProfileId, serverLocation: serverLocation, diagnose: false, userAgent: HttpContext.Request.UserAgent, throwException: false); } [Route(HttpVerbs.Post, "/diagnose")] - public Task Diagnose([QueryField] Guid? clientProfileId = null) + public Task Diagnose([QueryField] Guid? clientProfileId = null,[QueryField] string? serverLocation = null) { - return App.Connect(clientProfileId, diagnose: true, + return App.Connect(clientProfileId, serverLocation: serverLocation, diagnose: true, userAgent: HttpContext.Request.UserAgent, throwException: false); } diff --git a/VpnHood.Client.App.WebServer/VpnHood.Client.App.WebServer.csproj b/VpnHood.Client.App.WebServer/VpnHood.Client.App.WebServer.csproj index 4dd80ff2c..84fd70749 100644 --- a/VpnHood.Client.App.WebServer/VpnHood.Client.App.WebServer.csproj +++ b/VpnHood.Client.App.WebServer/VpnHood.Client.App.WebServer.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Win.Common/VpnHood.Client.App.Win.Common.csproj b/VpnHood.Client.App.Win.Common/VpnHood.Client.App.Win.Common.csproj index 0d203f792..7b4767c29 100644 --- a/VpnHood.Client.App.Win.Common/VpnHood.Client.App.Win.Common.csproj +++ b/VpnHood.Client.App.Win.Common/VpnHood.Client.App.Win.Common.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -32,7 +32,7 @@ - + diff --git a/VpnHood.Client.App.Win/VpnHood.Client.App.Win.csproj b/VpnHood.Client.App.Win/VpnHood.Client.App.Win.csproj index 5a0cff178..957a98406 100644 --- a/VpnHood.Client.App.Win/VpnHood.Client.App.Win.csproj +++ b/VpnHood.Client.App.Win/VpnHood.Client.App.Win.csproj @@ -27,7 +27,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App/Abstractions/BillingPurchaseState.cs b/VpnHood.Client.App/Abstractions/BillingPurchaseState.cs index 189957345..d73a62ce6 100644 --- a/VpnHood.Client.App/Abstractions/BillingPurchaseState.cs +++ b/VpnHood.Client.App/Abstractions/BillingPurchaseState.cs @@ -7,5 +7,5 @@ public enum BillingPurchaseState { None = 0, Started = 1, - Processing = 2, + Processing = 2 } \ No newline at end of file diff --git a/VpnHood.Client.App/AppLogService.cs b/VpnHood.Client.App/AppLogService.cs index 76fab4102..5f0411bdd 100644 --- a/VpnHood.Client.App/AppLogService.cs +++ b/VpnHood.Client.App/AppLogService.cs @@ -6,7 +6,7 @@ namespace VpnHood.Client.App; -public class AppLogService +public class AppLogService : IDisposable { private StreamLogger? _streamLogger; @@ -23,15 +23,15 @@ public Task GetLog() return File.ReadAllTextAsync(LogFilePath); } - public void Start(AppLogSettings logSettings, bool diagnose) + public void Start(AppLogSettings logSettings) { VhLogger.IsAnonymousMode = logSettings.LogAnonymous; - VhLogger.IsDiagnoseMode = diagnose | logSettings.LogVerbose; + VhLogger.IsDiagnoseMode = logSettings.LogVerbose; VhLogger.Instance = NullLogger.Instance; VhLogger.Instance = CreateLogger( addToConsole: logSettings.LogToConsole, - addToFile: logSettings.LogToFile | diagnose, - verbose: diagnose, + addToFile: logSettings.LogToFile, + verbose: logSettings.LogVerbose, removeLastFile: true); } @@ -100,4 +100,9 @@ private ILogger CreateLoggerInternal(bool addToConsole, bool addToFile, bool ver var logger = loggerFactory.CreateLogger(""); return new SyncLogger(logger); } + + public void Dispose() + { + Stop(); + } } diff --git a/VpnHood.Client.App/AppOptions.cs b/VpnHood.Client.App/AppOptions.cs index 9219a4e77..9256397be 100644 --- a/VpnHood.Client.App/AppOptions.cs +++ b/VpnHood.Client.App/AppOptions.cs @@ -1,4 +1,5 @@ using VpnHood.Client.App.Abstractions; +using VpnHood.Client.App.Settings; using VpnHood.Tunneling.Factory; namespace VpnHood.Client.App; @@ -7,11 +8,12 @@ public class AppOptions { public static string DefaultStorageFolderPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VpnHood"); public string StorageFolderPath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VpnHood"); - public TimeSpan SessionTimeout { get; set; } = new ClientOptions().SessionTimeout; + public TimeSpan SessionTimeout { get; set; } = ClientOptions.Default.SessionTimeout; public SocketFactory? SocketFactory { get; set; } public TimeSpan VersionCheckInterval { get; set; } = TimeSpan.FromHours(24); public Uri? UpdateInfoUrl { get; set; } - public bool LoadCountryIpGroups { get; set; } = true; + public bool UseIpGroupManager { get; set; } = true; + public bool UseExternalLocationService { get; set; } = true; public AppResource Resource { get; set; } = new(); public string? AppGa4MeasurementId { get; set; } = "G-4LE99XKZYE"; public string? UiName { get; set; } @@ -22,4 +24,8 @@ public class AppOptions public IAppUpdaterService? UpdaterService { get; set; } public IAppAccountService? AccountService { get; set; } public IAppAdService? AdService { get; set; } + public TimeSpan ReconnectTimeout { get; set; } = ClientOptions.Default.ReconnectTimeout; + public TimeSpan AutoWaitTimeout { get; set; } = ClientOptions.Default.AutoWaitTimeout; + public bool? LogVerbose { get; set; } + public bool? LogAnonymous { get; set; } } \ No newline at end of file diff --git a/VpnHood.Client.App/AppPersistState.cs b/VpnHood.Client.App/AppPersistState.cs index 0e9003f10..8b29933be 100644 --- a/VpnHood.Client.App/AppPersistState.cs +++ b/VpnHood.Client.App/AppPersistState.cs @@ -1,6 +1,8 @@ -using System.Text; +using System.Globalization; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; using VpnHood.Common.Logging; using VpnHood.Common.Utils; @@ -29,6 +31,36 @@ public DateTime UpdateIgnoreTime set { _updateIgnoreTime = value; Save(); } } + // prop + private string? _clientCountryCode; + public string? ClientCountryCode + { + get => _clientCountryCode; + set + { + if (_clientCountryCode == value) + return; + + // set country code and its name + _clientCountryCode = value; + try + { + ClientCountryName = value!=null ? new RegionInfo(value).EnglishName : null; + } + catch(Exception ex) + { + VhLogger.Instance.LogError(ex, "Could not get country name for code: {Code}", value); + ClientCountryName = value; + } + Save(); + } + } + + // prop + [JsonIgnore] + public string? ClientCountryName { get; private set; } + + internal static AppPersistState Load(string filePath) { var ret = VhUtil.JsonDeserializeFile(filePath, logger: VhLogger.Instance) ?? new AppPersistState(); diff --git a/VpnHood.Client.App/AppState.cs b/VpnHood.Client.App/AppState.cs index 369f67643..72dc67265 100644 --- a/VpnHood.Client.App/AppState.cs +++ b/VpnHood.Client.App/AppState.cs @@ -1,5 +1,6 @@ using VpnHood.Client.App.Abstractions; using VpnHood.Client.App.ClientProfiles; +using VpnHood.Common; using VpnHood.Common.Messaging; namespace VpnHood.Client.App; @@ -11,6 +12,8 @@ public class AppState public required AppConnectionState ConnectionState { get; init; } public required string? LastError { get; init; } public required ClientProfileBaseInfo? ClientProfile { get; init; } + public required ClientServerLocationInfo? ClientServerLocationInfo { get; init; } + public required ServerLocationInfo? ServerLocationInfo { get; init; } public required bool IsIdle { get; init; } public required bool LogExists { get; init; } public required bool HasDiagnoseStarted { get; init; } @@ -19,14 +22,16 @@ public class AppState public required SessionStatus? SessionStatus { get; init; } public required Traffic Speed { get; init; } public required Traffic SessionTraffic { get; init; } - public required Traffic AccountTraffic { get; init; } - public required IpGroup? ClientIpGroup { get; init; } + public required Traffic AccountTraffic { get; init; } + public required string? ClientCountryCode { get; init; } + public required string? ClientCountryName { get; init; } public required bool IsWaitingForAd { get; init; } public required VersionStatus VersionStatus { get; init; } public required PublishInfo? LastPublishInfo { get; init; } public required bool? IsUdpChannelSupported { get; init; } public required bool CanDisconnect { get; init; } public required bool CanConnect { get; init; } + public required bool CanDiagnose { get; init; } public required UiCultureInfo CurrentUiCultureInfo { get; init; } public required UiCultureInfo SystemUiCultureInfo { get; init; } public required BillingPurchaseState? PurchaseState { get; init; } diff --git a/VpnHood.Client.App/ClientProfiles/ClientProfile.cs b/VpnHood.Client.App/ClientProfiles/ClientProfile.cs index 4a0fb9e13..59c66094b 100644 --- a/VpnHood.Client.App/ClientProfiles/ClientProfile.cs +++ b/VpnHood.Client.App/ClientProfiles/ClientProfile.cs @@ -1,14 +1,28 @@ -using VpnHood.Common; +using System.Text.Json.Serialization; +using VpnHood.Common; namespace VpnHood.Client.App.ClientProfiles; public class ClientProfile { - public required Guid ClientProfileId { get; set; } + public required Guid ClientProfileId { get; init; } public required string? ClientProfileName { get; set; } - public string? RegionId { get; set; } - public required Token Token { get; set; } + public bool IsFavorite { get; set; } public bool IsForAccount { get; set; } + + private Token _token = default!; + public required Token Token + { + get => _token; + set + { + _token = value; + ServerLocationInfos = ClientServerLocationInfo.AddCategoryGaps(value.ServerToken.ServerLocations); + } + } + + [JsonIgnore] + public ClientServerLocationInfo[] ServerLocationInfos { get; private set; } = []; public ClientProfileInfo ToInfo() { @@ -19,10 +33,4 @@ public ClientProfileBaseInfo ToBaseInfo() { return new ClientProfileBaseInfo(this); } - - public HostRegionInfo? GetRegionInfo() - { - var region = Token.ServerToken.Regions?.SingleOrDefault(x => x.RegionId == RegionId); - return region != null ? new HostRegionInfo(region) : null; - } } \ No newline at end of file diff --git a/VpnHood.Client.App/ClientProfiles/ClientProfileBaseInfo.cs b/VpnHood.Client.App/ClientProfiles/ClientProfileBaseInfo.cs index 18920a8a2..5f9267474 100644 --- a/VpnHood.Client.App/ClientProfiles/ClientProfileBaseInfo.cs +++ b/VpnHood.Client.App/ClientProfiles/ClientProfileBaseInfo.cs @@ -6,7 +6,6 @@ public class ClientProfileBaseInfo(ClientProfile clientProfile) { public Guid ClientProfileId { get; private set; } = clientProfile.ClientProfileId; public string ClientProfileName { get; private set; } = GetTitle(clientProfile); - public string? RegionId { get; private set; } = clientProfile.RegionId; public string? SupportId { get; private set; } = clientProfile.Token.SupportId; private static string GetTitle(ClientProfile clientProfile) diff --git a/VpnHood.Client.App/ClientProfiles/ClientProfileInfo.cs b/VpnHood.Client.App/ClientProfiles/ClientProfileInfo.cs index f170cfcaf..5e0256ac0 100644 --- a/VpnHood.Client.App/ClientProfiles/ClientProfileInfo.cs +++ b/VpnHood.Client.App/ClientProfiles/ClientProfileInfo.cs @@ -3,14 +3,13 @@ namespace VpnHood.Client.App.ClientProfiles; -public class ClientProfileInfo(ClientProfile clientProfile) +public class ClientProfileInfo(ClientProfile clientProfile) : ClientProfileBaseInfo(clientProfile) { public string TokenId { get; private set; } = clientProfile.Token.TokenId; public string[] HostNames { get; private set; } = GetEndPoints(clientProfile.Token.ServerToken); public bool IsValidHostName { get; private set; } = clientProfile.Token.ServerToken.IsValidHostName; - public HostRegionInfo[] Regions { get; private set; } = - clientProfile.Token.ServerToken.Regions?.Select(x => new HostRegionInfo(x)).ToArray() ?? []; + public ClientServerLocationInfo[] ServerLocationInfos { get; private set; } = clientProfile.ServerLocationInfos; private static string[] GetEndPoints(ServerToken serverToken) { diff --git a/VpnHood.Client.App/ClientProfiles/ClientProfileService.cs b/VpnHood.Client.App/ClientProfiles/ClientProfileService.cs index dccafb97d..b7b515356 100644 --- a/VpnHood.Client.App/ClientProfiles/ClientProfileService.cs +++ b/VpnHood.Client.App/ClientProfiles/ClientProfileService.cs @@ -81,25 +81,18 @@ public ClientProfile Update(Guid clientProfileId, ClientProfileUpdateParams upda if (updateParams.ClientProfileName != null) { var name = updateParams.ClientProfileName.Value?.Trim(); + if (name == clientProfile.Token.Name?.Trim()) name = null; // set default if the name is same as token name if (name?.Length == 0) name = null; clientProfile.ClientProfileName = name; } - // update region - if (updateParams.RegionId != null) - { - if (updateParams.RegionId.Value != null && - clientProfile.Token.ServerToken.Regions?.SingleOrDefault(x => x.RegionId == updateParams.RegionId) == null) - throw new NotExistsException("RegionId does not exist."); - - clientProfile.RegionId = updateParams.RegionId; - } + if (updateParams.IsFavorite != null) + clientProfile.IsFavorite = updateParams.IsFavorite.Value; Save(); return clientProfile; } - public ClientProfile ImportAccessKey(string accessKey) { return ImportAccessKey(accessKey, false); diff --git a/VpnHood.Client.App/ClientProfiles/ClientProfileUpdateParams.cs b/VpnHood.Client.App/ClientProfiles/ClientProfileUpdateParams.cs index c5b40a527..85766335d 100644 --- a/VpnHood.Client.App/ClientProfiles/ClientProfileUpdateParams.cs +++ b/VpnHood.Client.App/ClientProfiles/ClientProfileUpdateParams.cs @@ -5,5 +5,5 @@ namespace VpnHood.Client.App.ClientProfiles; public class ClientProfileUpdateParams { public Patch? ClientProfileName { get; set; } - public Patch? RegionId { get; set; } + public Patch? IsFavorite { get; set; } } \ No newline at end of file diff --git a/VpnHood.Client.App/ClientProfiles/ClientServerLocationInfo.cs b/VpnHood.Client.App/ClientProfiles/ClientServerLocationInfo.cs new file mode 100644 index 000000000..5f3e4991f --- /dev/null +++ b/VpnHood.Client.App/ClientProfiles/ClientServerLocationInfo.cs @@ -0,0 +1,69 @@ +using VpnHood.Common; + +namespace VpnHood.Client.App.ClientProfiles; + +public class ClientServerLocationInfo : ServerLocationInfo +{ + public required bool IsNestedCountry { get; init; } + public required bool IsDefault { get; init; } + + public static ClientServerLocationInfo[] AddCategoryGaps(string[]? serverLocations) + { + serverLocations ??= []; + var locationInfos = serverLocations.Select(Parse).ToArray(); + + var results = new List(); + var countryCount = new Dictionary(); + + // Count occurrences of each country and region + foreach (var locationInfo in locationInfos) + { + if (!countryCount.TryAdd(locationInfo.CountryCode, 1)) + countryCount[locationInfo.CountryCode]++; + } + + // Add wildcard serverLocations for countries multiple occurrences + var seenCountries = new HashSet(); + foreach (var locationInfo in locationInfos) + { + var countryCode = locationInfo.CountryCode; + + // Add wildcard selector for country if it has multiple occurrences + var isMultipleCountry = countryCount[countryCode] > 1; + if (!seenCountries.Contains(countryCode)) + { + if (isMultipleCountry) + results.Add(new ClientServerLocationInfo + { + CountryCode = locationInfo.CountryCode, + RegionName = "*", + IsNestedCountry = false, + IsDefault = countryCount.Count == 1 + }); + seenCountries.Add(countryCode); + } + + results.Add(new ClientServerLocationInfo + { + CountryCode = locationInfo.CountryCode, + RegionName = locationInfo.RegionName, + IsNestedCountry = isMultipleCountry, + IsDefault = countryCount.Count == 1 && !isMultipleCountry + }); + } + + // Add auto if there is no item or if there are multiple countries + if (countryCount.Count > 1) + results.Insert(0, new ClientServerLocationInfo + { + CountryCode = Auto.CountryCode, + RegionName = Auto.RegionName, + IsNestedCountry = false, + IsDefault = true + }); + + results.Sort(); + var distinctResults = results.Distinct().ToArray(); + return distinctResults; + } +} \ No newline at end of file diff --git a/VpnHood.Client.App/ClientProfiles/HostRegionInfo.cs b/VpnHood.Client.App/ClientProfiles/HostRegionInfo.cs deleted file mode 100644 index d2a42f8bf..000000000 --- a/VpnHood.Client.App/ClientProfiles/HostRegionInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Globalization; -using VpnHood.Common; - -namespace VpnHood.Client.App.ClientProfiles; - -public class HostRegionInfo(HostRegion region) -{ - public string RegionId { get; } = region.CountryCode; - public string RegionName { get; } = string.IsNullOrWhiteSpace(region.RegionName) ? GetRegionName(region.CountryCode) : region.RegionName; - public string CountryCode { get; } = region.CountryCode; - - private static string GetRegionName(string regionCode) - { - try - { - var regionInfo = new RegionInfo(regionCode); - return regionInfo.EnglishName; - } - catch (Exception) - { - return regionCode; - } - } -} \ No newline at end of file diff --git a/VpnHood.Client.App/IpGroupManager.cs b/VpnHood.Client.App/IpGroupManager.cs index a05f93268..af812beb1 100644 --- a/VpnHood.Client.App/IpGroupManager.cs +++ b/VpnHood.Client.App/IpGroupManager.cs @@ -186,9 +186,29 @@ public async Task> GetIpRanges(string ipGroupId) return null; } + public async Task GetCountryCodeByCurrentIp() + { + try + { + var ipAddress = + await IPAddressUtil.GetPublicIpAddress(AddressFamily.InterNetwork) ?? + await IPAddressUtil.GetPublicIpAddress(AddressFamily.InterNetworkV6); + + if (ipAddress == null) + return null; + + var ipGroup = await FindIpGroup(ipAddress, null); + return ipGroup?.IpGroupId; + } + catch (Exception ex) + { + VhLogger.Instance.LogError(ex, "Could not retrieve client country from public ip services."); + return null; + } + } + private class IpGroupNetwork : IpGroup { public List IpRanges { get; set; } = []; } - } \ No newline at end of file diff --git a/VpnHood.Client.App/Resources/IP2LOCATION-LITE-DB1.IPV6.CSV.zip b/VpnHood.Client.App/Resources/IP2LOCATION-LITE-DB1.IPV6.CSV.zip index b2acb8a2c..b94d77eb6 100644 Binary files a/VpnHood.Client.App/Resources/IP2LOCATION-LITE-DB1.IPV6.CSV.zip and b/VpnHood.Client.App/Resources/IP2LOCATION-LITE-DB1.IPV6.CSV.zip differ diff --git a/VpnHood.Client.App/Services/AppAuthenticationService.cs b/VpnHood.Client.App/Services/AppAuthenticationService.cs index 63bb70700..99159fc37 100644 --- a/VpnHood.Client.App/Services/AppAuthenticationService.cs +++ b/VpnHood.Client.App/Services/AppAuthenticationService.cs @@ -19,7 +19,7 @@ public async Task SignInWithGoogle(IUiContext uiContext) public async Task SignOut(IUiContext uiContext) { await accountService.SignOut(uiContext); - await vpnHoodApp.RefreshAccount(); + await vpnHoodApp.RefreshAccount(updateCurrentClientProfile: true); } public void Dispose() diff --git a/VpnHood.Client.App/Services/AppBaseUiService.cs b/VpnHood.Client.App/Services/AppUiServiceBase.cs similarity index 95% rename from VpnHood.Client.App/Services/AppBaseUiService.cs rename to VpnHood.Client.App/Services/AppUiServiceBase.cs index c6320c674..b2da65802 100644 --- a/VpnHood.Client.App/Services/AppBaseUiService.cs +++ b/VpnHood.Client.App/Services/AppUiServiceBase.cs @@ -3,7 +3,7 @@ namespace VpnHood.Client.App.Services; -internal class AppBaseUiService +internal class AppUiServiceBase : IAppUiService { public bool IsQuickLaunchSupported => false; diff --git a/VpnHood.Client.App/Settings/AppSettings.cs b/VpnHood.Client.App/Settings/AppSettings.cs index b2eccca05..5033b47ed 100644 --- a/VpnHood.Client.App/Settings/AppSettings.cs +++ b/VpnHood.Client.App/Settings/AppSettings.cs @@ -8,7 +8,7 @@ namespace VpnHood.Client.App.Settings; public class AppSettings { private readonly object _saveLock = new(); - public event EventHandler? Saved; + public event EventHandler? BeforeSave; [JsonIgnore] public string SettingsFilePath { get; private set; } = null!; @@ -18,19 +18,17 @@ public class AppSettings public DateTime ConfigTime { get; set; } = DateTime.Now; public UserSettings UserSettings { get; set; } = new(); public Guid ClientId { get; set; } = Guid.NewGuid(); - public string? LastCountryIpGroupId { get; set; } - public void Save() { + BeforeSave?.Invoke(this, EventArgs.Empty); + lock (_saveLock) { ConfigTime = DateTime.Now; var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(SettingsFilePath, json, Encoding.UTF8); } - - Saved?.Invoke(this, EventArgs.Empty); } internal static AppSettings Load(string settingsFilePath) diff --git a/VpnHood.Client.App/Settings/UserSettings.cs b/VpnHood.Client.App/Settings/UserSettings.cs index 3fe794898..d2aa1b1f4 100644 --- a/VpnHood.Client.App/Settings/UserSettings.cs +++ b/VpnHood.Client.App/Settings/UserSettings.cs @@ -10,18 +10,17 @@ public class UserSettings public AppLogSettings Logging { get; set; } = new(); public string? CultureCode { get; set; } public Guid? ClientProfileId { get; set; } - public int MaxReconnectCount { get; set; } = int.MaxValue; + public string? ServerLocation { get; set; } public int MaxDatagramChannelCount { get; set; } = DefaultClientOptions.MaxDatagramChannelCount; public bool TunnelClientCountry { get; set; } = true; - public string[]? IpGroupFilters { get; set; } - public FilterMode IpGroupFiltersMode { get; set; } = FilterMode.All; - public IpRange[]? CustomIpRanges { get; set; } public string[]? AppFilters { get; set; } public FilterMode AppFiltersMode { get; set; } = FilterMode.All; public bool UseUdpChannel { get; set; } = DefaultClientOptions.UseUdpChannel; public bool DropUdpPackets { get; set; } = DefaultClientOptions.DropUdpPackets; public bool IncludeLocalNetwork { get; set; } = DefaultClientOptions.IncludeLocalNetwork; - public IpRange[] PacketCaptureIncludeIpRanges { get; set; } = IpNetwork.All.ToIpRanges().ToArray(); + public IpRange[]? IncludeIpRanges { get; set; } = IpNetwork.All.ToIpRanges().ToArray(); + public IpRange[]? ExcludeIpRanges { get; set; } = IpNetwork.None.ToIpRanges().ToArray(); + public IpRange[]? PacketCaptureIncludeIpRanges { get; set; } = IpNetwork.All.ToIpRanges().ToArray(); public IpRange[]? PacketCaptureExcludeIpRanges { get; set; } = IpNetwork.None.ToIpRanges().ToArray(); public bool AllowAnonymousTracker { get; set; } = DefaultClientOptions.AllowAnonymousTracker; public IPAddress[]? DnsServers { get; set; } diff --git a/VpnHood.Client.App/VpnHood.Client.App.csproj b/VpnHood.Client.App/VpnHood.Client.App.csproj index 9d3aae6e4..f02614e7f 100644 --- a/VpnHood.Client.App/VpnHood.Client.App.csproj +++ b/VpnHood.Client.App/VpnHood.Client.App.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App/VpnHoodApp.cs b/VpnHood.Client.App/VpnHoodApp.cs index d2f1b065a..0324a00ac 100644 --- a/VpnHood.Client.App/VpnHoodApp.cs +++ b/VpnHood.Client.App/VpnHoodApp.cs @@ -1,7 +1,6 @@ using System.Globalization; using System.IO.Compression; using System.Net; -using System.Net.Sockets; using System.Text.Json; using Microsoft.Extensions.Logging; using VpnHood.Client.Abstractions; @@ -12,6 +11,7 @@ using VpnHood.Client.Diagnosing; using VpnHood.Common; using VpnHood.Common.Exceptions; +using VpnHood.Common.IpLocations; using VpnHood.Common.Jobs; using VpnHood.Common.Logging; using VpnHood.Common.Messaging; @@ -30,29 +30,32 @@ public class VpnHoodApp : Singleton, private const string FileNamePersistState = "state.json"; private const string FolderNameProfiles = "profiles"; private readonly SocketFactory? _socketFactory; - private readonly bool _loadCountryIpGroups; + private readonly bool _useIpGroupManager; + private readonly bool _useExternalLocationService; private readonly string? _appGa4MeasurementId; - private bool _hasAnyDataArrived; private bool _hasConnectRequested; private bool _hasDiagnoseStarted; private bool _hasDisconnectedByUser; private Guid? _activeClientProfileId; + private string? _activeServerLocation; private DateTime? _connectRequestTime; private IpGroupManager? _ipGroupManager; private bool _isConnecting; private bool _isDisconnecting; private SessionStatus? _lastSessionStatus; - private IpGroup? _lastCountryIpGroup; private AppConnectionState _lastConnectionState; private bool _isLoadingIpGroup; private readonly TimeSpan _versionCheckInterval; private readonly AppPersistState _appPersistState; + private readonly TimeSpan _reconnectTimeout; + private readonly TimeSpan _autoWaitTimeout; private CancellationTokenSource? _connectCts; - private DateTime? _connectedTime; private ClientProfile? _currentClientProfile; private VersionCheckResult? _versionCheckResult; - private VpnHoodClient? Client => ClientConnect?.Client; - private SessionStatus? LastSessionStatus => Client?.SessionStatus ?? _lastSessionStatus; + private VpnHoodClient? _client; + private readonly bool? _logVerbose; + private readonly bool? _logAnonymous; + private SessionStatus? LastSessionStatus => _client?.SessionStatus ?? _lastSessionStatus; private string TempFolderPath => Path.Combine(StorageFolderPath, "Temp"); private string IpGroupsFolderPath => Path.Combine(TempFolderPath, "ipgroups"); private string VersionCheckFilePath => Path.Combine(StorageFolderPath, "version.json"); @@ -60,7 +63,6 @@ public class VpnHoodApp : Singleton, public event EventHandler? ConnectionStateChanged; public event EventHandler? UiHasChanged; public bool IsIdle => ConnectionState == AppConnectionState.None; - public VpnHoodConnect? ClientConnect { get; private set; } public TimeSpan SessionTimeout { get; set; } public Diagnoser Diagnoser { get; set; } = new(); public string StorageFolderPath { get; } @@ -74,6 +76,7 @@ public class VpnHoodApp : Singleton, public AppLogService LogService { get; } public AppResource Resource { get; } public AppServices Services { get; } + public DateTime? ConnectedTime { get; private set; } public IUiContext? UiContext { get; set; } public IUiContext RequiredUiContext => UiContext ?? throw new Exception("The main window app does not exists."); @@ -89,15 +92,20 @@ private VpnHoodApp(IDevice device, AppOptions? options = default) StorageFolderPath = options.StorageFolderPath ?? throw new ArgumentNullException(nameof(options.StorageFolderPath)); Settings = AppSettings.Load(Path.Combine(StorageFolderPath, FileNameSettings)); - Settings.Saved += Settings_Saved; + Settings.BeforeSave += SettingsBeforeSave; ClientProfileService = new ClientProfileService(Path.Combine(StorageFolderPath, FolderNameProfiles)); SessionTimeout = options.SessionTimeout; _socketFactory = options.SocketFactory; - _loadCountryIpGroups = options.LoadCountryIpGroups; + _useIpGroupManager = options.UseIpGroupManager; + _useExternalLocationService = options.UseExternalLocationService; _appGa4MeasurementId = options.AppGa4MeasurementId; _versionCheckInterval = options.VersionCheckInterval; + _reconnectTimeout = options.ReconnectTimeout; + _autoWaitTimeout = options.AutoWaitTimeout; _appPersistState = AppPersistState.Load(Path.Combine(StorageFolderPath, FileNamePersistState)); _versionCheckResult = VhUtil.JsonDeserializeFile(VersionCheckFilePath); + _logVerbose = options.LogVerbose; + _logAnonymous = options.LogAnonymous; Diagnoser.StateChanged += (_, _) => FireConnectionStateChanged(); LogService = new AppLogService(Path.Combine(StorageFolderPath, FileNameLog)); @@ -113,16 +121,20 @@ private VpnHoodApp(IDevice device, AppOptions? options = default) // create start up logger if (!device.IsLogToConsoleSupported) UserSettings.Logging.LogToConsole = false; - LogService.Start(Settings.UserSettings.Logging, false); + LogService.Start(new AppLogSettings + { + LogVerbose = options.LogVerbose ?? Settings.UserSettings.Logging.LogVerbose, + LogAnonymous = options.LogAnonymous ?? Settings.UserSettings.Logging.LogAnonymous, + LogToConsole = UserSettings.Logging.LogToConsole, + LogToFile = UserSettings.Logging.LogToFile + }); // add default test public server if not added yet - ClientProfileService.TryRemoveByTokenId( - "5aacec55-5cac-457a-acad-3976969236f8"); //remove obsoleted public server + ClientProfileService.TryRemoveByTokenId("5aacec55-5cac-457a-acad-3976969236f8"); //remove obsoleted public server var builtInProfileIds = ClientProfileService.ImportBuiltInAccessKeys(options.AccessKeys); - Settings.UserSettings.ClientProfileId ??= - builtInProfileIds.FirstOrDefault()?.ClientProfileId; // set first one as default + Settings.UserSettings.ClientProfileId ??= builtInProfileIds.FirstOrDefault()?.ClientProfileId; // set first one as default - var uiService = options.UiService ?? new AppBaseUiService(); + var uiService = options.UiService ?? new AppUiServiceBase(); // initialize features Features = new AppFeatures @@ -138,7 +150,7 @@ private VpnHoodApp(IDevice device, AppOptions? options = default) IsBillingSupported = options.AccountService?.Billing != null, IsQuickLaunchSupported = uiService.IsQuickLaunchSupported, IsNotificationSupported = uiService.IsNotificationSupported, - IsAlwaysOnSupported = device.IsAlwaysOnSupported, + IsAlwaysOnSupported = device.IsAlwaysOnSupported }; // initialize services @@ -149,7 +161,7 @@ private VpnHoodApp(IDevice device, AppOptions? options = default) AccountService = options.AccountService != null ? new AppAccountService(this, options.AccountService) : null, UpdaterService = options.UpdaterService, - UiService = uiService, + UiService = uiService }; // Clear last update status if version has changed @@ -179,12 +191,13 @@ public AppState State get { var connectionState = ConnectionState; - return new AppState + var appState = new AppState { ConfigTime = Settings.ConfigTime, ConnectionState = connectionState, IsIdle = IsIdle, CanConnect = connectionState is AppConnectionState.None, + CanDiagnose = !_hasDiagnoseStarted && (connectionState is AppConnectionState.None or AppConnectionState.Connected or AppConnectionState.Connecting), CanDisconnect = !_isDisconnecting && (connectionState is AppConnectionState.Connected or AppConnectionState.Connecting or AppConnectionState.Diagnosing or AppConnectionState.Waiting), @@ -193,16 +206,16 @@ is AppConnectionState.Connected or AppConnectionState.Connecting LastError = _appPersistState.LastErrorMessage, HasDiagnoseStarted = _hasDiagnoseStarted, HasDisconnectedByUser = _hasDisconnectedByUser, - HasProblemDetected = _hasConnectRequested && IsIdle && - (_hasDiagnoseStarted || _appPersistState.LastErrorMessage != null), + HasProblemDetected = _hasConnectRequested && IsIdle && (_hasDiagnoseStarted || _appPersistState.LastErrorMessage != null), SessionStatus = LastSessionStatus, - Speed = Client?.Stat.Speed ?? new Traffic(), - AccountTraffic = Client?.Stat.AccountTraffic ?? new Traffic(), - SessionTraffic = Client?.Stat.SessionTraffic ?? new Traffic(), - ClientIpGroup = _lastCountryIpGroup, - IsWaitingForAd = Client?.Stat.IsWaitingForAd is true, + Speed = _client?.Stat.Speed ?? new Traffic(), + AccountTraffic = _client?.Stat.AccountTraffic ?? new Traffic(), + SessionTraffic = _client?.Stat.SessionTraffic ?? new Traffic(), + ClientCountryCode = _appPersistState.ClientCountryCode, + ClientCountryName = _appPersistState.ClientCountryName, + IsWaitingForAd = _client?.Stat.IsWaitingForAd is true, ConnectRequestTime = _connectRequestTime, - IsUdpChannelSupported = Client?.Stat.IsUdpChannelSupported, + IsUdpChannelSupported = _client?.Stat.IsUdpChannelSupported, CurrentUiCultureInfo = new UiCultureInfo(CultureInfo.DefaultThreadCurrentUICulture), SystemUiCultureInfo = new UiCultureInfo(SystemUiCulture), VersionStatus = _versionCheckResult?.VersionStatus ?? VersionStatus.Unknown, @@ -210,7 +223,13 @@ is AppConnectionState.Connected or AppConnectionState.Connecting LastPublishInfo = _versionCheckResult?.VersionStatus is VersionStatus.Deprecated or VersionStatus.Old ? _versionCheckResult.PublishInfo : null, + ServerLocationInfo = _client?.Stat.ServerLocationInfo, + ClientServerLocationInfo = UserSettings.ServerLocation is null + ? CurrentClientProfile?.ServerLocationInfos.FirstOrDefault(x => x.IsDefault) + : CurrentClientProfile?.ServerLocationInfos.FirstOrDefault(x => x.ServerLocation == UserSettings.ServerLocation) }; + + return appState; } } @@ -220,11 +239,11 @@ public AppConnectionState ConnectionState { if (_isLoadingIpGroup) return AppConnectionState.Initializing; if (Diagnoser.IsWorking) return AppConnectionState.Diagnosing; - if (_isDisconnecting || Client?.State == ClientState.Disconnecting) return AppConnectionState.Disconnecting; - if (_isConnecting || Client?.State == ClientState.Connecting) return AppConnectionState.Connecting; - if (Client?.Stat.IsWaitingForAd is true) return AppConnectionState.Connecting; - if (Client?.State == ClientState.Connected) return AppConnectionState.Connected; - if (ClientConnect?.IsWaiting is true) return AppConnectionState.Waiting; + if (_isDisconnecting || _client?.State == ClientState.Disconnecting) return AppConnectionState.Disconnecting; + if (_isConnecting || _client?.State == ClientState.Connecting) return AppConnectionState.Connecting; + if (_client?.State == ClientState.Waiting) return AppConnectionState.Waiting; + if (_client?.Stat.IsWaitingForAd is true) return AppConnectionState.Connecting; + if (_client?.State == ClientState.Connected) return AppConnectionState.Connected; return AppConnectionState.None; } } @@ -242,11 +261,10 @@ public async ValueTask DisposeAsync() { await Disconnect(); Device.Dispose(); + LogService.Dispose(); DisposeSingleton(); } - public event EventHandler? ClientConnectCreated; - public static VpnHoodApp Init(IDevice device, AppOptions? options = default) { return new VpnHoodApp(device, options); @@ -272,9 +290,11 @@ public void ClearLastError() _appPersistState.LastErrorMessage = null; _hasDiagnoseStarted = false; _hasDisconnectedByUser = false; + _hasConnectRequested = false; + _connectRequestTime = null; } - public async Task Connect(Guid? clientProfileId = null, bool diagnose = false, + public async Task Connect(Guid? clientProfileId = null, string? serverLocation = null, bool diagnose = false, string? userAgent = default, bool throwException = true, CancellationToken cancellationToken = default) { // disconnect current connection @@ -284,15 +304,16 @@ public async Task Connect(Guid? clientProfileId = null, bool diagnose = false, // request features for the first time await RequestFeatures(cancellationToken); - // set default profileId to clientProfileId if not set + // set use default clientProfile and serverLocation + serverLocation ??= UserSettings.ServerLocation; clientProfileId ??= UserSettings.ClientProfileId; - var clientProfile = ClientProfileService.FindById(clientProfileId ?? Guid.Empty) - ?? throw new NotExistsException("Could not find any VPN profile to connect."); + var clientProfile = ClientProfileService.FindById(clientProfileId ?? Guid.Empty) ?? throw new NotExistsException("Could not find any VPN profile to connect."); // set current profile only if it has been updated to avoid unnecessary new config time - if (clientProfile.ClientProfileId != UserSettings.ClientProfileId) + if (clientProfile.ClientProfileId != UserSettings.ClientProfileId || serverLocation != UserSettings.ServerLocation) { UserSettings.ClientProfileId = clientProfile.ClientProfileId; + UserSettings.ServerLocation = serverLocation; Settings.Save(); } @@ -301,14 +322,21 @@ public async Task Connect(Guid? clientProfileId = null, bool diagnose = false, // prepare logger ClearLastError(); _activeClientProfileId = clientProfileId; + _activeServerLocation = State.ClientServerLocationInfo?.ServerLocation; _isConnecting = true; - _hasAnyDataArrived = false; _hasDisconnectedByUser = false; _hasConnectRequested = true; _hasDiagnoseStarted = diagnose; _connectRequestTime = DateTime.Now; FireConnectionStateChanged(); - LogService.Start(Settings.UserSettings.Logging, diagnose); + LogService.Start(new AppLogSettings + { + LogVerbose = _logVerbose ?? Settings.UserSettings.Logging.LogVerbose | diagnose, + LogAnonymous = _logAnonymous ?? Settings.UserSettings.Logging.LogAnonymous, + LogToConsole = UserSettings.Logging.LogToConsole, + LogToFile = UserSettings.Logging.LogToFile | diagnose + }); + // log general info VhLogger.Instance.LogInformation("AppVersion: {AppVersion}", GetType().Assembly.GetName().Version); @@ -329,23 +357,8 @@ public async Task Connect(Guid? clientProfileId = null, bool diagnose = false, using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _connectCts.Token); cancellationToken = linkedCts.Token; - // create packet capture - var packetCapture = await Device.CreatePacketCapture(UiContext); - - // init packet capture - if (packetCapture.IsMtuSupported) - packetCapture.Mtu = TunnelDefaults.MtuWithoutFragmentation; - - // App filters - if (packetCapture.CanExcludeApps && UserSettings.AppFiltersMode == FilterMode.Exclude) - packetCapture.ExcludeApps = UserSettings.AppFilters; - - if (packetCapture.CanIncludeApps && UserSettings.AppFiltersMode == FilterMode.Include) - packetCapture.IncludeApps = UserSettings.AppFilters; - // connect - await ConnectInternal(packetCapture, clientProfile.Token, clientProfile.RegionId, userAgent, true, - cancellationToken); + await ConnectInternal(clientProfile.Token, _activeServerLocation, userAgent, true, cancellationToken); } catch (Exception ex) { @@ -370,6 +383,107 @@ await ConnectInternal(packetCapture, clientProfile.Token, clientProfile.RegionId } } + private async Task CreatePacketCapture() + { + // create packet capture + var packetCapture = await Device.CreatePacketCapture(UiContext); + + // init packet capture + if (packetCapture.IsMtuSupported) + packetCapture.Mtu = TunnelDefaults.MtuWithoutFragmentation; + + // App filters + if (packetCapture.CanExcludeApps && UserSettings.AppFiltersMode == FilterMode.Exclude) + packetCapture.ExcludeApps = UserSettings.AppFilters; + + if (packetCapture.CanIncludeApps && UserSettings.AppFiltersMode == FilterMode.Include) + packetCapture.IncludeApps = UserSettings.AppFilters; + + return packetCapture; + } + + private async Task ConnectInternal(Token token, string? serverLocationInfo, string? userAgent, + bool allowUpdateToken, CancellationToken cancellationToken) + { + // show token info + VhLogger.Instance.LogInformation( + $"TokenId: {VhLogger.FormatId(token.TokenId)}, SupportId: {VhLogger.FormatId(token.SupportId)}"); + + // calculate packetCaptureIpRanges + var packetCaptureIpRanges = IpNetwork.All.ToIpRanges(); + if (!VhUtil.IsNullOrEmpty(UserSettings.PacketCaptureIncludeIpRanges)) + packetCaptureIpRanges = packetCaptureIpRanges.Intersect(UserSettings.PacketCaptureIncludeIpRanges); + if (!VhUtil.IsNullOrEmpty(UserSettings.PacketCaptureExcludeIpRanges)) + packetCaptureIpRanges = packetCaptureIpRanges.Exclude(UserSettings.PacketCaptureExcludeIpRanges); + + // create clientOptions + var clientOptions = new ClientOptions + { + SessionTimeout = SessionTimeout, + ReconnectTimeout = _reconnectTimeout, + AutoWaitTimeout = _autoWaitTimeout, + IncludeLocalNetwork = UserSettings.IncludeLocalNetwork, + IpRangeProvider = this, + AdProvider = this, + PacketCaptureIncludeIpRanges = packetCaptureIpRanges.ToArray(), + MaxDatagramChannelCount = UserSettings.MaxDatagramChannelCount, + ConnectTimeout = TcpTimeout, + AllowAnonymousTracker = UserSettings.AllowAnonymousTracker, + DropUdpPackets = UserSettings.DebugData1?.Contains("/drop-udp") == true || UserSettings.DropUdpPackets, + AppGa4MeasurementId = _appGa4MeasurementId, + ServerLocation = serverLocationInfo == ServerLocationInfo.Auto.ServerLocation ? null : serverLocationInfo, + UseUdpChannel = UserSettings.UseUdpChannel + }; + + if (_socketFactory != null) clientOptions.SocketFactory = _socketFactory; + if (userAgent != null) clientOptions.UserAgent = userAgent; + + // Create Client with a new PacketCapture + if (_client != null) throw new Exception("Last client has not been disposed properly."); + var packetCapture = await CreatePacketCapture(); + _client = new VpnHoodClient(packetCapture, Settings.ClientId, token, clientOptions); + _client.StateChanged += Client_StateChanged; + + try + { + if (_hasDiagnoseStarted) + await Diagnoser.Diagnose(_client, cancellationToken); + else + await Diagnoser.Connect(_client, cancellationToken); + + // set connected time + ConnectedTime = DateTime.Now; + + // update access token if ResponseAccessKey is set + if (_client.ResponseAccessKey != null) + token = ClientProfileService.UpdateTokenByAccessKey(token, _client.ResponseAccessKey); + + // check version after first connection + _ = VersionCheck(); + } + catch (Exception) when (_client is null) + { + packetCapture.Dispose(); // don't miss to dispose when there is no client to handle it + } + catch (Exception) + { + await _client.DisposeAsync(); + _client = null; + + // try to update token from url after connection or error if ResponseAccessKey is not set + // check _client is not null to make sure + if (allowUpdateToken && !string.IsNullOrEmpty(token.ServerToken.Url) && + await ClientProfileService.UpdateServerTokenByUrl(token)) + { + token = ClientProfileService.GetToken(token.TokenId); + await ConnectInternal(token, serverLocationInfo, userAgent, false, cancellationToken); + return; + } + + throw; + } + } + private async Task RequestFeatures(CancellationToken cancellationToken) { // QuickLaunch @@ -418,141 +532,73 @@ private void InitCulture() // set default culture var firstSelected = Services.AppCultureService.SelectedCultures.FirstOrDefault(); CultureInfo.CurrentUICulture = (firstSelected != null) ? new CultureInfo(firstSelected) : SystemUiCulture; - CultureInfo.DefaultThreadCurrentUICulture = - new CultureInfo(Services.AppCultureService.SelectedCultures.FirstOrDefault() ?? "en"); CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CurrentUICulture; // sync UserSettings from the System App Settings UserSettings.CultureCode = firstSelected?.Split("-").FirstOrDefault(); } - private void Settings_Saved(object sender, EventArgs e) + private void SettingsBeforeSave(object sender, EventArgs e) { - if (Client != null) + var state = State; + if (_client != null) { - Client.UseUdpChannel = UserSettings.UseUdpChannel; - Client.DropUdpPackets = UserSettings.DebugData1?.Contains("/drop-udp") == true || UserSettings.DropUdpPackets; + var client = _client; // it may get null + client.UseUdpChannel = UserSettings.UseUdpChannel; + client.DropUdpPackets = UserSettings.DebugData1?.Contains("/drop-udp") == true || UserSettings.DropUdpPackets; - //ClientProfileId has been changed - if (!IsIdle && _activeClientProfileId != null && UserSettings.ClientProfileId != _activeClientProfileId) - _ = Disconnect(true); + // check is disconnect required + var disconnectRequired = + (_activeClientProfileId != null && UserSettings.ClientProfileId != _activeClientProfileId) || //ClientProfileId has been changed + (state.CanDisconnect && _activeServerLocation != state.ClientServerLocationInfo?.ServerLocation) || //ClientProfileId has been changed + (state.CanDisconnect && UserSettings.IncludeLocalNetwork != client.IncludeLocalNetwork); // IncludeLocalNetwork has been changed - // IncludeLocalNetwork has been changed - if (!IsIdle && UserSettings.IncludeLocalNetwork != Client.IncludeLocalNetwork) + // disconnect + if (state.CanDisconnect && disconnectRequired) _ = Disconnect(true); } //lets refresh clientProfile _currentClientProfile = null; + // set ServerLocation to null if the item is SameAsGlobalAuto + if (UserSettings.ServerLocation != null && + CurrentClientProfile?.ServerLocationInfos.FirstOrDefault(x => x.ServerLocation == UserSettings.ServerLocation)?.IsDefault == true) + UserSettings.ServerLocation = null; + // sync culture to app settings Services.AppCultureService.SelectedCultures = UserSettings.CultureCode != null ? [UserSettings.CultureCode] : []; - InitCulture(); - } - - private async Task GetClientCountry() - { - try - { - var ipAddress = - await IPAddressUtil.GetPublicIpAddress(AddressFamily.InterNetwork) ?? - await IPAddressUtil.GetPublicIpAddress(AddressFamily.InterNetworkV6); - if (ipAddress == null) - return null; - - var ipGroupManager = await GetIpGroupManager(); - _lastCountryIpGroup = await ipGroupManager.FindIpGroup(ipAddress, Settings.LastCountryIpGroupId); - Settings.LastCountryIpGroupId = _lastCountryIpGroup?.IpGroupId; - return _lastCountryIpGroup?.IpGroupName; - } - catch (Exception ex) - { - VhLogger.Instance.LogError(ex, "Could not retrieve client country from public ip services."); - return null; - } + InitCulture(); } - private async Task ConnectInternal(IPacketCapture packetCapture, Token token, string? regionId, string? userAgent, - bool allowUpdateToken, CancellationToken cancellationToken) + public async Task GetClientCountry() { - // show token info - VhLogger.Instance.LogInformation( - $"TokenId: {VhLogger.FormatId(token.TokenId)}, SupportId: {VhLogger.FormatId(token.SupportId)}"); - - // calculate packetCaptureIpRanges - var packetCaptureIpRanges = IpNetwork.All.ToIpRanges(); - if (!VhUtil.IsNullOrEmpty(UserSettings.PacketCaptureIncludeIpRanges)) - packetCaptureIpRanges = packetCaptureIpRanges.Intersect(UserSettings.PacketCaptureIncludeIpRanges); - if (!VhUtil.IsNullOrEmpty(UserSettings.PacketCaptureExcludeIpRanges)) - packetCaptureIpRanges = packetCaptureIpRanges.Exclude(UserSettings.PacketCaptureExcludeIpRanges); - - // create clientOptions - var clientOptions = new ClientOptions + // try to get by external service + if (_appPersistState.ClientCountryCode == null && _useExternalLocationService) { - SessionTimeout = SessionTimeout, - IncludeLocalNetwork = UserSettings.IncludeLocalNetwork, - IpRangeProvider = this, - AdProvider = this, - PacketCaptureIncludeIpRanges = packetCaptureIpRanges.ToArray(), - MaxDatagramChannelCount = UserSettings.MaxDatagramChannelCount, - ConnectTimeout = TcpTimeout, - AllowAnonymousTracker = UserSettings.AllowAnonymousTracker, - DropUdpPackets = UserSettings.DebugData1?.Contains("/drop-udp") == true || UserSettings.DropUdpPackets, - AppGa4MeasurementId = _appGa4MeasurementId, - RegionId = regionId - }; - - if (_socketFactory != null) clientOptions.SocketFactory = _socketFactory; - if (userAgent != null) clientOptions.UserAgent = userAgent; - - // Create Client - var clientConnect = new VpnHoodConnect( - packetCapture, - Settings.ClientId, - token, - clientOptions, - new ConnectOptions + try { - MaxReconnectCount = UserSettings.MaxReconnectCount, - UdpChannelMode = UserSettings.UseUdpChannel ? UdpChannelMode.On : UdpChannelMode.Off - }); - - ClientConnectCreated?.Invoke(this, EventArgs.Empty); - clientConnect.StateChanged += ClientConnect_StateChanged; - ClientConnect = clientConnect; // set here to allow disconnection - - try - { - if (_hasDiagnoseStarted) - await Diagnoser.Diagnose(clientConnect, cancellationToken); - else - await Diagnoser.Connect(clientConnect, cancellationToken); - - // set connected time - _connectedTime = DateTime.Now; - - // update access token if ResponseAccessKey is set - if (clientConnect.Client.ResponseAccessKey != null) - token = ClientProfileService.UpdateTokenByAccessKey(token, clientConnect.Client.ResponseAccessKey); - - // check version after first connection - _ = VersionCheck(); - } - catch - { - // try to update token from url after connection or error if ResponseAccessKey is not set - if (!string.IsNullOrEmpty(token.ServerToken.Url) && allowUpdateToken && - await ClientProfileService.UpdateServerTokenByUrl(token)) + var ipLocationProvider = new IpLocationProviderFactory().CreateDefault("VpnHood-Client"); + var ipLocation = await ipLocationProvider.GetLocation(new HttpClient()); + _appPersistState.ClientCountryCode = ipLocation.CountryCode; + } + catch (Exception ex) { - token = ClientProfileService.GetToken(token.TokenId); - await ConnectInternal(packetCapture, token, regionId, userAgent, false, cancellationToken); - return; + VhLogger.Instance.LogError(ex, "Could not get country code from IpApi service."); } + } - throw; + // try to get by ip group + if (_appPersistState.ClientCountryCode == null && _useIpGroupManager) + { + var ipGroupManager = await GetIpGroupManager(); + _appPersistState.ClientCountryCode ??= await ipGroupManager.GetCountryCodeByCurrentIp(); } + + // return last country + return _appPersistState.ClientCountryName; } public async Task ShowAd(string sessionId, CancellationToken cancellationToken) @@ -563,63 +609,24 @@ public async Task ShowAd(string sessionId, CancellationToken cancellatio return adData; } - private void ClientConnect_StateChanged(object sender, EventArgs e) - { - if (ClientConnect?.IsDisposed == true) - _ = Disconnect(); - else - FireConnectionStateChanged(); - } - - private async Task GetIncludeIpRanges(FilterMode filterMode, string[]? ipGroupIds) - { - if (filterMode == FilterMode.All || VhUtil.IsNullOrEmpty(ipGroupIds)) - return null; - - if (filterMode == FilterMode.Include) - return await GetIpRanges(ipGroupIds); - - return IpRange.Invert(await GetIpRanges(ipGroupIds)).ToArray(); - } - - private async Task GetIpRanges(IEnumerable ipGroupIds) - { - var ipRanges = new List(); - foreach (var ipGroupId in ipGroupIds) - try - { - if (ipGroupId.Equals("custom", StringComparison.OrdinalIgnoreCase)) - ipRanges.AddRange(UserSettings.CustomIpRanges ?? []); - else - { - var ipGroupManager = await GetIpGroupManager(); - ipRanges.AddRange(await ipGroupManager.GetIpRanges(ipGroupId)); - } - } - catch (Exception ex) - { - VhLogger.Instance.LogError(ex, $"Could not add {nameof(IpRange)} of Group {ipGroupId}"); - } - - return IpRange.Sort(ipRanges).ToArray(); - } - - private readonly object _disconnectLock = new(); - private Task? _disconnectTask; - - public Task Disconnect(bool byUser = false) + private void Client_StateChanged(object sender, EventArgs e) { - lock (_disconnectLock) + // do not disconnect in _isConnecting by ClientState, because the AutoUploadToken from url & reconnect may inprogress + // _isConnecting will disconnect the connection by try catch + if (_client?.State == ClientState.Disposed && !_isConnecting) { - if (_disconnectTask == null || _disconnectTask.IsCompleted) - _disconnectTask = DisconnectCore(byUser); + _ = Disconnect(); + return; } - return _disconnectTask; + FireConnectionStateChanged(); } - private async Task DisconnectCore(bool byUser) + + private readonly AsyncLock _disconnectLock = new(); + public async Task Disconnect(bool byUser = false) { + using var lockAsync = await _disconnectLock.LockAsync(); if (_isDisconnecting || IsIdle) return; @@ -634,45 +641,34 @@ private async Task DisconnectCore(bool byUser) _isDisconnecting = true; FireConnectionStateChanged(); - // check for any success - if (Client != null && _connectedTime != null) - { - _hasAnyDataArrived = Client.Stat.SessionTraffic.Received > 1000; - if (_appPersistState.LastErrorMessage == null && !_hasAnyDataArrived && UserSettings is - { IpGroupFiltersMode: FilterMode.All, TunnelClientCountry: true }) - _appPersistState.LastErrorMessage = "No data has been received."; - } - // check diagnose if (_hasDiagnoseStarted && _appPersistState.LastErrorMessage == null) _appPersistState.LastErrorMessage = "Diagnoser has finished and no issue has been detected."; - // close client - try - { - // cancel current connecting if any - _connectCts?.Cancel(); + // cancel current connecting if any + _connectCts?.Cancel(); - // do not wait for bye if user request disconnection - if (ClientConnect != null) - await ClientConnect.DisposeAsync(waitForBye: !byUser); - } - catch (Exception ex) - { - VhLogger.Instance.LogError(GeneralEventId.Session, ex, "Could not dispose the client properly."); - } + // close client + // do not wait for bye if user request disconnection + if (_client != null) + await _client.DisposeAsync(waitForBye: !byUser); LogService.Stop(); } + catch (Exception ex) + { + VhLogger.Instance.LogError(ex, "Error in disconnecting."); + } finally { _appPersistState.LastErrorMessage ??= LastSessionStatus?.ErrorMessage; _activeClientProfileId = null; - _lastSessionStatus = Client?.SessionStatus; + _activeServerLocation = null; + _lastSessionStatus = _client?.SessionStatus; _isConnecting = false; _isDisconnecting = false; - _connectedTime = null; - ClientConnect = null; + ConnectedTime = null; + _client = null; FireConnectionStateChanged(); } } @@ -692,7 +688,7 @@ private async Task GetIpGroupManager() _ipGroupManager = await IpGroupManager.Create(IpGroupsFolderPath); // ignore country ip groups if not required usually by tests - if (!_loadCountryIpGroups) + if (!_useIpGroupManager) return _ipGroupManager; // AddFromIp2Location if hash has been changed @@ -809,19 +805,24 @@ public async Task VersionCheck(bool force = false) public async Task GetIncludeIpRanges(IPAddress clientIp) { - // use TunnelMyCountry - if (UserSettings.TunnelClientCountry) - return await GetIncludeIpRanges(UserSettings.IpGroupFiltersMode, UserSettings.IpGroupFilters); + // calculate packetCaptureIpRanges + var ipRanges = IpNetwork.All.ToIpRanges(); + if (!VhUtil.IsNullOrEmpty(UserSettings.IncludeIpRanges)) ipRanges = ipRanges.Intersect(UserSettings.IncludeIpRanges); + if (!VhUtil.IsNullOrEmpty(UserSettings.ExcludeIpRanges)) ipRanges = ipRanges.Exclude(UserSettings.ExcludeIpRanges); - // Exclude my country - var ipGroupManager = await GetIpGroupManager(); - _lastCountryIpGroup = await ipGroupManager.FindIpGroup(clientIp, Settings.LastCountryIpGroupId); - Settings.LastCountryIpGroupId = _lastCountryIpGroup?.IpGroupId; - VhLogger.Instance.LogInformation($"Client Country is: {_lastCountryIpGroup?.IpGroupName}"); + // exclude client country IPs + if (!UserSettings.TunnelClientCountry) + { + var ipGroupManager = await GetIpGroupManager(); + var ipGroup = await ipGroupManager.FindIpGroup(clientIp, _appPersistState.ClientCountryCode); + _appPersistState.ClientCountryCode = ipGroup?.IpGroupId; + VhLogger.Instance.LogInformation("Client Country is: {Country}", _appPersistState.ClientCountryName); + if (ipGroup != null) + ipRanges = ipRanges.Exclude(await ipGroupManager.GetIpRanges(ipGroup.IpGroupId)); - return _lastCountryIpGroup != null - ? await GetIncludeIpRanges(FilterMode.Exclude, [_lastCountryIpGroup.IpGroupId]) - : null; + } + + return ipRanges.ToArray(); } public async Task RefreshAccount(bool updateCurrentClientProfile = false) @@ -854,7 +855,7 @@ public async Task RefreshAccount(bool updateCurrentClientProfile = false) Settings.Save(); } } - + // update current profile if removed if (ClientProfileService.FindById(UserSettings.ClientProfileId ?? Guid.Empty) == null) { diff --git a/VpnHood.Client.Device.Android/VpnHood.Client.Device.Android.csproj b/VpnHood.Client.Device.Android/VpnHood.Client.Device.Android.csproj index 309b6283d..223a4a889 100644 --- a/VpnHood.Client.Device.Android/VpnHood.Client.Device.Android.csproj +++ b/VpnHood.Client.Device.Android/VpnHood.Client.Device.Android.csproj @@ -21,7 +21,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.Device.WinDivert/VpnHood.Client.Device.WinDivert.csproj b/VpnHood.Client.Device.WinDivert/VpnHood.Client.Device.WinDivert.csproj index b8793e67b..c2578a805 100644 --- a/VpnHood.Client.Device.WinDivert/VpnHood.Client.Device.WinDivert.csproj +++ b/VpnHood.Client.Device.WinDivert/VpnHood.Client.Device.WinDivert.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -34,12 +34,6 @@ - - - SharpPcap.dll - - - diff --git a/VpnHood.Client.Device.WinDivert/WinDivertPacketCapture.cs b/VpnHood.Client.Device.WinDivert/WinDivertPacketCapture.cs index 10a6722c1..2b52f2101 100644 --- a/VpnHood.Client.Device.WinDivert/WinDivertPacketCapture.cs +++ b/VpnHood.Client.Device.WinDivert/WinDivertPacketCapture.cs @@ -95,8 +95,11 @@ private static string Ip(IpRange ipRange) public void StartCapture() { + if (_disposed) + throw new ObjectDisposedException(VhLogger.FormatType(this)); + if (Started) - throw new InvalidOperationException("_device has been already started!"); + throw new InvalidOperationException("PacketCapture has been already started."); // create include and exclude phrases var phraseX = "true"; @@ -143,7 +146,9 @@ public void Dispose() if (_disposed) return; - StopCapture(); + if (_device.Started) + StopCapture(); + _device.Dispose(); _disposed = true; } diff --git a/VpnHood.Client.Device/VpnHood.Client.Device.csproj b/VpnHood.Client.Device/VpnHood.Client.Device.csproj index 4a7db0a91..72a9067f5 100644 --- a/VpnHood.Client.Device/VpnHood.Client.Device.csproj +++ b/VpnHood.Client.Device/VpnHood.Client.Device.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client/ClientOptions.cs b/VpnHood.Client/ClientOptions.cs index 95ae7ad75..8f23013cb 100644 --- a/VpnHood.Client/ClientOptions.cs +++ b/VpnHood.Client/ClientOptions.cs @@ -7,6 +7,12 @@ namespace VpnHood.Client; public class ClientOptions { + public static ClientOptions Default { get; } = new(); + +#if DEBUG + public int ProtocolVersion { get; set; } +#endif + /// /// a never used IPv4 that must be outside the local network /// @@ -20,6 +26,8 @@ public class ClientOptions public bool AutoDisposePacketCapture { get; set; } = true; public TimeSpan SessionTimeout { get; set; } = TimeSpan.FromDays(3); public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan ReconnectTimeout { get; set; } = TimeSpan.FromSeconds(60); // connect timeout before pause + public TimeSpan AutoWaitTimeout { get; set; } = TimeSpan.FromSeconds(30); // auto resume after pause public Version Version { get; set; } = typeof(ClientOptions).Assembly.GetName().Version; public bool UseUdpChannel { get; set; } public bool IncludeLocalNetwork { get; set; } @@ -34,9 +42,5 @@ public class ClientOptions public bool AllowAnonymousTracker { get; set; } = true; public bool DropUdpPackets { get; set; } public string? AppGa4MeasurementId { get; set; } - public string? RegionId { get; set; } - -#if DEBUG - public int ProtocolVersion { get; set; } -#endif + public string? ServerLocation { get; set; } } \ No newline at end of file diff --git a/VpnHood.Client/ClientState.cs b/VpnHood.Client/ClientState.cs index 94b1a6223..7a2f07abf 100644 --- a/VpnHood.Client/ClientState.cs +++ b/VpnHood.Client/ClientState.cs @@ -5,6 +5,7 @@ public enum ClientState None, Connecting, Connected, + Waiting, Disconnecting, Disposed } \ No newline at end of file diff --git a/VpnHood.Client/ConnectOptions.cs b/VpnHood.Client/ConnectOptions.cs deleted file mode 100644 index 576511246..000000000 --- a/VpnHood.Client/ConnectOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace VpnHood.Client; - -public class ConnectOptions -{ - public int MaxReconnectCount { get; set; } = 3; - public TimeSpan ReconnectDelay { get; set; } = TimeSpan.FromSeconds(20); - public UdpChannelMode UdpChannelMode { get; set; } = UdpChannelMode.Off; -} \ No newline at end of file diff --git a/VpnHood.Client/ConnectorServices/ConnectorService.cs b/VpnHood.Client/ConnectorServices/ConnectorService.cs index 88a607821..9b67671a8 100644 --- a/VpnHood.Client/ConnectorServices/ConnectorService.cs +++ b/VpnHood.Client/ConnectorServices/ConnectorService.cs @@ -10,8 +10,12 @@ namespace VpnHood.Client.ConnectorServices; -internal class ConnectorService(ISocketFactory socketFactory, TimeSpan tcpTimeout) - : ConnectorServiceBase(socketFactory, tcpTimeout) +internal class ConnectorService( + ConnectorEndPointInfo endPointInfo, + ISocketFactory socketFactory, + TimeSpan tcpConnectTimeout, + bool allowTcpReuse = true) + : ConnectorServiceBase(endPointInfo, socketFactory, tcpConnectTimeout, allowTcpReuse) { public async Task> SendRequest(ClientRequest request, CancellationToken cancellationToken) where T : SessionResponse @@ -103,8 +107,9 @@ private static async Task ReadSessionResponse(Stream stream, CancellationT ProcessResponseException(response); return response; } - catch (Exception ex) when (ex is not SessionException) + catch (Exception ex) when (ex is not SessionException && typeof(T) != typeof(SessionResponse)) { + // try to deserialize as a SessionResponse (base) var sessionResponse = VhUtil.JsonDeserialize(message); ProcessResponseException(sessionResponse); throw; diff --git a/VpnHood.Client/ConnectorServices/ConnectorServiceBase.cs b/VpnHood.Client/ConnectorServices/ConnectorServiceBase.cs index 93ae3b541..b76cc8044 100644 --- a/VpnHood.Client/ConnectorServices/ConnectorServiceBase.cs +++ b/VpnHood.Client/ConnectorServices/ConnectorServiceBase.cs @@ -5,9 +5,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.Extensions.Logging; -using VpnHood.Client.Exceptions; using VpnHood.Common.Collections; -using VpnHood.Common.Exceptions; using VpnHood.Common.Jobs; using VpnHood.Common.Logging; using VpnHood.Common.Utils; @@ -22,30 +20,34 @@ namespace VpnHood.Client.ConnectorServices; internal class ConnectorServiceBase : IAsyncDisposable, IJob { private readonly ISocketFactory _socketFactory; + private readonly bool _allowTcpReuse; private readonly ConcurrentQueue _freeClientStreams = new(); protected readonly TaskCollection DisposingTasks = new(); private string _apiKey = ""; - public TimeSpan TcpTimeout { get; set; } - public ConnectorEndPointInfo? EndPointInfo { get; set; } + public TimeSpan TcpConnectTimeout { get; set; } + public ConnectorEndPointInfo EndPointInfo { get; } public JobSection JobSection { get; } public ConnectorStat Stat { get; } public TimeSpan RequestTimeout { get; private set; } public TimeSpan TcpReuseTimeout { get; private set; } public int ServerProtocolVersion { get; private set; } - public ConnectorServiceBase(ISocketFactory socketFactory, TimeSpan tcpTimeout) + public ConnectorServiceBase(ConnectorEndPointInfo endPointInfo, ISocketFactory socketFactory, + TimeSpan tcpConnectTimeout, bool allowTcpReuse) { _socketFactory = socketFactory; + _allowTcpReuse = allowTcpReuse; Stat = new ConnectorStatImpl(this); - TcpTimeout = tcpTimeout; - JobSection = new JobSection(tcpTimeout); + TcpConnectTimeout = tcpConnectTimeout; + JobSection = new JobSection(tcpConnectTimeout); RequestTimeout = TimeSpan.FromSeconds(30); TcpReuseTimeout = TimeSpan.FromSeconds(60); + EndPointInfo = endPointInfo; JobRunner.Default.Add(this); } - public void Init(int serverProtocolVersion, TimeSpan tcpRequestTimeout, TimeSpan tcpReuseTimeout, byte[]? serverSecret) + public void Init(int serverProtocolVersion, TimeSpan tcpRequestTimeout, byte[]? serverSecret, TimeSpan tcpReuseTimeout) { ServerProtocolVersion = serverProtocolVersion; RequestTimeout = tcpRequestTimeout; @@ -55,9 +57,10 @@ public void Init(int serverProtocolVersion, TimeSpan tcpRequestTimeout, TimeSpan private async Task CreateClientStream(TcpClient tcpClient, Stream sslStream, string streamId, CancellationToken cancellationToken) { - var binaryStreamType = string.IsNullOrEmpty(_apiKey) ? BinaryStreamType.None : BinaryStreamType.Standard; const bool useBuffer = true; - const int version = 3; + const int version = 3; + var binaryStreamType = string.IsNullOrEmpty(_apiKey) || !_allowTcpReuse + ? BinaryStreamType.None : BinaryStreamType.Standard; // write HTTP request var header = @@ -71,7 +74,7 @@ private async Task CreateClientStream(TcpClient tcpClient, Stream // Send header and wait for its response await sslStream.WriteAsync(Encoding.UTF8.GetBytes(header), cancellationToken); await HttpUtil.ReadHeadersAsync(sslStream, cancellationToken); - return binaryStreamType == BinaryStreamType.None + return binaryStreamType == BinaryStreamType.None ? new TcpClientStream(tcpClient, sslStream, streamId) : new TcpClientStream(tcpClient, new BinaryStreamStandard(tcpClient.GetStream(), streamId, useBuffer), streamId, ReuseStreamClient); } @@ -85,34 +88,22 @@ protected async Task GetTlsConnectionToServer(string streamId, Ca // create new stream var tcpClient = _socketFactory.CreateTcpClient(tcpEndPoint.AddressFamily); - try - { - // Client.SessionTimeout does not affect in ConnectAsync - VhLogger.Instance.LogTrace(GeneralEventId.Tcp, "Connecting to Server... EndPoint: {EndPoint}", VhLogger.Format(tcpEndPoint)); - - await VhUtil.RunTask(tcpClient.ConnectAsync(tcpEndPoint.Address, tcpEndPoint.Port), TcpTimeout, cancellationToken); - - // Establish a TLS connection - var sslStream = new SslStream(tcpClient.GetStream(), true, UserCertificateValidationCallback); - VhLogger.Instance.LogTrace(GeneralEventId.Tcp, "TLS Authenticating... HostName: {HostName}", VhLogger.FormatHostName(hostName)); - await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions - { - TargetHost = hostName, - EnabledSslProtocols = SslProtocols.None // auto - }, cancellationToken); - - var clientStream = await CreateClientStream(tcpClient, sslStream, streamId, cancellationToken); - lock (Stat) Stat.CreatedConnectionCount++; - return clientStream; - } - catch (MaintenanceException) - { - throw; - } - catch (Exception ex) + // Client.SessionTimeout does not affect in ConnectAsync + VhLogger.Instance.LogTrace(GeneralEventId.Tcp, "Connecting to Server... EndPoint: {EndPoint}", VhLogger.Format(tcpEndPoint)); + await VhUtil.RunTask(tcpClient.ConnectAsync(tcpEndPoint.Address, tcpEndPoint.Port), TcpConnectTimeout, cancellationToken); + + // Establish a TLS connection + var sslStream = new SslStream(tcpClient.GetStream(), true, UserCertificateValidationCallback); + VhLogger.Instance.LogTrace(GeneralEventId.Tcp, "TLS Authenticating... HostName: {HostName}", VhLogger.FormatHostName(hostName)); + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions { - throw new ConnectorEstablishException(ex.Message, ex); - } + TargetHost = hostName, + EnabledSslProtocols = SslProtocols.None // auto + }, cancellationToken); + + var clientStream = await CreateClientStream(tcpClient, sslStream, streamId, cancellationToken); + lock (Stat) Stat.CreatedConnectionCount++; + return clientStream; } protected IClientStream? GetFreeClientStream() @@ -155,12 +146,12 @@ private bool UserCertificateValidationCallback(object sender, X509Certificate ce return true; var ret = sslPolicyErrors == SslPolicyErrors.None || - EndPointInfo?.CertificateHash?.SequenceEqual(certificate.GetCertHash()) == true; + EndPointInfo.CertificateHash?.SequenceEqual(certificate.GetCertHash()) == true; return ret; } - public ValueTask DisposeAsync() + public ValueTask DisposeAsync() { while (_freeClientStreams.TryDequeue(out var queueItem)) DisposingTasks.Add(queueItem.ClientStream.DisposeAsync(false)); @@ -174,7 +165,7 @@ private class ClientStreamItem public DateTime EnqueueTime { get; } = FastDateTime.Now; } - internal class ConnectorStatImpl(ConnectorServiceBase connectorServiceBase) + internal class ConnectorStatImpl(ConnectorServiceBase connectorServiceBase) : ConnectorStat { public override int FreeConnectionCount => connectorServiceBase._freeClientStreams.Count; diff --git a/VpnHood.Client/Diagnosing/Diagnoser.cs b/VpnHood.Client/Diagnosing/Diagnoser.cs index 503b229d5..95974eed4 100644 --- a/VpnHood.Client/Diagnosing/Diagnoser.cs +++ b/VpnHood.Client/Diagnosing/Diagnoser.cs @@ -30,18 +30,23 @@ private set } } - public async Task Connect(VpnHoodConnect clientConnect, CancellationToken cancellationToken) + public async Task Connect(VpnHoodClient vpnHoodClient, CancellationToken cancellationToken) { try { - await clientConnect.Connect(cancellationToken); + await vpnHoodClient.Connect(cancellationToken); } - catch + catch (OperationCanceledException) + { + throw; + } + catch (Exception) { VhLogger.Instance.LogTrace("Checking the Internet connection..."); IsWorking = true; if (!await NetworkCheck()) throw new NoInternetException(); + throw; } finally @@ -50,7 +55,7 @@ public async Task Connect(VpnHoodConnect clientConnect, CancellationToken cancel } } - public async Task Diagnose(VpnHoodConnect clientConnect, CancellationToken cancellationToken) + public async Task Diagnose(VpnHoodClient vpnHoodClient, CancellationToken cancellationToken) { try { @@ -61,7 +66,7 @@ public async Task Diagnose(VpnHoodConnect clientConnect, CancellationToken cance // ping server VhLogger.Instance.LogTrace("Checking the VpnServer ping..."); - var hostEndPoint = await ServerTokenHelper.ResolveHostEndPoint(clientConnect.Client.Token.ServerToken); + var hostEndPoint = await ServerTokenHelper.ResolveHostEndPoint(vpnHoodClient.Token.ServerToken); var pingRes = await DiagnoseUtil.CheckPing([hostEndPoint.Address], NsTimeout, true); if (pingRes == null) VhLogger.Instance.LogTrace("Pinging server is OK."); @@ -70,7 +75,7 @@ public async Task Diagnose(VpnHoodConnect clientConnect, CancellationToken cance // VpnConnect IsWorking = false; - await clientConnect.Connect(cancellationToken); + await vpnHoodClient.Connect(cancellationToken); VhLogger.Instance.LogTrace("Checking the Vpn Connection..."); IsWorking = true; diff --git a/VpnHood.Client/Exceptions/ConnectorEstablishException.cs b/VpnHood.Client/Exceptions/ConnectorEstablishException.cs deleted file mode 100644 index a7ffd40f7..000000000 --- a/VpnHood.Client/Exceptions/ConnectorEstablishException.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace VpnHood.Client.Exceptions; - -internal class ConnectorEstablishException(string message, Exception innerException) - : Exception(message, innerException); \ No newline at end of file diff --git a/VpnHood.Client/ServerFinder.cs b/VpnHood.Client/ServerFinder.cs new file mode 100644 index 000000000..5a44d50c1 --- /dev/null +++ b/VpnHood.Client/ServerFinder.cs @@ -0,0 +1,104 @@ +using System.Collections.Concurrent; +using System.Net; +using Microsoft.Extensions.Logging; +using VpnHood.Client.ConnectorServices; +using VpnHood.Common; +using VpnHood.Common.Exceptions; +using VpnHood.Common.Logging; +using VpnHood.Common.Messaging; +using VpnHood.Common.Utils; +using VpnHood.Tunneling.Factory; +using VpnHood.Tunneling.Messaging; + +namespace VpnHood.Client; + +public class ServerFinder(int maxDegreeOfParallelism = 10) +{ + public IReadOnlyDictionary? HostEndPointStatus { get; private set; } + + // There are much work to be done here + public async Task FindBestServerAsync(ServerToken serverToken, ISocketFactory socketFactory, CancellationToken cancellationToken) + { + // create a connector for each endpoint + var hostEndPoints = serverToken.HostEndPoints ?? []; + var connectors = hostEndPoints.Select(tcpEndPoint => + { + var endPointInfo = new ConnectorEndPointInfo + { + CertificateHash = serverToken.CertificateHash, + HostName = serverToken.HostName, + TcpEndPoint = tcpEndPoint + }; + var connector = new ConnectorService(endPointInfo, socketFactory, TimeSpan.FromSeconds(10), false); + connector.Init(0, TimeSpan.FromSeconds(10), serverToken.Secret, TimeSpan.FromSeconds(10)); + return connector; + }); + + // find endpoint status + HostEndPointStatus = await VerifyServersStatus(connectors, cancellationToken); + return HostEndPointStatus.FirstOrDefault(x=>x.Value).Key; //todo check if it is null + } + + private async Task> VerifyServersStatus(IEnumerable connectors, + CancellationToken cancellationToken) + { + var hostEndPointStatus = new ConcurrentDictionary(); + + try + { + // check all servers + using var cancellationTokenSource = new CancellationTokenSource(); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, cancellationToken); + await VhUtil.ParallelForEachAsync(connectors, async connector => + { + var serverStatus = await VerifyServerStatus(connector, linkedCancellationTokenSource.Token); + hostEndPointStatus[connector.EndPointInfo.TcpEndPoint] = serverStatus; + if (serverStatus) + linkedCancellationTokenSource.Cancel(); // no need to continue, we find a server + + }, maxDegreeOfParallelism, linkedCancellationTokenSource.Token); + + } + catch (OperationCanceledException) + { + // it means a server has been found + } + + return hostEndPointStatus; + } + + + private static async Task VerifyServerStatus(ConnectorService connector, CancellationToken cancellationToken) + { + try + { + var requestResult = await connector.SendRequest( + new ServerStatusRequest + { + RequestId = Guid.NewGuid().ToString(), + Message = "Hi, How are you?" + }, cancellationToken); + + // this should be already handled by the connector and never happen + if (requestResult.Response.ErrorCode != SessionErrorCode.Ok) + throw new SessionException(requestResult.Response.ErrorCode); + + return true; + } + catch (UnauthorizedAccessException) + { + throw; + } + catch (SessionException) + { + throw; + } + catch (Exception ex) + { + VhLogger.Instance.LogError(ex, "Could not get server status. EndPoint: {EndPoint}", + VhLogger.Format(connector.EndPointInfo.TcpEndPoint)); + + return false; + } + } +} \ No newline at end of file diff --git a/VpnHood.Client/UdpChannelMode.cs b/VpnHood.Client/UdpChannelMode.cs deleted file mode 100644 index b3a80e86a..000000000 --- a/VpnHood.Client/UdpChannelMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace VpnHood.Client; - -public enum UdpChannelMode -{ - Auto, - On, - Off -} \ No newline at end of file diff --git a/VpnHood.Client/VpnHood.Client.csproj b/VpnHood.Client/VpnHood.Client.csproj index 6c8844e48..7e9ac153a 100644 --- a/VpnHood.Client/VpnHood.Client.csproj +++ b/VpnHood.Client/VpnHood.Client.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client/VpnHoodClient.cs b/VpnHood.Client/VpnHoodClient.cs index a7229763a..381c4f868 100644 --- a/VpnHood.Client/VpnHoodClient.cs +++ b/VpnHood.Client/VpnHoodClient.cs @@ -38,7 +38,6 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable private readonly IAdProvider? _adProvider; private readonly TimeSpan _minTcpDatagramLifespan; private readonly TimeSpan _maxTcpDatagramLifespan; - private readonly ConnectorService _connectorService; private readonly bool _allowAnonymousTracker; private readonly string? _appGa4MeasurementId; private IPAddress[] _dnsServersIpV4 = []; @@ -52,6 +51,11 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable private bool _useUdpChannel; private ClientState _state = ClientState.None; private bool _isWaitingForAd; + private ConnectorService? _connectorService; + private readonly TimeSpan _tcpConnectTimeout; + private DateTime? _autoWaitTime; + private readonly string? _serverLocation; + private ConnectorService ConnectorService => VhUtil.GetRequiredInstance(_connectorService); private int ProtocolVersion { get; } internal Nat Nat { get; } @@ -63,6 +67,8 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable public IPAddress? PublicAddress { get; private set; } public bool IsIpV6Supported { get; private set; } public TimeSpan SessionTimeout { get; set; } + public TimeSpan AutoWaitTimeout { get; set; } + public TimeSpan ReconnectTimeout { get; set; } public Token Token { get; } public Guid ClientId { get; } public ulong SessionId { get; private set; } @@ -72,14 +78,14 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable public IpRange[] IncludeIpRanges { get; private set; } = IpNetwork.All.ToIpRanges().ToArray(); public IpRange[] PacketCaptureIncludeIpRanges { get; private set; } public string UserAgent { get; } - public IPEndPoint? HostTcpEndPoint => _connectorService.EndPointInfo?.TcpEndPoint; + public IPEndPoint? HostTcpEndPoint => _connectorService?.EndPointInfo.TcpEndPoint; public IPEndPoint? HostUdpEndPoint { get; private set; } public bool DropUdpPackets { get; set; } public ClientStat Stat { get; } public byte[] SessionKey => _sessionKey ?? throw new InvalidOperationException($"{nameof(SessionKey)} has not been initialized."); public byte[]? ServerSecret { get; private set; } public string? ResponseAccessKey { get; private set; } - public string? RegionId { get; } + public VpnHoodClient(IPacketCapture packetCapture, Guid clientId, Token token, ClientOptions options) { @@ -103,10 +109,12 @@ public VpnHoodClient(IPacketCapture packetCapture, Guid clientId, Token token, C _proxyManager = new ClientProxyManager(packetCapture, SocketFactory, new ProxyManagerOptions()); _ipRangeProvider = options.IpRangeProvider; _appGa4MeasurementId = options.AppGa4MeasurementId; - _connectorService = new ConnectorService(SocketFactory, options.ConnectTimeout); + _tcpConnectTimeout = options.ConnectTimeout; _useUdpChannel = options.UseUdpChannel; _adProvider = options.AdProvider; + ReconnectTimeout = options.ReconnectTimeout; + AutoWaitTimeout = options.AutoWaitTimeout; Token = token; Version = options.Version; UserAgent = options.UserAgent; @@ -116,7 +124,7 @@ public VpnHoodClient(IPacketCapture packetCapture, Guid clientId, Token token, C IncludeLocalNetwork = options.IncludeLocalNetwork; PacketCaptureIncludeIpRanges = options.PacketCaptureIncludeIpRanges; DropUdpPackets = options.DropUdpPackets; - RegionId = options.RegionId; + _serverLocation = options.ServerLocation; // NAT Nat = new Nat(true); @@ -185,7 +193,7 @@ internal async Task AddPassthruTcpStream(IClientStream orgTcpClientStream, IPEnd CancellationToken cancellationToken) { // set timeout - using var cancellationTokenSource = new CancellationTokenSource(_connectorService.RequestTimeout); + using var cancellationTokenSource = new CancellationTokenSource(ConnectorService.RequestTimeout); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, cancellationToken); cancellationToken = linkedCancellationTokenSource.Token; @@ -229,13 +237,13 @@ public async Task Connect(CancellationToken cancellationToken = default) try { // Init hostEndPoint - _connectorService.EndPointInfo = new ConnectorEndPointInfo + var endPointInfo = new ConnectorEndPointInfo { HostName = Token.ServerToken.HostName, TcpEndPoint = await ServerTokenHelper.ResolveHostEndPoint(Token.ServerToken), CertificateHash = Token.ServerToken.CertificateHash }; - SetHostEndPoint(_connectorService.EndPointInfo.TcpEndPoint); + _connectorService = new ConnectorService(endPointInfo, SocketFactory, _tcpConnectTimeout); // Establish first connection and create a session using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, cancellationToken); @@ -245,11 +253,11 @@ public async Task Connect(CancellationToken cancellationToken = default) _clientHost.Start(); // Preparing device; - if (!_packetCapture.Started) //make sure it is not a shared packet capture - { - ConfigPacketFilter(_connectorService.EndPointInfo.TcpEndPoint.Address); - _packetCapture.StartCapture(); - } + if (_packetCapture.Started) //make sure it is not a shared packet capture + throw new InvalidOperationException("PacketCapture should not be started before connect."); + + ConfigPacketFilter(ConnectorService.EndPointInfo.TcpEndPoint.Address); + _packetCapture.StartCapture(); // disable IncludeIpRanges if it contains all networks if (IncludeIpRanges.ToIpNetworks().IsAll()) @@ -314,6 +322,7 @@ private void Tunnel_OnPacketReceived(object sender, ChannelPacketReceivedEventAr // WARNING: Performance Critical! private void PacketCapture_OnPacketReceivedFromInbound(object sender, PacketReceivedEventArgs e) { + // stop traffic if the client has been disposed if (_disposed || _initConnectedTime is null) return; @@ -404,7 +413,20 @@ private void PacketCapture_OnPacketReceivedFromInbound(object sender, PacketRece else droppedPackets.Add(ipPacket); + } + } + // Stop tunnel traffics if the client is paused and unpause after AutoPauseTimeout + if (_autoWaitTime != null) + { + if (FastDateTime.Now - _autoWaitTime.Value < AutoWaitTimeout) + { + tunnelPackets.Clear(); + } + else + { + State = ClientState.Connecting; + _autoWaitTime = null; } } @@ -577,7 +599,7 @@ private async Task AddUdpChannel() } var udpClient = SocketFactory.CreateUdpClient(HostTcpEndPoint.AddressFamily); - var udpChannel = new UdpChannel(SessionId, _sessionKey, false, _connectorService.ServerProtocolVersion); + var udpChannel = new UdpChannel(SessionId, _sessionKey, false, ConnectorService.ServerProtocolVersion); try { var udpChannelTransmitter = new ClientUdpChannelTransmitter(udpChannel, udpClient, ServerSecret); @@ -612,7 +634,7 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all EncryptedClientId = VhUtil.EncryptClientId(clientInfo.ClientId, Token.Secret), ClientInfo = clientInfo, TokenId = Token.TokenId, - RegionId = RegionId, + ServerLocation = _serverLocation, AllowRedirect = allowRedirect }; @@ -622,11 +644,11 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all throw new SessionException(SessionErrorCode.UnsupportedServer, "This server is outdated and does not support this client!"); // initialize the connector - _connectorService.Init( + ConnectorService.Init( sessionResponse.ServerProtocolVersion, sessionResponse.RequestTimeout, - sessionResponse.TcpReuseTimeout, - sessionResponse.ServerSecret); + sessionResponse.ServerSecret, + sessionResponse.TcpReuseTimeout); // log response VhLogger.Instance.LogInformation(GeneralEventId.Session, @@ -679,8 +701,9 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all PublicAddress = sessionResponse.ClientPublicAddress; ServerVersion = Version.Parse(sessionResponse.ServerVersion); IsIpV6Supported = sessionResponse.IsIpV6Supported; - if (sessionResponse.UdpPort > 0 && HostTcpEndPoint != null) - HostUdpEndPoint = new IPEndPoint(HostTcpEndPoint.Address, sessionResponse.UdpPort.Value); + Stat.ServerLocationInfo = sessionResponse.ServerLocation != null ? ServerLocationInfo.Parse(sessionResponse.ServerLocation) : null; + if (sessionResponse.UdpPort > 0) + HostUdpEndPoint = new IPEndPoint(ConnectorService.EndPointInfo.TcpEndPoint.Address, sessionResponse.UdpPort.Value); // PacketCaptureIpRanges if (!VhUtil.IsNullOrEmpty(sessionResponse.PacketCaptureIncludeIpRanges)) @@ -726,8 +749,8 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all VhLogger.Instance.LogWarning("You suppressed a session of another client!"); // show ad if required - if (sessionResponse.IsAdRequired || sessionResponse.AdShow is not AdShow.None) - await ShowAd(sessionResponse.AdShow is AdShow.Flexible, cancellationToken); + if (sessionResponse.IsAdRequired || sessionResponse.AdRequirement is not AdRequirement.None) + await ShowAd(sessionResponse.AdRequirement is AdRequirement.Flexible, cancellationToken); // manage datagram channels await ManageDatagramChannels(cancellationToken); @@ -735,25 +758,12 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all } catch (RedirectHostException ex) when (allowRedirect) { - SetHostEndPoint(ex.RedirectHostEndPoint); + // todo; init new connector + ConnectorService.EndPointInfo.TcpEndPoint = ex.RedirectHostEndPoint; await ConnectInternal(cancellationToken, false); } } - private void SetHostEndPoint(IPEndPoint ipEndPoint) - { - if (_connectorService.EndPointInfo == null) - throw new InvalidOperationException($"{nameof(ConnectorServiceBase.EndPointInfo)} is not initialized."); - - // update _connectorService - _connectorService.EndPointInfo.TcpEndPoint = ipEndPoint; - - // Restart the packet capture if it captures Host IpAddress - if (_packetCapture is { Started: true, CanProtectSocket: false, IncludeNetworks: not null } && - IpRange.IsInSortedRanges(_packetCapture.IncludeNetworks.ToIpRanges().ToArray(), ipEndPoint.Address)) - _packetCapture.StopCapture(); - } - private async Task AddTcpDatagramChannel(CancellationToken cancellationToken) { // Create and send the Request Message @@ -794,7 +804,7 @@ internal async Task> SendRequest(ClientRequest requ try { // create a connection and send the request - var requestResult = await _connectorService.SendRequest(request, cancellationToken); + var requestResult = await ConnectorService.SendRequest(request, cancellationToken); // set SessionStatus if (requestResult.Response.AccessUsage != null) @@ -808,7 +818,7 @@ internal async Task> SendRequest(ClientRequest requ State = ClientState.Connected; return requestResult; } - catch (SessionException ex) when (ex.SessionResponse.ErrorCode is SessionErrorCode.GeneralError or SessionErrorCode.RedirectHost) + catch (SessionException ex) { // set SessionStatus if (ex.SessionResponse.AccessUsage != null) @@ -816,29 +826,67 @@ internal async Task> SendRequest(ClientRequest requ // GeneralError and RedirectHost mean that the request accepted by server but there is an error for that request _lastConnectionErrorTime = null; + + // close session if server has ended the session + if (ex.SessionResponse.ErrorCode != SessionErrorCode.GeneralError && + ex.SessionResponse.ErrorCode != SessionErrorCode.RedirectHost) + _ = DisposeAsync(ex); + + throw; + } + catch (UnauthorizedAccessException ex) + { + _ = DisposeAsync(ex); throw; } catch (Exception ex) { - // set connecting state if it could not establish any connection - if (!_disposed && State == ClientState.Connected) - State = ClientState.Connecting; + if (_disposed) + throw; + + var now = FastDateTime.Now; + _lastConnectionErrorTime ??= now; - // dispose by session timeout or known exception - _lastConnectionErrorTime ??= FastDateTime.Now; - if (ex is SessionException or UnauthorizedAccessException || FastDateTime.Now - _lastConnectionErrorTime.Value > SessionTimeout) + // dispose by session timeout and must before pause because SessionTimeout is bigger than ReconnectTimeout + if (now - _lastConnectionErrorTime.Value > SessionTimeout) _ = DisposeAsync(ex); + // pause after retry limit + else if (now - _lastConnectionErrorTime.Value > ReconnectTimeout) + { + _autoWaitTime = now; + State = ClientState.Waiting; + } + + // set connecting state if it could not establish any connection + else if (State == ClientState.Connected) + State = ClientState.Connecting; + throw; } } + public async Task UpdateSessionStatus(CancellationToken cancellationToken = default) + { + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, cancellationToken); + + // don't use SendRequest because it can be disposed + await using var requestResult = await SendRequest( + new SessionStatusRequest() + { + RequestId = Guid.NewGuid() + ":client", + SessionId = SessionId, + SessionKey = SessionKey + }, + linkedCancellationTokenSource.Token); + } + private async Task SendByeRequest(CancellationToken cancellationToken) { try { // don't use SendRequest because it can be disposed - await using var requestResult = await _connectorService.SendRequest( + await using var requestResult = await ConnectorService.SendRequest( new ByeRequest { RequestId = Guid.NewGuid() + ":client", @@ -945,27 +993,22 @@ public void Dispose() _ = DisposeAsync(); } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; public ValueTask DisposeAsync() { return DisposeAsync(false); } - public ValueTask DisposeAsync(bool waitForBye) + private readonly AsyncLock _disposeLock = new(); + public async ValueTask DisposeAsync(bool waitForBye) { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(waitForBye); - return _disposeTask.Value; - } + using var lockResult = await _disposeLock.LockAsync(); + if (_disposed) return; + _disposed = true; - private async ValueTask DisposeAsyncCore(bool waitForBye) - { // shutdown VhLogger.Instance.LogTrace("Shutting down..."); - _disposed = true; _cancellationTokenSource.Cancel(); var wasConnected = State is ClientState.Connecting or ClientState.Connected; State = ClientState.Disconnecting; @@ -1020,11 +1063,10 @@ private async Task Finalize(bool wasConnected) // dispose ConnectorService VhLogger.Instance.LogTrace("Disposing ConnectorService..."); - await _connectorService.DisposeAsync(); + await ConnectorService.DisposeAsync(); State = ClientState.Disposed; VhLogger.Instance.LogInformation("Bye Bye!"); - } private class SendingPackets @@ -1048,7 +1090,7 @@ public void Clear() public class ClientStat { private readonly VpnHoodClient _client; - public ConnectorStat ConnectorStat => _client._connectorService.Stat; + public ConnectorStat ConnectorStat => _client.ConnectorService.Stat; public Traffic Speed => _client.Tunnel.Speed; public Traffic SessionTraffic => _client.Tunnel.Traffic; public Traffic AccountTraffic => _client._helloTraffic + SessionTraffic; @@ -1057,6 +1099,7 @@ public class ClientStat public bool IsUdpChannelSupported => _client.HostUdpEndPoint != null; public bool IsWaitingForAd => _client._isWaitingForAd; public bool IsDnsServersAccepted { get; internal set; } + public ServerLocationInfo? ServerLocationInfo{ get; internal set; } internal ClientStat(VpnHoodClient vpnHoodClient) { diff --git a/VpnHood.Client/VpnHoodConnect.cs b/VpnHood.Client/VpnHoodConnect.cs deleted file mode 100644 index 410a7653b..000000000 --- a/VpnHood.Client/VpnHoodConnect.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Microsoft.Extensions.Logging; -using VpnHood.Client.Device; -using VpnHood.Common; -using VpnHood.Common.Logging; -using VpnHood.Common.Messaging; -using VpnHood.Common.Utils; -using VpnHood.Tunneling; - -namespace VpnHood.Client; - -public class VpnHoodConnect : IAsyncDisposable -{ - private readonly bool _autoDisposePacketCapture; - private readonly Guid _clientId; - private readonly ClientOptions _clientOptions; - private readonly IPacketCapture _packetCapture; - private readonly Token _token; - private DateTime _reconnectTime = DateTime.MinValue; - - public bool IsWaiting { get; private set; } - public bool IsDisposed { get; private set; } - public event EventHandler? StateChanged; - - public VpnHoodConnect(IPacketCapture packetCapture, Guid clientId, Token token, - ClientOptions? clientOptions = null, ConnectOptions? connectOptions = null) - { - connectOptions ??= new ConnectOptions(); - _clientOptions = clientOptions ?? new ClientOptions(); - _autoDisposePacketCapture = _clientOptions.AutoDisposePacketCapture; - _packetCapture = packetCapture; - _clientId = clientId; - _token = token; - MaxReconnectCount = connectOptions.MaxReconnectCount; - ReconnectDelay = connectOptions.ReconnectDelay; - - //this class Connect change this option temporary and restore it after last attempt - _clientOptions.AutoDisposePacketCapture = false; - _clientOptions.UseUdpChannel = connectOptions.UdpChannelMode is UdpChannelMode.On or UdpChannelMode.Auto; - - // let always have a Client to access its member after creating VpnHoodConnect - Client = new VpnHoodClient(_packetCapture, _clientId, _token, _clientOptions); - } - - public int AttemptCount { get; private set; } - public TimeSpan ReconnectDelay { get; set; } - public int MaxReconnectCount { get; set; } - public VpnHoodClient Client { get; private set; } - - public Task Connect(CancellationToken cancellationToken = default) - { - if (IsDisposed) - throw new ObjectDisposedException($"{VhLogger.FormatType(this)} is disposed!"); - - if (Client.State != ClientState.None && Client.State != ClientState.Disposed) - throw new InvalidOperationException("Connection is already in progress."); - - if (Client.State == ClientState.Disposed) - Client = new VpnHoodClient(_packetCapture, _clientId, _token, _clientOptions); - - Client.StateChanged += Client_StateChanged; - return Client.Connect(cancellationToken); - } - - private void Client_StateChanged(object sender, EventArgs e) - { - StateChanged?.Invoke(this, EventArgs.Empty); - if (Client.State == ClientState.Disposed) _ = Reconnect(); - } - - private async Task Reconnect() - { - if ((FastDateTime.Now - _reconnectTime).TotalMinutes > 5) - AttemptCount = 0; - - // check reconnecting - var reconnect = AttemptCount < MaxReconnectCount && - Client.SessionStatus.ErrorCode is SessionErrorCode.GeneralError; - - if (reconnect) - { - _reconnectTime = FastDateTime.Now; - AttemptCount++; - - // delay - IsWaiting = true; - StateChanged?.Invoke(this, EventArgs.Empty); - await Task.Delay(ReconnectDelay); - IsWaiting = false; - StateChanged?.Invoke(this, EventArgs.Empty); - - // connect again - if (IsDisposed) return; - await Connect(); - } - else - { - await DisposeAsync(); - } - } - - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - public ValueTask DisposeAsync() - { - return DisposeAsync(true); - } - - public ValueTask DisposeAsync(bool waitForBye) - { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(waitForBye); - return _disposeTask.Value; - } - - private async ValueTask DisposeAsyncCore(bool waitForBye) - { - if (IsDisposed) return; - IsDisposed = true; - - // close client - try - { - await Client.DisposeAsync(waitForBye); - Client.StateChanged -= Client_StateChanged; //must be after Client.Dispose to capture dispose event - } - catch (Exception ex) - { - VhLogger.Instance.LogError(GeneralEventId.Session, ex, "Could not dispose client properly."); - } - - // release _packetCapture - if (_autoDisposePacketCapture) - _packetCapture.Dispose(); - - // notify state changed - StateChanged?.Invoke(this, EventArgs.Empty); - } -} \ No newline at end of file diff --git a/VpnHood.Common/HostRegion.cs b/VpnHood.Common/HostRegion.cs deleted file mode 100644 index 1bcf0b9b6..000000000 --- a/VpnHood.Common/HostRegion.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace VpnHood.Common; - -public class HostRegion -{ - public required string RegionId { get; set; } - public string? RegionName { get; set; } - public required string CountryCode { get; set; } -} \ No newline at end of file diff --git a/VpnHood.Common/IpLocations/IIpLocationProvider.cs b/VpnHood.Common/IpLocations/IIpLocationProvider.cs new file mode 100644 index 000000000..61388c196 --- /dev/null +++ b/VpnHood.Common/IpLocations/IIpLocationProvider.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace VpnHood.Common.IpLocations; + +public interface IIpLocationProvider +{ + Task GetLocation(HttpClient httpClient, IPAddress ipAddress); + Task GetLocation(HttpClient httpClient); +} \ No newline at end of file diff --git a/VpnHood.Common/IpLocations/IpLocation.cs b/VpnHood.Common/IpLocations/IpLocation.cs new file mode 100644 index 000000000..64919d3dc --- /dev/null +++ b/VpnHood.Common/IpLocations/IpLocation.cs @@ -0,0 +1,18 @@ +using System.Net; +using System.Text.Json.Serialization; +using VpnHood.Common.Converters; + +namespace VpnHood.Common.IpLocations; + +public class IpLocation +{ + [JsonConverter(typeof(IPAddressConverter))] + public required IPAddress IpAddress { get; init; } + public required string CountryName { get; init; } + public required string CountryCode { get; init; } + public required string? RegionName { get; init; } + public required string? RegionCode { get; init; } + public required string? CityName { get; init; } + public required string? CityCode { get; init; } + public required string? ContinentCode { get; init; } +} \ No newline at end of file diff --git a/VpnHood.Common/IpLocations/IpLocationProviderFactory.cs b/VpnHood.Common/IpLocations/IpLocationProviderFactory.cs new file mode 100644 index 000000000..5beeca948 --- /dev/null +++ b/VpnHood.Common/IpLocations/IpLocationProviderFactory.cs @@ -0,0 +1,13 @@ +using VpnHood.Common.IpLocations.Providers; + +namespace VpnHood.Common.IpLocations; + +public class IpLocationProviderFactory +{ + public IIpLocationProvider CreateDefault(string agent) => new IpApiCoLocationProvider(agent); + + public static string GetPath(string countryCode, string? regionName, string? cityName) + { + return $"{countryCode}/{regionName ?? cityName ?? ""}".Trim('/'); + } +} \ No newline at end of file diff --git a/VpnHood.Common/IpLocations/Providers/IpApiCoLocationProvider.cs b/VpnHood.Common/IpLocations/Providers/IpApiCoLocationProvider.cs new file mode 100644 index 000000000..550ad983c --- /dev/null +++ b/VpnHood.Common/IpLocations/Providers/IpApiCoLocationProvider.cs @@ -0,0 +1,71 @@ +using System.Net; +using System.Text.Json.Serialization; +using VpnHood.Common.Converters; +using VpnHood.Common.Utils; + +namespace VpnHood.Common.IpLocations.Providers; + +public class IpApiCoLocationProvider(string userAgent) : IIpLocationProvider +{ + internal class ApiLocation + { + [JsonPropertyName("ip")] + [JsonConverter(typeof(IPAddressConverter))] + public required IPAddress Ip { get; set; } + + [JsonPropertyName("country_name")] + public required string CountryName { get; set; } + + [JsonPropertyName("country_code")] + public required string CountryCode { get; set; } + + [JsonPropertyName("region")] + public string? RegionName { get; set; } + + [JsonPropertyName("region_code")] + public string? RegionCode { get; set; } + + [JsonPropertyName("city")] + public string? CityName { get; set; } + + [JsonPropertyName("continent_code")] + public string? ContinentCode { get; set; } + } + + public Task GetLocation(HttpClient httpClient, IPAddress ipAddress) + { + var uri = new Uri($"https://ipapi.co/{ipAddress}/json/"); + return GetLocation(httpClient, uri, userAgent); + } + + public Task GetLocation(HttpClient httpClient) + { + var uri = new Uri("https://ipapi.co/json/"); + return GetLocation(httpClient, uri, userAgent); + } + + private static async Task GetLocation(HttpClient httpClient, Uri url, string userAgent) + { + // get json from the service provider + var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + requestMessage.Headers.Add("User-Agent", userAgent); + var responseMessage = await httpClient.SendAsync(requestMessage); + responseMessage.EnsureSuccessStatusCode(); + var json = await responseMessage.Content.ReadAsStringAsync(); + + var apiLocation = VhUtil.JsonDeserialize(json); + var ipLocation = new IpLocation + { + IpAddress = apiLocation.Ip, + CountryName = apiLocation.CountryName, + CountryCode = apiLocation.CountryCode, + RegionName = apiLocation.RegionName == "NA" ? null : apiLocation.RegionName, + RegionCode = apiLocation.RegionCode == "NA" ? null : apiLocation.RegionCode, + CityName = apiLocation.CityName == "NA" ? null : apiLocation.CityName, + CityCode = apiLocation.CityName == "NA" ? null : apiLocation.CityName, + ContinentCode = apiLocation.ContinentCode == "NA" ? null : apiLocation.ContinentCode + }; + + return ipLocation; + } +} \ No newline at end of file diff --git a/VpnHood.Common/Logging/VhLogger.cs b/VpnHood.Common/Logging/VhLogger.cs index 21eeccbd8..077145c23 100644 --- a/VpnHood.Common/Logging/VhLogger.cs +++ b/VpnHood.Common/Logging/VhLogger.cs @@ -95,10 +95,10 @@ public static string FormatSessionId(object? id) public static string FormatHostName(string? dnsName) { if (dnsName == null) return ""; + if (IPEndPointConverter.TryParse(dnsName, out var ipEndPoint)) + return Format(ipEndPoint); - return IPEndPointConverter.TryParse(dnsName, out var ipEndPoint) - ? Format(ipEndPoint) - : VhUtil.RedactHostName(dnsName); + return IsAnonymousMode ? VhUtil.RedactHostName(dnsName) : dnsName; } public static string FormatIpPacket(string ipPacketText) diff --git a/VpnHood.Common/Messaging/AdRequirement.cs b/VpnHood.Common/Messaging/AdRequirement.cs new file mode 100644 index 000000000..2f3e65e35 --- /dev/null +++ b/VpnHood.Common/Messaging/AdRequirement.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace VpnHood.Common.Messaging; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AdRequirement +{ + None, + Flexible, + Required +} diff --git a/VpnHood.Common/Messaging/ClientInfo.cs b/VpnHood.Common/Messaging/ClientInfo.cs index 771e16b94..573215c50 100644 --- a/VpnHood.Common/Messaging/ClientInfo.cs +++ b/VpnHood.Common/Messaging/ClientInfo.cs @@ -2,12 +2,10 @@ public class ClientInfo { - public Guid ClientId { get; set; } - // ReSharper disable once UnusedMember.Global - public string? UserToken { get; set; } - public string ClientVersion { get; set; } = null!; - public int ProtocolVersion { get; set; } - public string UserAgent { get; set; } = null!; + public required Guid ClientId { get; init; } + public required string ClientVersion { get; init; } + public required int ProtocolVersion { get; init; } + public required string UserAgent { get; init; } public override string ToString() { diff --git a/VpnHood.Common/Messaging/SessionResponse.cs b/VpnHood.Common/Messaging/SessionResponse.cs index 4bd5b8e59..711de2ba5 100644 --- a/VpnHood.Common/Messaging/SessionResponse.cs +++ b/VpnHood.Common/Messaging/SessionResponse.cs @@ -20,4 +20,8 @@ public class SessionResponse [JsonConverter(typeof(IPEndPointConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public IPEndPoint? RedirectHostEndPoint { get; set; } + + [JsonConverter(typeof(ArrayConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IPEndPoint[]? RedirectHostEndPoints { get; set; } } \ No newline at end of file diff --git a/VpnHood.Common/ServerLocationInfo.cs b/VpnHood.Common/ServerLocationInfo.cs new file mode 100644 index 000000000..f0e9a00eb --- /dev/null +++ b/VpnHood.Common/ServerLocationInfo.cs @@ -0,0 +1,79 @@ +using System.Globalization; +using System.Text.Json.Serialization; + +namespace VpnHood.Common; + +public class ServerLocationInfo : IComparable +{ + [JsonIgnore] // required to prevent NSwag generate incorrect code + public static ServerLocationInfo Auto { get; } = new() { CountryCode = "*", RegionName = "*" }; + public required string CountryCode { get; init; } + public required string RegionName { get; init; } + public string ServerLocation => $"{CountryCode}/{RegionName}"; + public string CountryName => GetCountryName(CountryCode); + + public int CompareTo(ServerLocationInfo other) + { + var countryComparison = string.Compare(CountryCode, other.CountryCode, StringComparison.OrdinalIgnoreCase); + return countryComparison != 0 ? countryComparison : string.Compare(RegionName, other.RegionName, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object? obj) + { + return ServerLocation == (obj as ServerLocationInfo)?.ServerLocation; + } + + + public override string ToString() + { + return ServerLocation; + } + + public override int GetHashCode() + { + return ServerLocation.GetHashCode(); + } + + public static ServerLocationInfo Parse(string value) + { + var parts = value.Split('/'); + var ret = new ServerLocationInfo + { + CountryCode = ParseLocationPart(parts, 0), + RegionName = ParseLocationPart(parts, 1) + }; + return ret; + } + + public bool IsMatch(ServerLocationInfo serverLocationInfo) + { + return + MatchLocationPart(CountryCode, serverLocationInfo.CountryCode) && + MatchLocationPart(RegionName, serverLocationInfo.RegionName); + } + + private static string ParseLocationPart(IReadOnlyList parts, int index) + { + return parts.Count <= index || string.IsNullOrWhiteSpace(parts[index]) ? "*" : parts[index].Trim(); + } + + private static bool MatchLocationPart(string serverPart, string requestPart) + { + return requestPart == "*" || requestPart.Equals(serverPart, StringComparison.OrdinalIgnoreCase); + } + + private static string GetCountryName(string countryCode) + { + try + { + if (countryCode == "*") return "(auto)"; + var regionInfo = new RegionInfo(countryCode); + return regionInfo.EnglishName; + } + catch (Exception) + { + return countryCode; + } + } + +} \ No newline at end of file diff --git a/VpnHood.Common/ServerToken.cs b/VpnHood.Common/ServerToken.cs index 9333c1b20..36f5b4fe4 100644 --- a/VpnHood.Common/ServerToken.cs +++ b/VpnHood.Common/ServerToken.cs @@ -37,9 +37,9 @@ public class ServerToken [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public IPEndPoint[]? HostEndPoints { get; set; } - [JsonPropertyName("regions")] + [JsonPropertyName("loc")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public HostRegion[]? Regions { get; set; } + public string[]? ServerLocations { get; set; } public string Encrypt() { diff --git a/VpnHood.Common/Token.cs b/VpnHood.Common/Token.cs index 141ab4bcf..162ab50c1 100644 --- a/VpnHood.Common/Token.cs +++ b/VpnHood.Common/Token.cs @@ -1,7 +1,6 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using VpnHood.Common.Messaging; using VpnHood.Common.TokenLegacy; using VpnHood.Common.Utils; @@ -47,7 +46,7 @@ public static Token FromAccessKey(string base64) base64 = base64[prefix.Length..]; // load - var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + var json = Encoding.UTF8.GetString(VhUtil.ConvertFromBase64AndFixPadding(base64)); var tokenVersion = VhUtil.JsonDeserialize(json); return tokenVersion.Version switch diff --git a/VpnHood.Common/TokenLegacy/TokenVersion.cs b/VpnHood.Common/TokenVersion.cs similarity index 78% rename from VpnHood.Common/TokenLegacy/TokenVersion.cs rename to VpnHood.Common/TokenVersion.cs index 76b8d29a3..14ef81ce2 100644 --- a/VpnHood.Common/TokenLegacy/TokenVersion.cs +++ b/VpnHood.Common/TokenVersion.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace VpnHood.Common.TokenLegacy; +namespace VpnHood.Common; internal class TokenVersion { diff --git a/VpnHood.Common/Utils/VhTestUtil.cs b/VpnHood.Common/Utils/VhTestUtil.cs index 8c6aaf3d5..542c5466f 100644 --- a/VpnHood.Common/Utils/VhTestUtil.cs +++ b/VpnHood.Common/Utils/VhTestUtil.cs @@ -19,32 +19,36 @@ public AssertException(string message, Exception innerException) } } - private static async Task WaitForValue(object? expectedValue, Func valueFactory, int timeout = 5000) + private static async Task WaitForValue(TValue expectedValue, Func valueFactory, int timeout = 5000) { const int waitTime = 100; + var actualValue = valueFactory(); for (var elapsed = 0; elapsed < timeout; elapsed += waitTime) { - if (Equals(expectedValue, valueFactory())) - return; + if (Equals(expectedValue, actualValue)) + return actualValue; await Task.Delay(waitTime); + actualValue = valueFactory(); } - throw new TimeoutException(); + return actualValue; } - private static async Task WaitForValue(object? expectedValue, Func> valueFactory, int timeout = 5000) + private static async Task WaitForValue(TValue expectedValue, Func> valueFactory, int timeout = 5000) { const int waitTime = 100; + var actualValue = await valueFactory(); for (var elapsed = 0; elapsed < timeout; elapsed += waitTime) { - if (Equals(expectedValue, await valueFactory())) - return; + if (Equals(expectedValue, actualValue)) + return actualValue; await Task.Delay(waitTime); + actualValue = await valueFactory(); } - throw new TimeoutException(); + return actualValue; } @@ -55,25 +59,25 @@ private static void AssertEquals(object? expected, object? actual, string? messa throw new AssertException($"{message}. Expected: {expected}, Actual: {actual}"); } - public static async Task AssertEqualsWait(object? expectedValue, Func valueFactory, + public static async Task AssertEqualsWait(TValue expectedValue, Func valueFactory, string? message = null, int timeout = 5000) { - await WaitForValue(expectedValue, valueFactory, timeout); - AssertEquals(expectedValue, valueFactory(), message); + var actualValue = await WaitForValue(expectedValue, valueFactory, timeout); + AssertEquals(expectedValue, actualValue, message); } - public static async Task AssertEqualsWait(object? expectedValue, Func> valueFactory, + public static async Task AssertEqualsWait(TValue expectedValue, Func> valueFactory, string? message = null, int timeout = 5000) { - await WaitForValue(expectedValue, valueFactory, timeout); - AssertEquals(expectedValue, await valueFactory(), message); + var actualValue = await WaitForValue(expectedValue, valueFactory, timeout); + AssertEquals(expectedValue, actualValue, message); } - public static async Task AssertEqualsWait(object? expectedValue, Task task, + public static async Task AssertEqualsWait(TValue expectedValue, Task task, string? message = null, int timeout = 5000) { - await WaitForValue(expectedValue, () => task, timeout); - AssertEquals(expectedValue, await task, message); + var actualValue = await WaitForValue(expectedValue, () => task, timeout); + AssertEquals(expectedValue, actualValue, message); } public static Task AssertApiException(HttpStatusCode expectedStatusCode, Task task, string? message = null) diff --git a/VpnHood.Common/Utils/VhUtil.cs b/VpnHood.Common/Utils/VhUtil.cs index 303580000..214920a9b 100644 --- a/VpnHood.Common/Utils/VhUtil.cs +++ b/VpnHood.Common/Utils/VhUtil.cs @@ -56,6 +56,27 @@ public static IPEndPoint GetFreeUdpEndPoint(IPAddress ipAddress, int defaultPort } } + private static string FixBase64String(string base64) + { + base64 = base64.Trim(); + var padding = base64.Length % 4; + if (padding > 0) + base64 = base64.PadRight(base64.Length + (4 - padding), '='); + return base64; + } + + public static byte[] ConvertFromBase64AndFixPadding(string base64) + { + try + { + return Convert.FromBase64String(base64); + } + catch (FormatException) + { + return Convert.FromBase64String(FixBase64String(base64)); + } + } + public static void DirectoryCopy(string sourcePath, string destinationPath, bool recursive) { // Get the subdirectories for the specified directory. @@ -103,7 +124,7 @@ public static async Task RunTask(Task task, TimeSpan timeout = default, public static async Task RunTask(Task task, TimeSpan timeout = default, CancellationToken cancellationToken = default) { if (timeout == TimeSpan.Zero) - timeout = TimeSpan.FromMilliseconds(-1); + timeout = Timeout.InfiniteTimeSpan; var timeoutTask = Task.Delay(timeout, cancellationToken); await Task.WhenAny(task, timeoutTask); @@ -187,8 +208,8 @@ public static T JsonDeserialize(string json, JsonSerializerOptions? options = return default; var json = File.ReadAllText(filePath); - var appAccount = JsonDeserialize(json, options); - return appAccount; + var obj = JsonDeserialize(json, options); + return obj; } catch (Exception ex) { @@ -367,6 +388,11 @@ public static string RedactJsonValue(string json, string[] keys) return json; } + public static T GetRequiredInstance(T? obj) + { + return obj ?? throw new InvalidOperationException($"{typeof(T)} has not been initialized yet."); + } + public static DateTime RemoveMilliseconds(DateTime dateTime) { return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Kind); @@ -379,4 +405,38 @@ public static string GetAssemblyMetadata(Assembly assembly, string key, string d return string.IsNullOrEmpty(metadataAttribute?.Value) ? defaultValue : metadataAttribute.Value; } + + public static async Task ParallelForEachAsync(IEnumerable source, Func body, int maxDegreeOfParallelism, + CancellationToken cancellationToken) + { + var tasks = new List(); + foreach (var t in source) + { + cancellationToken.ThrowIfCancellationRequested(); + + tasks.Add(body(t)); + if (tasks.Count == maxDegreeOfParallelism) + { + await Task.WhenAny(tasks); + foreach (var completedTask in tasks.Where(x => x.IsCompleted).ToArray()) + tasks.Remove(completedTask); + } + } + await Task.WhenAll(tasks); + } + + public static bool TryDeleteFile(string filePath) + { + try + { + if (File.Exists(filePath)) + File.Delete(filePath); + + return true; + } + catch + { + return false; + } + } } \ No newline at end of file diff --git a/VpnHood.Common/VpnHood.Common.csproj b/VpnHood.Common/VpnHood.Common.csproj index 137c0fea7..5c600a659 100644 --- a/VpnHood.Common/VpnHood.Common.csproj +++ b/VpnHood.Common/VpnHood.Common.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Server.Access/Managers/File/FileAccessManager.cs b/VpnHood.Server.Access/Managers/File/FileAccessManager.cs index e682583ce..20c030d65 100644 --- a/VpnHood.Server.Access/Managers/File/FileAccessManager.cs +++ b/VpnHood.Server.Access/Managers/File/FileAccessManager.cs @@ -6,6 +6,7 @@ using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using VpnHood.Common; +using VpnHood.Common.IpLocations; using VpnHood.Common.Logging; using VpnHood.Common.Messaging; using VpnHood.Common.Utils; @@ -23,6 +24,7 @@ public class FileAccessManager : IAccessManager public string StoragePath { get; } public FileAccessManagerSessionController SessionController { get; } public string CertsFolderPath => Path.Combine(StoragePath, "certificates"); + public string SessionsFolderPath => Path.Combine(StoragePath, "sessions"); public X509Certificate2 DefaultCert { get; } public ServerStatus? ServerStatus { get; private set; } public ServerInfo? ServerInfo { get; private set; } @@ -34,7 +36,7 @@ public FileAccessManager(string storagePath, FileAccessManagerOptions options) StoragePath = storagePath ?? throw new ArgumentNullException(nameof(storagePath)); ServerConfig = options; - SessionController = new FileAccessManagerSessionController(); + SessionController = new FileAccessManagerSessionController(SessionsFolderPath); Directory.CreateDirectory(StoragePath); var defaultCertFile = Path.Combine(CertsFolderPath, "default.pfx"); @@ -64,13 +66,15 @@ public void ClearCache() } private ServerToken GetAndUpdateServerToken() => GetAndUpdateServerToken(ServerConfig, DefaultCert, Path.Combine(StoragePath, "server-token", "enc-server-token")); - private static ServerToken GetAndUpdateServerToken(FileAccessManagerOptions serverConfig, X509Certificate2 certificate, string encServerTokenFilePath) + private ServerToken GetAndUpdateServerToken(FileAccessManagerOptions serverConfig, X509Certificate2 certificate, string encServerTokenFilePath) { // PublicEndPoints var publicEndPoints = serverConfig.PublicEndPoints ?? serverConfig.TcpEndPointsValue; if (!publicEndPoints.Any() || publicEndPoints.Any(x => x.Address.Equals(IPAddress.Any) || x.Address.Equals(IPAddress.IPv6Any))) throw new Exception("PublicEndPoints has not been configured properly."); + + var serverLocation = LoadServerLocation().Result; var serverToken = new ServerToken { CertificateHash = serverConfig.IsValidHostName ? null : certificate.GetCertHash(), @@ -80,7 +84,8 @@ private static ServerToken GetAndUpdateServerToken(FileAccessManagerOptions serv IsValidHostName = serverConfig.IsValidHostName, Secret = serverConfig.ServerSecretValue, Url = serverConfig.ServerTokenUrl, - CreatedTime = VhUtil.RemoveMilliseconds(DateTime.UtcNow) + CreatedTime = VhUtil.RemoveMilliseconds(DateTime.UtcNow), + ServerLocations = serverLocation != null ? [serverLocation] : null }; // write encrypted server token @@ -101,13 +106,53 @@ private static ServerToken GetAndUpdateServerToken(FileAccessManagerOptions serv return serverToken; } - public byte[] LoadServerSecret() + private byte[] LoadServerSecret() { var serverSecretFile = Path.Combine(CertsFolderPath, "secret"); - if (!System.IO.File.Exists(serverSecretFile)) - System.IO.File.WriteAllText(serverSecretFile, Convert.ToBase64String(VhUtil.GenerateKey(128))); + var secretBase64 = TryToReadFile(serverSecretFile); + if (string.IsNullOrEmpty(secretBase64)) + { + secretBase64 = Convert.ToBase64String(VhUtil.GenerateKey(128)); + System.IO.File.WriteAllText(serverSecretFile, secretBase64); + } - return Convert.FromBase64String(System.IO.File.ReadAllText(serverSecretFile)); + return Convert.FromBase64String(secretBase64); + } + + private async Task LoadServerLocation() + { + try + { + var serverCountryFile = Path.Combine(StoragePath, "server_location"); + var serverLocation = TryToReadFile(serverCountryFile); + if (string.IsNullOrEmpty(serverLocation) && ServerConfig.UseExternalLocationService) + { + var ipLocationProvider = new IpLocationProviderFactory().CreateDefault("VpnHood-Server"); + var ipLocation = await ipLocationProvider.GetLocation(new HttpClient()); + serverLocation = IpLocationProviderFactory.GetPath(ipLocation.CountryCode, ipLocation.RegionName, ipLocation.CityName); + await System.IO.File.WriteAllTextAsync(serverCountryFile, serverLocation); + } + + VhLogger.Instance.LogInformation("ServerLocation: {ServerLocation}", serverLocation ?? "Unknown"); + return serverLocation; + } + catch (Exception ex) + { + VhLogger.Instance.LogWarning(ex, "Could not read server location."); + return null; + } + } + private static string? TryToReadFile(string filePath) + { + try + { + return System.IO.File.Exists(filePath) ? System.IO.File.ReadAllText(filePath) : null; + } + catch (Exception ex) + { + VhLogger.Instance.LogWarning(ex, "Could not read file: {FilePath}", filePath); + return null; + } } public virtual Task Server_UpdateStatus(ServerStatus serverStatus) @@ -144,6 +189,7 @@ public virtual async Task Session_Create(SessionRequestEx ses }; var ret = SessionController.CreateSession(sessionRequestEx, accessItem); + ret.ServerLocation = _serverToken.ServerLocations?.FirstOrDefault(); // update accesskey if (ServerConfig.ReplyAccessKey) @@ -199,6 +245,7 @@ protected virtual bool IsValidAd(string? adData) private async Task Session_AddUsage(ulong sessionId, Traffic traffic, string? adData, bool closeSession) { + // find token var tokenId = SessionController.TokenIdFromSessionId(sessionId); if (tokenId == null) @@ -295,7 +342,7 @@ public AccessItem AccessItem_Create( string? tokenName = null, int maxTrafficByteCount = 0, DateTime? expirationTime = null, - AdShow adShow = AdShow.None) + AdRequirement adRequirement = AdRequirement.None) { // generate key var aes = Aes.Create(); @@ -308,7 +355,7 @@ public AccessItem AccessItem_Create( MaxTraffic = maxTrafficByteCount, MaxClientCount = maxClientCount, ExpirationTime = expirationTime, - AdShow = adShow, + AdRequirement = adRequirement, Token = new Token { IssuedAt = DateTime.UtcNow, @@ -413,7 +460,7 @@ public class AccessItem public DateTime? ExpirationTime { get; set; } public int MaxClientCount { get; set; } public long MaxTraffic { get; set; } - public AdShow AdShow { get; set; } + public AdRequirement AdRequirement { get; set; } public required Token Token { get; set; } [JsonIgnore] diff --git a/VpnHood.Server.Access/Managers/File/FileAccessManagerOptions.cs b/VpnHood.Server.Access/Managers/File/FileAccessManagerOptions.cs index 07dbe0124..d5b3d9775 100644 --- a/VpnHood.Server.Access/Managers/File/FileAccessManagerOptions.cs +++ b/VpnHood.Server.Access/Managers/File/FileAccessManagerOptions.cs @@ -15,4 +15,5 @@ public class FileAccessManagerOptions : ServerConfig public bool IsValidHostName { get; set; } public string? ServerTokenUrl { get; set; } public bool ReplyAccessKey { get; set; } = true; // if false, access tokens will only be updated by url + public bool UseExternalLocationService { get; set; } = true; } \ No newline at end of file diff --git a/VpnHood.Server.Access/Managers/File/FileAccessManagerSessionController.cs b/VpnHood.Server.Access/Managers/File/FileAccessManagerSessionController.cs index af02fd9d3..ab0364c58 100644 --- a/VpnHood.Server.Access/Managers/File/FileAccessManagerSessionController.cs +++ b/VpnHood.Server.Access/Managers/File/FileAccessManagerSessionController.cs @@ -1,6 +1,11 @@ using System.Collections.Concurrent; using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using VpnHood.Common.Converters; using VpnHood.Common.Jobs; +using VpnHood.Common.Logging; using VpnHood.Common.Messaging; using VpnHood.Common.Utils; using VpnHood.Server.Access.Messaging; @@ -9,16 +14,44 @@ namespace VpnHood.Server.Access.Managers.File; public class FileAccessManagerSessionController : IDisposable, IJob { + private const string SessionFileExtension = "session"; private readonly TimeSpan _sessionPermanentlyTimeout = TimeSpan.FromHours(48); private readonly TimeSpan _sessionTemporaryTimeout = TimeSpan.FromHours(20); private readonly TimeSpan _adRequiredTimeout = TimeSpan.FromMinutes(4); - private uint _lastSessionId; + private ulong _lastSessionId; + private readonly string _sessionsFolderPath; + public JobSection JobSection { get; } = new(); - public ConcurrentDictionary Sessions { get; } = new(); + public ConcurrentDictionary Sessions { get; } - public FileAccessManagerSessionController() + public FileAccessManagerSessionController(string sessionsFolderPath) { JobRunner.Default.Add(this); + _sessionsFolderPath = sessionsFolderPath; + Directory.CreateDirectory(sessionsFolderPath); + + // load all previous sessions to dictionary + Sessions = LoadAllSessions(sessionsFolderPath); + } + + private static ConcurrentDictionary LoadAllSessions(string sessionsFolderPath) + { + // read all session from files + var sessions = new ConcurrentDictionary(); + foreach (var filePath in Directory.GetFiles(sessionsFolderPath, $"*.{SessionFileExtension}")) + { + var session = VhUtil.JsonDeserializeFile(filePath); + if (session == null) + { + VhLogger.Instance.LogError("Could not load session file. File: {File}", filePath); + VhUtil.TryDeleteFile(filePath); + continue; + } + + sessions.TryAdd(session.SessionId, session); + } + + return sessions; } public Task RunJob() @@ -27,7 +60,10 @@ public Task RunJob() return Task.CompletedTask; } - public JobSection JobSection { get; } = new(); + private string GetSessionFilePath(ulong sessionId) + { + return Path.Combine(_sessionsFolderPath, $"{sessionId}.{SessionFileExtension}"); + } private void CleanupSessions() { @@ -40,7 +76,10 @@ private void CleanupSessions() (x.Value.EndTime != null && x.Value.LastUsedTime < minCloseWaitTime)); foreach (var item in timeoutSessions) + { Sessions.TryRemove(item.Key, out _); + VhUtil.TryDeleteFile(GetSessionFilePath(item.Key)); + } } public string? TokenIdFromSessionId(ulong sessionId) @@ -65,9 +104,15 @@ public SessionResponseEx CreateSession(SessionRequestEx sessionRequestEx, ErrorMessage = "Could not validate the request." }; + + //increment session id using atomic operation + lock(Sessions) + _lastSessionId++; + // create a new session var session = new Session { + SessionId = _lastSessionId, TokenId = accessItem.Token.TokenId, ClientInfo = sessionRequestEx.ClientInfo, CreatedTime = FastDateTime.Now, @@ -77,7 +122,7 @@ public SessionResponseEx CreateSession(SessionRequestEx sessionRequestEx, HostEndPoint = sessionRequestEx.HostEndPoint, ClientIp = sessionRequestEx.ClientIp, ExtraData = sessionRequestEx.ExtraData, - ExpirationTime = accessItem.AdShow is AdShow.Required ? DateTime.UtcNow + _adRequiredTimeout : null + ExpirationTime = accessItem.AdRequirement is AdRequirement.Required ? DateTime.UtcNow + _adRequiredTimeout : null }; //create response @@ -87,10 +132,10 @@ public SessionResponseEx CreateSession(SessionRequestEx sessionRequestEx, return ret; // add the new session to collection - session.SessionId = ++_lastSessionId; Sessions.TryAdd(session.SessionId, session); - ret.SessionId = session.SessionId; + System.IO.File.WriteAllText(GetSessionFilePath(session.SessionId), JsonSerializer.Serialize(session)); + ret.SessionId = session.SessionId; return ret; } @@ -204,39 +249,44 @@ private SessionResponseEx BuildSessionResponse(Session session, FileAccessManage ErrorMessage = session.ErrorMessage, AccessUsage = accessUsage, RedirectHostEndPoint = null, - IsAdRequired = accessItem.AdShow is AdShow.Required, - AdShow = accessItem.AdShow + IsAdRequired = accessItem.AdRequirement is AdRequirement.Required, + AdRequirement = accessItem.AdRequirement }; } public void CloseSession(ulong sessionId) { - if (Sessions.TryGetValue(sessionId, out var session)) - { - if (session.ErrorCode == SessionErrorCode.Ok) - session.ErrorCode = SessionErrorCode.SessionClosed; - session.Kill(); - } + if (!Sessions.TryGetValue(sessionId, out var session)) + return; + + if (session.ErrorCode == SessionErrorCode.Ok) + session.ErrorCode = SessionErrorCode.SessionClosed; + + session.Kill(); } public class Session { - public uint SessionId { get; internal set; } + public required ulong SessionId { get; init; } public required string TokenId { get; init; } - public ClientInfo ClientInfo { get; internal set; } = null!; - public byte[] SessionKey { get; internal set; } = null!; - public DateTime CreatedTime { get; internal set; } = FastDateTime.Now; - public DateTime LastUsedTime { get; internal set; } = FastDateTime.Now; - public DateTime? EndTime { get; internal set; } + public required ClientInfo ClientInfo { get; init; } + public required byte[] SessionKey { get; init; } + public DateTime CreatedTime { get; init; } = FastDateTime.Now; + public DateTime LastUsedTime { get; init; } = FastDateTime.Now; + public DateTime? EndTime { get; set; } public DateTime? ExpirationTime { get; set; } public bool IsAlive => EndTime == null; - public SessionSuppressType SuppressedBy { get; internal set; } - public SessionSuppressType SuppressedTo { get; internal set; } - public SessionErrorCode ErrorCode { get; internal set; } - public string? ErrorMessage { get; internal set; } - public IPEndPoint HostEndPoint { get; internal set; } = null!; - public IPAddress? ClientIp { get; internal set; } - public string? ExtraData { get; internal set; } + public SessionSuppressType SuppressedBy { get; set; } + public SessionSuppressType SuppressedTo { get; set; } + public SessionErrorCode ErrorCode { get; set; } + public string? ErrorMessage { get; init; } + public string? ExtraData { get; init; } + + [JsonConverter(typeof(IPEndPointConverter))] + public required IPEndPoint HostEndPoint { get; set; } + + [JsonConverter(typeof(IPAddressConverter))] + public IPAddress? ClientIp { get; init; } public void Kill() { diff --git a/VpnHood.Server.Access/Messaging/SessionRequestEx.cs b/VpnHood.Server.Access/Messaging/SessionRequestEx.cs index 29a046524..a4b5ce22a 100644 --- a/VpnHood.Server.Access/Messaging/SessionRequestEx.cs +++ b/VpnHood.Server.Access/Messaging/SessionRequestEx.cs @@ -18,6 +18,6 @@ public class SessionRequestEx public required IPAddress? ClientIp { get; set; } public required string? ExtraData { get; set; } - public string? RegionId { get; set; } + public string? ServerLocation { get; set; } public bool AllowRedirect { get; set; } = true; } \ No newline at end of file diff --git a/VpnHood.Server.Access/Messaging/SessionResponseEx.cs b/VpnHood.Server.Access/Messaging/SessionResponseEx.cs index eea846e97..430e7cb56 100644 --- a/VpnHood.Server.Access/Messaging/SessionResponseEx.cs +++ b/VpnHood.Server.Access/Messaging/SessionResponseEx.cs @@ -1,11 +1,9 @@ -using System.Text.Json.Serialization; -using VpnHood.Common.Messaging; +using VpnHood.Common.Messaging; namespace VpnHood.Server.Access.Messaging; public class SessionResponseEx : SessionResponse { - [JsonIgnore(Condition =JsonIgnoreCondition.WhenWritingNull)] public string? ExtraData { get; set; } public string? GaMeasurementId { get; set; } public string? AccessKey { get; set; } @@ -14,6 +12,6 @@ public class SessionResponseEx : SessionResponse public ulong SessionId { get; set; } public byte[] SessionKey { get; set; } = []; public bool IsAdRequired { get; set; } //todo: deprecated in version 504 or later - public AdShow AdShow { get; set; } = AdShow.None; - + public AdRequirement AdRequirement { get; set; } = AdRequirement.None; + public string? ServerLocation { get; set; } } \ No newline at end of file diff --git a/VpnHood.Server.Access/VpnHood.Server.Access.csproj b/VpnHood.Server.Access/VpnHood.Server.Access.csproj index 315ba6d3f..d5b42a3b1 100644 --- a/VpnHood.Server.Access/VpnHood.Server.Access.csproj +++ b/VpnHood.Server.Access/VpnHood.Server.Access.csproj @@ -20,7 +20,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Server.App.Net/ServerApp.cs b/VpnHood.Server.App.Net/ServerApp.cs index 5fcd46cc5..fec5fdb58 100644 --- a/VpnHood.Server.App.Net/ServerApp.cs +++ b/VpnHood.Server.App.Net/ServerApp.cs @@ -64,7 +64,10 @@ public ServerApp() AppSettings = File.Exists(appSettingsFilePath) ? VhUtil.JsonDeserialize(File.ReadAllText(appSettingsFilePath)) : new AppSettings(); + + // Init File Logger before starting server VhLogger.IsDiagnoseMode = AppSettings.IsDiagnoseMode; + InitFileLogger(storagePath); //create command Listener _commandListener = new CommandListener(Path.Combine(storagePath, FileNameAppCommand)); @@ -89,7 +92,7 @@ public ServerApp() : CreateFileAccessManager(StoragePath, AppSettings.FileAccessManager); } - private void InitFileLogger() + private void InitFileLogger(string storagePath) { var configFilePath = Path.Combine(StoragePath, "NLog.config"); if (!File.Exists(configFilePath)) configFilePath = Path.Combine(AppFolderPath, "NLog.config"); @@ -101,7 +104,7 @@ private void InitFileLogger() if (AppSettings.IsDiagnoseMode) builder.SetMinimumLevel(LogLevel.Trace); }); - LogManager.Configuration.Variables["mydir"] = StoragePath; + LogManager.Configuration.Variables["mydir"] = storagePath; VhLogger.Instance = loggerFactory.CreateLogger("NLog"); } else @@ -221,8 +224,7 @@ private void StartServer(CommandLineApplication cmdApp) "There is no token in the store! Use the following command to create one:\n " + "dotnet VpnHoodServer.dll gen -?"); - // Init File Logger before starting server; other log should be on console or other file - InitFileLogger(); + // Init logger for http access manager if (AccessManager is HttpAccessManager httpAccessManager) httpAccessManager.Logger = VhLogger.Instance; diff --git a/VpnHood.Server.App.Net/VpnHood.Server.App.Net.csproj b/VpnHood.Server.App.Net/VpnHood.Server.App.Net.csproj index 7ef494350..3765a32e0 100644 --- a/VpnHood.Server.App.Net/VpnHood.Server.App.Net.csproj +++ b/VpnHood.Server.App.Net/VpnHood.Server.App.Net.csproj @@ -22,7 +22,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) VpnHoodServer @@ -49,8 +49,8 @@ - - + + diff --git a/VpnHood.Server/ServerHost.cs b/VpnHood.Server/ServerHost.cs index a4e21f872..ad99a53ef 100644 --- a/VpnHood.Server/ServerHost.cs +++ b/VpnHood.Server/ServerHost.cs @@ -16,6 +16,7 @@ using VpnHood.Tunneling.ClientStreams; using VpnHood.Tunneling.Messaging; using VpnHood.Tunneling.Utils; +using Exception = System.Exception; namespace VpnHood.Server; @@ -23,20 +24,17 @@ internal class ServerHost : IAsyncDisposable, IJob { private readonly HashSet _clientStreams = []; private const int ServerProtocolVersion = 5; - private CancellationTokenSource _cancellationTokenSource = new(); + private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly SessionManager _sessionManager; private readonly List _tcpListeners; private readonly List _udpChannelTransmitters = []; - private Task? _listenerTask; + private readonly List _tcpListenerTasks = []; private bool _disposed; public JobSection JobSection { get; } = new(TimeSpan.FromMinutes(5)); public bool IsIpV6Supported { get; set; } public IpRange[]? NetFilterPacketCaptureIncludeIpRanges { get; set; } public IpRange[]? NetFilterIncludeIpRanges { get; set; } - public bool IsStarted { get; private set; } - public IPEndPoint[] TcpEndPoints { get; private set; } = []; - public IPEndPoint[] UdpEndPoints { get; private set; } = []; public IPAddress[]? DnsServers { get; set; } public CertificateHostName[] Certificates { get; private set; } = []; @@ -47,160 +45,111 @@ public ServerHost(SessionManager sessionManager) JobRunner.Default.Add(this); } - private async Task Start(IPEndPoint[] tcpEndPoints, IPEndPoint[] udpEndPoints) + public async Task Configure(IPEndPoint[] tcpEndPoints, IPEndPoint[] udpEndPoints, + IPAddress[]? dnsServers, X509Certificate2[] certificates) { - if (_disposed) throw new ObjectDisposedException(GetType().Name); - if (IsStarted) throw new Exception("ServerHost is already Started!"); - if (tcpEndPoints.Length == 0) throw new ArgumentNullException(nameof(tcpEndPoints), "No TcpEndPoint has been configured."); + if (VhUtil.IsNullOrEmpty(certificates)) + throw new ArgumentNullException(nameof(certificates), "No certificate has been configured."); - _cancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = _cancellationTokenSource.Token; - IsStarted = true; - lock (_stopLock) _stopTask = null; - - try - { - //start UDPs - lock (_udpChannelTransmitters) - { - foreach (var udpEndPoint in udpEndPoints) - { - if (udpEndPoint.Port != 0) - VhLogger.Instance.LogInformation("Start listening on UdpEndPoint: {UdpEndPoint}", VhLogger.Format(udpEndPoint)); - - try - { - var udpClient = new UdpClient(udpEndPoint); - var udpChannelTransmitter = new ServerUdpChannelTransmitter(udpClient, _sessionManager); - _udpChannelTransmitters.Add(udpChannelTransmitter); - - if (udpEndPoint.Port == 0) - VhLogger.Instance.LogInformation("Started listening on UdpEndPoint: {UdpEndPoint}", VhLogger.Format(udpChannelTransmitter.LocalEndPoint)); - } - catch (Exception ex) - { - ex.Data.Add("UdpEndPoint", udpEndPoint); - throw; - } - } + if (_disposed) + throw new ObjectDisposedException(GetType().Name); - UdpEndPoints = udpEndPoints; - } + // wait for last configure to finish + using var lockResult = await _configureLock.LockAsync(_cancellationTokenSource.Token); - //start TCPs - var tasks = new List(); - lock (_tcpListeners) - { - foreach (var tcpEndPoint in tcpEndPoints) - { - try - { - VhLogger.Instance.LogInformation("Start listening on TcpEndPoint: {TcpEndPoint}", VhLogger.Format(tcpEndPoint)); - cancellationToken.ThrowIfCancellationRequested(); - var tcpListener = new TcpListener(tcpEndPoint); - tcpListener.Start(); - _tcpListeners.Add(tcpListener); - tasks.Add(ListenTask(tcpListener, cancellationToken)); - } - catch (Exception ex) - { - ex.Data.Add("TcpEndPoint", tcpEndPoint); - throw; - } - } - } - TcpEndPoints = tcpEndPoints; - _listenerTask = Task.WhenAll(tasks); - } - catch - { - await Stop(); - throw; - } - } - - private readonly AsyncLock _stopLock = new(); - private Task? _stopTask; - - public Task Stop() - { - lock (_stopLock) - _stopTask ??= StopCore(); + // reconfigure + DnsServers = dnsServers; + Certificates = certificates.Select(x => new CertificateHostName(x)).ToArray(); - return _stopTask; + // Configure + await Task.WhenAll(ConfigureUdpListeners(udpEndPoints), ConfigureTcpListeners(tcpEndPoints)); + _tcpListenerTasks.RemoveAll(x => x.IsCompleted); } - private async Task StopCore() + private readonly AsyncLock _configureLock = new(); + private Task ConfigureUdpListeners(IPEndPoint[] udpEndPoints) { - if (!IsStarted) - return; + // UDP port zero must be specified in preparation + if (udpEndPoints.Any(x => x.Port == 0)) + throw new InvalidOperationException("UDP port has not been specified."); - VhLogger.Instance.LogTrace("Stopping ServerHost..."); - _cancellationTokenSource.Cancel(); - - // UDPs - VhLogger.Instance.LogTrace("Disposing UdpChannelTransmitters..."); - lock (_udpChannelTransmitters) + // stop listeners that are not in the list + foreach (var udpChannelTransmitter in _udpChannelTransmitters + .Where(x => !udpEndPoints.Contains(x.LocalEndPoint)).ToArray()) { - foreach (var udpChannelClient in _udpChannelTransmitters) - udpChannelClient.Dispose(); - _udpChannelTransmitters.Clear(); + VhLogger.Instance.LogInformation("Stop listening on UdpEndPoint: {UdpEndPoint}", VhLogger.Format(udpChannelTransmitter.LocalEndPoint)); + udpChannelTransmitter.Dispose(); + _udpChannelTransmitters.Remove(udpChannelTransmitter); } - // TCPs - VhLogger.Instance.LogTrace("Disposing TcpListeners..."); - lock (_tcpListeners) + // start new listeners + foreach (var udpEndPoint in udpEndPoints) { - foreach (var tcpListener in _tcpListeners) - tcpListener.Stop(); - _tcpListeners.Clear(); - } + // ignore already listening + if (_udpChannelTransmitters.Any(x => x.LocalEndPoint.Equals(udpEndPoint))) + continue; - // dispose clientStreams - VhLogger.Instance.LogTrace("Disposing ClientStreams..."); - Task[] disposeTasks; - lock (_clientStreams) - disposeTasks = _clientStreams.Select(x => x.DisposeAsync(false).AsTask()).ToArray(); - await Task.WhenAll(disposeTasks); + // report if port is set + if (udpEndPoint.Port == 0) + VhLogger.Instance.LogInformation("Start listening on UdpEndPoint: {UdpEndPoint}", VhLogger.Format(udpEndPoint)); - VhLogger.Instance.LogTrace("Disposing current processing requests..."); - try - { - if (_listenerTask != null) - await _listenerTask; - } - catch (Exception ex) - { - VhLogger.Instance.LogTrace(ex, "Error in stopping ServerHost."); + try + { + var udpClient = new UdpClient(udpEndPoint); + var udpChannelTransmitter = new ServerUdpChannelTransmitter(udpClient, _sessionManager); + _udpChannelTransmitters.Add(udpChannelTransmitter); + + if (udpEndPoint.Port == 0) + VhLogger.Instance.LogInformation("Started listening on UdpEndPoint: {UdpEndPoint}", VhLogger.Format(udpChannelTransmitter.LocalEndPoint)); + } + catch (Exception ex) + { + ex.Data.Add("UdpEndPoint", udpEndPoint); + throw; + } } - _listenerTask = null; - IsStarted = false; + return Task.CompletedTask; } - public async Task Configure(IPEndPoint[] tcpEndPoints, IPEndPoint[] udpEndPoints, - IPAddress[]? dnsServers, X509Certificate2[] certificates) + private Task ConfigureTcpListeners(IPEndPoint[] tcpEndPoints) { - if (VhUtil.IsNullOrEmpty(certificates)) throw new ArgumentNullException(nameof(certificates), "Certificates has not been configured."); + if (tcpEndPoints.Length == 0) + throw new ArgumentNullException(nameof(tcpEndPoints), "No TcpEndPoint has been configured."); - // Clear certificate cache - DnsServers = dnsServers; - Certificates = certificates.Select(x => new CertificateHostName(x)).ToArray(); - - // Restart if endPoints has been changed - if (IsStarted && - (!TcpEndPoints.SequenceEqual(tcpEndPoints) || - !UdpEndPoints.SequenceEqual(udpEndPoints))) + // start TCPs + // stop listeners that are not in the new list + foreach (var tcpListener in _tcpListeners + .Where(x => !tcpEndPoints.Contains((IPEndPoint)x.LocalEndpoint)).ToArray()) { - VhLogger.Instance.LogInformation("EndPoints has been changed. Stopping ServerHost..."); - await Stop(); + VhLogger.Instance.LogInformation("Stop listening on TcpEndPoint: {TcpEndPoint}", VhLogger.Format(tcpListener.LocalEndpoint)); + tcpListener.Stop(); + _tcpListeners.Remove(tcpListener); } - if (!IsStarted) + foreach (var tcpEndPoint in tcpEndPoints) { - VhLogger.Instance.LogInformation("Starting ServerHost..."); - await Start(tcpEndPoints, udpEndPoints); + // check already listening + if (_tcpListeners.Any(x => x.LocalEndpoint.Equals(tcpEndPoint))) + continue; + + try + { + VhLogger.Instance.LogInformation("Start listening on TcpEndPoint: {TcpEndPoint}", VhLogger.Format(tcpEndPoint)); + var tcpListener = new TcpListener(tcpEndPoint); + tcpListener.Start(); + _tcpListeners.Add(tcpListener); + _tcpListenerTasks.Add(ListenTask(tcpListener, _cancellationTokenSource.Token)); + } + catch (Exception ex) + { + VhLogger.Instance.LogInformation("Error listening on TcpEndPoint: {TcpEndPoint}", VhLogger.Format(tcpEndPoint)); + ex.Data.Add("TcpEndPoint", tcpEndPoint); + throw; + } } + + return Task.CompletedTask; } private async Task ListenTask(TcpListener tcpListener, CancellationToken cancellationToken) @@ -215,6 +164,9 @@ private async Task ListenTask(TcpListener tcpListener, CancellationToken cancell try { var tcpClient = await tcpListener.AcceptTcpClientAsync(); + if (_disposed) + throw new ObjectDisposedException("ServerHost has been stopped."); + VhUtil.ConfigTcpClient(tcpClient, _sessionManager.SessionOptions.TcpKernelSendBufferSize, _sessionManager.SessionOptions.TcpKernelReceiveBufferSize); @@ -223,6 +175,10 @@ private async Task ListenTask(TcpListener tcpListener, CancellationToken cancell _ = ProcessTcpClient(tcpClient, cancellationToken); errorCounter = 0; } + catch (Exception) when (!tcpListener.Server.IsBound) + { + break; + } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) { errorCounter++; @@ -234,12 +190,17 @@ private async Task ListenTask(TcpListener tcpListener, CancellationToken cancell catch (Exception ex) { errorCounter++; - VhLogger.Instance.LogError(GeneralEventId.Tcp, ex, "ServerHost could not AcceptTcpClient. ErrorCounter: {ErrorCounter}", errorCounter); if (errorCounter > maxErrorCount) { - VhLogger.Instance.LogError("Too many unexpected errors in AcceptTcpClient. Stopping the ServerHost..."); - _ = Stop(); + VhLogger.Instance.LogError( + "Too many unexpected errors in AcceptTcpClient. Stopping the Listener... LocalEndPint: {LocalEndPint}", + tcpListener.LocalEndpoint); + break; } + + VhLogger.Instance.LogError(GeneralEventId.Tcp, ex, + "ServerHost could not AcceptTcpClient. LocalEndPint: {LocalEndPint}, ErrorCounter: {ErrorCounter}", + tcpListener.LocalEndpoint, errorCounter); } } @@ -292,19 +253,6 @@ private async Task CreateClientStream(TcpClient tcpClient, Stream VhLogger.Instance.LogTrace(GeneralEventId.Tcp, "Waiting for request..."); var streamId = Guid.NewGuid() + ":incoming"; - // todo: deprecated on >= 451 { - #region Deprecated on >= 451 - var buffer = new byte[16]; - var res = await sslStream.ReadAsync(buffer, 0, 1, cancellationToken); - if (res == 0) - throw new Exception("Connection has been closed before receiving any request."); - - // check request version - var version = buffer[0]; - if (version == 1) - return new TcpClientStream(tcpClient, new ReadCacheStream(sslStream, false, cacheData: [version], cacheSize: 1), streamId); - #endregion - // Version 2 is HTTP and starts with POST try { @@ -312,11 +260,10 @@ private async Task CreateClientStream(TcpClient tcpClient, Stream await HttpUtil.ParseHeadersAsync(sslStream, cancellationToken) ?? throw new Exception("Connection has been closed before receiving any request."); - int.TryParse(headers.GetValueOrDefault("X-Version", "0"), out var xVersion); + // int.TryParse(headers.GetValueOrDefault("X-Version", "0"), out var xVersion); Enum.TryParse(headers.GetValueOrDefault("X-BinaryStream", ""), out var binaryStreamType); bool.TryParse(headers.GetValueOrDefault("X-Buffered", "true"), out var useBuffer); var authorization = headers.GetValueOrDefault("Authorization", string.Empty); - if (xVersion == 2) binaryStreamType = BinaryStreamType.Custom; // read api key if (!CheckApiKeyAuthorization(authorization)) @@ -334,14 +281,6 @@ await HttpUtil.ParseHeadersAsync(sslStream, cancellationToken) switch (binaryStreamType) { - case BinaryStreamType.Custom: - { - await sslStream.DisposeAsync(); // dispose Ssl - var xSecret = headers.GetValueOrDefault("X-Secret", string.Empty); - var secret = Convert.FromBase64String(xSecret); - return new TcpClientStream(tcpClient, new BinaryStreamCustom(tcpClient.GetStream(), streamId, secret, useBuffer), streamId, ReuseClientStream); - } - case BinaryStreamType.Standard: return new TcpClientStream(tcpClient, new BinaryStreamStandard(tcpClient.GetStream(), streamId, useBuffer), streamId, ReuseClientStream); @@ -511,6 +450,14 @@ private async Task ProcessRequest(IClientStream clientStream, CancellationToken var requestCode = (RequestCode)buffer[0]; switch (requestCode) { + case RequestCode.ServerStatus: + await ProcessServerStatus(clientStream, cancellationToken); + break; + + case RequestCode.SessionStatus: + await ProcessSessionStatus(clientStream, cancellationToken); + break; + case RequestCode.Hello: await ProcessHello(clientStream, cancellationToken); break; @@ -602,9 +549,10 @@ private async Task ProcessHello(IClientStream clientStream, CancellationToken ca VhLogger.Instance.LogTrace(GeneralEventId.Session, $"Replying Hello response. SessionId: {VhLogger.FormatSessionId(sessionResponse.SessionId)}"); - var udpPort = - UdpEndPoints.SingleOrDefault(x => x.Address.Equals(ipEndPointPair.LocalEndPoint.Address))?.Port ?? - UdpEndPoints.SingleOrDefault(x => x.Address.Equals(IPAddressUtil.GetAnyIpAddress(ipEndPointPair.LocalEndPoint.AddressFamily)))?.Port; + // find udp port that match to the same tcp local IpAddress + var udpPort = _udpChannelTransmitters + .SingleOrDefault(x => x.LocalEndPoint.Address.Equals(ipEndPointPair.LocalEndPoint.Address))?.LocalEndPoint + .Port; var helloResponse = new HelloResponse { @@ -633,7 +581,8 @@ private async Task ProcessHello(IClientStream clientStream, CancellationToken ca AccessKey = sessionResponse.AccessKey, DnsServers = DnsServers, IsAdRequired = sessionResponse.IsAdRequired, - AdShow = sessionResponse.AdShow + AdRequirement = sessionResponse.AdRequirement, + ServerLocation = sessionResponse.ServerLocation }; await StreamUtil.WriteJsonAsync(clientStream.Stream, helloResponse, cancellationToken); await clientStream.DisposeAsync(); @@ -647,6 +596,35 @@ private async Task ProcessAdRewardRequest(IClientStream clientStream, Cancellati await session.ProcessAdRewardRequest(request, clientStream, cancellationToken); } + private static async Task ProcessServerStatus(IClientStream clientStream, CancellationToken cancellationToken) + { + VhLogger.Instance.LogTrace(GeneralEventId.Session, "Reading the ServerStatus request..."); + var request = await ReadRequest(clientStream, cancellationToken); + + // Before calling CloseSession. Session must be validated by GetSession + await StreamUtil.WriteJsonAsync(clientStream.Stream, + new ServerStatusResponse + { + ErrorCode = SessionErrorCode.Ok, + Message = request.Message == "Hi, How are you?" ? "I am OK. How are you?" : "OK. Who are you?" + + },cancellationToken); + + await clientStream.DisposeAsync(); + } + + private async Task ProcessSessionStatus(IClientStream clientStream, CancellationToken cancellationToken) + { + VhLogger.Instance.LogTrace(GeneralEventId.Session, "Reading the SessionStatus request..."); + var request = await ReadRequest(clientStream, cancellationToken); + + // finding session + var session = await _sessionManager.GetSession(request, clientStream.IpEndPointPair); + + // processing request + await session.ProcessSessionStatusRequest(request, clientStream, cancellationToken); + } + private async Task ProcessBye(IClientStream clientStream, CancellationToken cancellationToken) { VhLogger.Instance.LogTrace(GeneralEventId.Session, "Reading the Bye request..."); @@ -674,7 +652,6 @@ private async Task ProcessUdpPacketRequest(IClientStream clientStream, Cancellat await session.ProcessUdpPacketRequest(request, clientStream, cancellationToken); } - private async Task ProcessTcpDatagramChannel(IClientStream clientStream, CancellationToken cancellationToken) { VhLogger.Instance.LogTrace(GeneralEventId.StreamProxyChannel, "Reading the TcpDatagramChannelRequest..."); @@ -705,23 +682,54 @@ public Task RunJob() return Task.CompletedTask; } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - public ValueTask DisposeAsync() + private async Task Stop() { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(); - return _disposeTask.Value; + VhLogger.Instance.LogTrace("Stopping ServerHost..."); + _cancellationTokenSource.Cancel(); + + // UDPs + VhLogger.Instance.LogTrace("Disposing UdpChannelTransmitters..."); + foreach (var udpChannelClient in _udpChannelTransmitters) + udpChannelClient.Dispose(); + _udpChannelTransmitters.Clear(); + + // TCPs + VhLogger.Instance.LogTrace("Disposing TcpListeners..."); + foreach (var tcpListener in _tcpListeners) + tcpListener.Stop(); + _tcpListeners.Clear(); + + // dispose clientStreams + VhLogger.Instance.LogTrace("Disposing ClientStreams..."); + Task[] disposeTasks; + lock (_clientStreams) + disposeTasks = _clientStreams.Select(x => x.DisposeAsync(false).AsTask()).ToArray(); + await Task.WhenAll(disposeTasks); + + // wait for finalizing all listener tasks + VhLogger.Instance.LogTrace("Disposing current processing requests..."); + try + { + await VhUtil.RunTask(Task.WhenAll(_tcpListenerTasks), TimeSpan.FromSeconds(15)); + } + catch (Exception ex) + { + if (ex is TimeoutException) + VhLogger.Instance.LogError(ex, "Error in stopping ServerHost."); + } + _tcpListenerTasks.Clear(); } - private async ValueTask DisposeAsyncCore() + private readonly AsyncLock _disposeLock = new(); + public async ValueTask DisposeAsync() { + using var lockResult = await _disposeLock.LockAsync(); if (_disposed) return; _disposed = true; - await Stop(); } + // cache HostName for performance public class CertificateHostName(X509Certificate2 certificate) { public string HostName { get; } = certificate.GetNameInfo(X509NameType.DnsName, false) ?? throw new Exception("Could not get the HostName from the certificate."); diff --git a/VpnHood.Server/Session.cs b/VpnHood.Server/Session.cs index 7f5d06127..aed73f202 100644 --- a/VpnHood.Server/Session.cs +++ b/VpnHood.Server/Session.cs @@ -13,7 +13,6 @@ using VpnHood.Server.Exceptions; using VpnHood.Tunneling; using VpnHood.Tunneling.Channels; -using VpnHood.Tunneling.Channels.Streams; using VpnHood.Tunneling.ClientStreams; using VpnHood.Tunneling.Factory; using VpnHood.Tunneling.Messaging; @@ -108,7 +107,9 @@ internal Session(IAccessManager accessManager, SessionResponseEx sessionResponse public Task RunJob() { - return Sync(true, false); + return IsDisposed + ? Task.CompletedTask + : Sync(true, false); } public bool UseUdpChannel @@ -281,6 +282,12 @@ public Task ProcessUdpPacketRequest(UdpPacketRequest request, IClientStream clie throw new NotImplementedException(); } + public async Task ProcessSessionStatusRequest(SessionStatusRequest request, IClientStream clientStream, CancellationToken cancellationToken) + { + await StreamUtil.WriteJsonAsync(clientStream.Stream, SessionResponse, cancellationToken); + await clientStream.DisposeAsync(); + } + public async Task ProcessAdRewardRequest(AdRewardRequest request, IClientStream clientStream, CancellationToken cancellationToken) { await Sync(force: true, closeSession: false, adData: request.AdData); @@ -328,10 +335,6 @@ await VhUtil.RunTask( // send response await StreamUtil.WriteJsonAsync(clientStream.Stream, SessionResponse, cancellationToken); - // MaxEncryptChunk - if (clientStream.Stream is BinaryStreamCustom binaryStream) - binaryStream.MaxEncryptChunk = TunnelDefaults.TcpProxyEncryptChunkCount; - // add the connection VhLogger.Instance.LogTrace(GeneralEventId.StreamProxyChannel, "Adding a StreamProxyChannel. SessionId: {SessionId}", VhLogger.FormatSessionId(SessionId)); diff --git a/VpnHood.Server/SessionManager.cs b/VpnHood.Server/SessionManager.cs index 3a7ffb3cc..295a41307 100644 --- a/VpnHood.Server/SessionManager.cs +++ b/VpnHood.Server/SessionManager.cs @@ -23,7 +23,6 @@ public class SessionManager : IAsyncDisposable, IJob private readonly IAccessManager _accessManager; private readonly SocketFactory _socketFactory; private byte[] _serverSecret; - private bool _disposed; private readonly JobSection _heartbeatSection = new(TimeSpan.FromMinutes(10)); public string ApiKey { get; private set; } @@ -122,7 +121,7 @@ public async Task CreateSession(HelloRequest helloRequest, IP ClientInfo = helloRequest.ClientInfo, EncryptedClientId = helloRequest.EncryptedClientId, TokenId = helloRequest.TokenId, - RegionId = helloRequest.RegionId, + ServerLocation = helloRequest.ServerLocation, AllowRedirect = helloRequest.AllowRedirect }); @@ -327,21 +326,15 @@ public async Task CloseSession(ulong sessionId) await session.Close(); } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - - public ValueTask DisposeAsync() - { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(); - return _disposeTask.Value; - } - - private async ValueTask DisposeAsyncCore() + private bool _disposed; + private readonly AsyncLock _disposeLock = new(); + public async ValueTask DisposeAsync() { + using var lockResult = await _disposeLock.LockAsync(); if (_disposed) return; _disposed = true; await Task.WhenAll(Sessions.Values.Select(x => x.DisposeAsync().AsTask())); } -} \ No newline at end of file +} + diff --git a/VpnHood.Server/VpnHood.Server.csproj b/VpnHood.Server/VpnHood.Server.csproj index 061ebb7f0..5e2fe036e 100644 --- a/VpnHood.Server/VpnHood.Server.csproj +++ b/VpnHood.Server/VpnHood.Server.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Server/VpnHoodServer.cs b/VpnHood.Server/VpnHoodServer.cs index f2fbd15cb..ac460ed5e 100644 --- a/VpnHood.Server/VpnHoodServer.cs +++ b/VpnHood.Server/VpnHoodServer.cs @@ -190,7 +190,6 @@ await _serverHost.Configure(serverConfig.TcpEndPointsValue, serverConfig.UdpEndP _ = SessionManager.GaTracker?.TrackErrorByTag("configure", ex.Message); VhLogger.Instance.LogError(ex, "Could not configure server! Retrying after {TotalSeconds} seconds.", JobSection.Interval.TotalSeconds); - await _serverHost.Stop(); await SendStatusToAccessManager(false); } } @@ -378,17 +377,10 @@ public void Dispose() .GetResult(); } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - public ValueTask DisposeAsync() - { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(); - return _disposeTask.Value; - } - - private async ValueTask DisposeAsyncCore() + private readonly AsyncLock _disposeLock = new(); + public async ValueTask DisposeAsync() { + using var lockResult = await _disposeLock.LockAsync(); if (_disposed) return; _disposed = true; diff --git a/VpnHood.Tunneling/Channels/StreamDatagramChannel.cs b/VpnHood.Tunneling/Channels/StreamDatagramChannel.cs index 741090417..00fab48b7 100644 --- a/VpnHood.Tunneling/Channels/StreamDatagramChannel.cs +++ b/VpnHood.Tunneling/Channels/StreamDatagramChannel.cs @@ -14,7 +14,6 @@ public class StreamDatagramChannel : IDatagramChannel, IJob private readonly byte[] _buffer = new byte[0xFFFF]; private const int Mtu = 0xFFFF; private readonly IClientStream _clientStream; - private bool _disposed; private readonly DateTime _lifeTime = DateTime.MaxValue; private readonly SemaphoreSlim _sendSemaphore = new(1, 1); private readonly CancellationTokenSource _cancellationTokenSource = new(); @@ -220,22 +219,18 @@ public Task RunJob() return Task.CompletedTask; } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; public ValueTask DisposeAsync() { return DisposeAsync(true); } - public ValueTask DisposeAsync(bool graceful) + private bool _disposed; + private readonly AsyncLock _disposeLock = new(); + public async ValueTask DisposeAsync(bool graceful) { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(graceful); - return _disposeTask.Value; - } + using var lockResult = await _disposeLock.LockAsync(); + if (_disposed) return; - private async ValueTask DisposeAsyncCore(bool graceful) - { if (graceful) await SendClose(); // this won't throw any error diff --git a/VpnHood.Tunneling/Channels/StreamProxyChannel.cs b/VpnHood.Tunneling/Channels/StreamProxyChannel.cs index 05c2be41c..75ab174f8 100644 --- a/VpnHood.Tunneling/Channels/StreamProxyChannel.cs +++ b/VpnHood.Tunneling/Channels/StreamProxyChannel.cs @@ -17,8 +17,6 @@ public class StreamProxyChannel : IChannel, IJob private const int BufferSizeDefault = TunnelDefaults.StreamProxyBufferSize; private const int BufferSizeMax = 0x14000; private const int BufferSizeMin = 0x1000; - private bool _disposed; - public JobSection JobSection { get; } = new(TunnelDefaults.TcpCheckInterval); public bool Connected { get; private set; } public Traffic Traffic { get; } = new(); @@ -173,24 +171,20 @@ private async Task CopyToInternalAsync(Stream source, Stream destination, bool i } } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; public ValueTask DisposeAsync() { return DisposeAsync(true); } - public ValueTask DisposeAsync(bool graceful) + private bool _disposed; + private readonly AsyncLock _disposeLock = new(); + public async ValueTask DisposeAsync(bool graceful) { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(graceful); - return _disposeTask.Value; - } + using var lockResult = await _disposeLock.LockAsync(); + if (_disposed) return; + _disposed = true; - private async ValueTask DisposeAsyncCore(bool graceful) - { Connected = false; - _disposed = true; await Task.WhenAll( _hostTcpClientStream.DisposeAsync(graceful).AsTask(), _tunnelTcpClientStream.DisposeAsync(graceful).AsTask()); diff --git a/VpnHood.Tunneling/Channels/Streams/BinaryStreamCustom.cs b/VpnHood.Tunneling/Channels/Streams/BinaryStreamCustom.cs deleted file mode 100644 index 11b47b2ff..000000000 --- a/VpnHood.Tunneling/Channels/Streams/BinaryStreamCustom.cs +++ /dev/null @@ -1,332 +0,0 @@ -using System.Net.Sockets; -using Microsoft.Extensions.Logging; -using VpnHood.Common.Logging; -using VpnHood.Common.Utils; - -namespace VpnHood.Tunneling.Channels.Streams; - -// this was used for .NET 6 or earlier as .net didn't use hardware acceleration for encryption in android and SslStream was very slow -// todo remove -[Obsolete] -public class BinaryStreamCustom : ChunkStream -{ - private const int ChunkHeaderLength = 5; - private int _remainingChunkBytes; - private bool _disposed; - private bool _finished; - private bool _hasError; - private readonly CancellationTokenSource _readCts = new(); - private readonly CancellationTokenSource _writeCts = new(); - private Task _readTask = Task.FromResult(0); - private Task _writeTask = Task.CompletedTask; - private bool _isConnectionClosed; - private bool _isCurrentReadingChunkEncrypted; - private readonly StreamCryptor _streamCryptor; - private readonly byte[] _readChunkHeaderBuffer = new byte[ChunkHeaderLength]; - private byte[] _writeBuffer = []; - - public long MaxEncryptChunk { get; set; } = long.MaxValue; - public override int PreserveWriteBufferLength => ChunkHeaderLength; - - [Obsolete] - public BinaryStreamCustom(Stream sourceStream, string streamId, byte[] secret, bool useBuffer) - : base(useBuffer ? new ReadCacheStream(sourceStream, false, TunnelDefaults.StreamProxyBufferSize) : sourceStream, streamId) - { - _streamCryptor = StreamCryptor.Create(SourceStream, secret, leaveOpen: true, encryptInGivenBuffer: true); - } - - private BinaryStreamCustom(Stream sourceStream, string streamId, StreamCryptor streamCryptor, int reusedCount) - : base(sourceStream, streamId, reusedCount) - { - _streamCryptor = streamCryptor; - } - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (_disposed) - throw new ObjectDisposedException(GetType().Name); - - // Create CancellationToken - using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(_readCts.Token, cancellationToken); - _readTask = ReadInternalAsync(buffer, offset, count, tokenSource.Token); - return await _readTask; // await needed to dispose tokenSource - } - - private async Task ReadInternalAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - try - { - // check is stream has finished - if (_finished) - return 0; - - // If there are no more in the chunks read the next chunk - if (_remainingChunkBytes == 0) - { - _remainingChunkBytes = await ReadChunkHeaderAsync(cancellationToken); - _finished = _remainingChunkBytes == 0; - - // check last chunk - if (_finished) - return 0; - } - - var bytesToRead = Math.Min(_remainingChunkBytes, count); - var stream = _isCurrentReadingChunkEncrypted ? _streamCryptor : SourceStream; - var bytesRead = await stream.ReadAsync(buffer, offset, bytesToRead, cancellationToken); - if (bytesRead == 0 && count != 0) // count zero is used for checking the connection - throw new Exception("BinaryStream has been closed unexpectedly."); - - // update remaining chunk - _remainingChunkBytes -= bytesRead; - if (_remainingChunkBytes == 0) ReadChunkCount++; - return bytesRead; - } - catch (Exception ex) - { - CloseByError(ex); - throw; - } - } - - private void CloseByError(Exception ex) - { - if (_hasError) return; - _hasError = true; - - VhLogger.LogError(GeneralEventId.TcpLife, ex, "Disposing BinaryStream. StreamId: {StreamId}", StreamId); - _ = DisposeAsync(); - } - - private async Task ReadChunkHeaderAsync(CancellationToken cancellationToken) - { - // read chunk header by cryptor - if (!await StreamUtil.ReadWaitForFillAsync(_streamCryptor, _readChunkHeaderBuffer, 0, _readChunkHeaderBuffer.Length, cancellationToken)) - { - if (!_finished && ReadChunkCount > 0) - VhLogger.Instance.LogWarning(GeneralEventId.TcpLife, "BinaryStream has been closed unexpectedly."); - _isConnectionClosed = true; - return 0; - } - - // get chunk size - var chunkSize = BitConverter.ToInt32(_readChunkHeaderBuffer); - _isCurrentReadingChunkEncrypted = _readChunkHeaderBuffer[4] == 1; - return chunkSize; - - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (_disposed) - throw new ObjectDisposedException(GetType().Name); - - if (_finished) - throw new SocketException((int)SocketError.ConnectionAborted); - - // Create CancellationToken - using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(_writeCts.Token, cancellationToken); - - // should not write a zero chunk if caller data is zero - try - { - _writeTask = count == 0 - ? SourceStream.WriteAsync(buffer, offset, count, tokenSource.Token) - : WriteInternalAsync(buffer, offset, count, tokenSource.Token); - - await _writeTask; - } - catch (Exception ex) - { - CloseByError(ex); - throw; - } - } - - private async Task WriteInternalAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - try - { - var encryptChunk = MaxEncryptChunk > 0; - - if (PreserveWriteBuffer) - { - // create the chunk header and encrypt it - BitConverter.GetBytes(count).CopyTo(buffer, offset - ChunkHeaderLength); - buffer[4] = encryptChunk ? (byte)1 : (byte)0; - _streamCryptor.Encrypt(buffer, 0, ChunkHeaderLength); //header should always be encrypted - - // encrypt chunk - if (encryptChunk) - _streamCryptor.Encrypt(buffer, offset, count); - - await SourceStream.WriteAsync(buffer, offset - ChunkHeaderLength, ChunkHeaderLength + count, cancellationToken); - } - else - { - var size = ChunkHeaderLength + count; - if (size > _writeBuffer.Length) - Array.Resize(ref _writeBuffer, size); - - // create the chunk header and encrypt it - BitConverter.GetBytes(count).CopyTo(_writeBuffer, 0); - _writeBuffer[4] = encryptChunk ? (byte)1 : (byte)0; - _streamCryptor.Encrypt(_writeBuffer, 0, ChunkHeaderLength); //header should always be encrypted - - // add buffer to chunk and encrypt - Buffer.BlockCopy(buffer, offset, _writeBuffer, ChunkHeaderLength, count); - if (encryptChunk) - _streamCryptor.Encrypt(_writeBuffer, ChunkHeaderLength, count); - - // Copy write buffer to output - await SourceStream.WriteAsync(_writeBuffer, 0, size, cancellationToken); - } - - // make sure chunk is sent - await SourceStream.FlushAsync(cancellationToken); - WroteChunkCount++; - if (MaxEncryptChunk > 0) MaxEncryptChunk--; - } - catch (Exception ex) - { - CloseByError(ex); - throw; - } - } - - private bool _leaveOpen; - public override bool CanReuse => !_hasError && !_isConnectionClosed; - public override async Task CreateReuse() - { - if (_disposed) - throw new ObjectDisposedException(GetType().Name); - - if (_hasError) - throw new InvalidOperationException($"Could not reuse a BinaryStream that has error. StreamId: {StreamId}"); - - if (_isConnectionClosed) - throw new InvalidOperationException($"Could not reuse a BinaryStream that its underling stream has been closed . StreamId: {StreamId}"); - - // Dispose the stream but keep the original stream open - _leaveOpen = true; - await DisposeAsync(); - - // reuse if the stream has been closed gracefully - if (_finished && !_hasError) - return new BinaryStreamCustom(SourceStream, StreamId, _streamCryptor, ReusedCount + 1); - - // dispose and throw the ungraceful BinaryStream - await base.DisposeAsync(); - throw new InvalidOperationException($"Could not reuse a BinaryStream that has not been closed gracefully. StreamId: {StreamId}"); - } - - private async Task CloseStream(CancellationToken cancellationToken) - { - // cancel writing requests. - _readCts.CancelAfter(TunnelDefaults.TcpGracefulTimeout); - _writeCts.CancelAfter(TunnelDefaults.TcpGracefulTimeout); - - // wait for finishing current write - try - { - await _writeTask; - } - catch (Exception ex) - { - VhLogger.LogError(GeneralEventId.TcpLife, ex, - "Final stream write has not been completed gracefully. StreamId: {StreamId}", - StreamId); - return; - } - _writeCts.Dispose(); - - // finish writing current BinaryStream gracefully - try - { - if (PreserveWriteBuffer) - await WriteInternalAsync(new byte[ChunkHeaderLength], ChunkHeaderLength, 0, cancellationToken); - else - await WriteInternalAsync([], 0, 0, cancellationToken); - - } - catch (Exception ex) - { - VhLogger.LogError(GeneralEventId.TcpLife, ex, - "Could not write the chunk terminator. StreamId: {StreamId}", - StreamId); - return; - } - - // make sure current caller read has been finished gracefully or wait for cancellation time - try - { - await _readTask; - } - catch (Exception ex) - { - VhLogger.LogError(GeneralEventId.TcpLife, ex, - "Final stream read has not been completed gracefully. StreamId: {StreamId}", - StreamId); - return; - } - - _readCts.Dispose(); - - try - { - if (_hasError) - throw new InvalidOperationException("Could not close a BinaryStream due internal error."); - - if (!_finished) - { - var buffer = new byte[500]; - var trashedLength = 0; - while (true) - { - var read = await ReadInternalAsync(buffer, 0, buffer.Length, cancellationToken); - if (read == 0) - break; - - trashedLength += read; - } - - if (trashedLength > 0) - VhLogger.Instance.LogWarning(GeneralEventId.TcpLife, - "Trashing unexpected binary stream data. StreamId: {StreamId}, TrashedLength: {TrashedLength}", - StreamId, trashedLength); - } - } - catch (Exception ex) - { - VhLogger.LogError(GeneralEventId.TcpLife, ex, - "BinaryStream has not been closed gracefully. StreamId: {StreamId}", - StreamId); - } - } - - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - public override ValueTask DisposeAsync() - { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(); - return _disposeTask.Value; - } - - private async ValueTask DisposeAsyncCore() - { - // prevent other caller requests - _disposed = true; - - // close stream - if (!_hasError && !_isConnectionClosed) - { - // create a new cancellation token for CloseStream - using var cancellationTokenSource = new CancellationTokenSource(TunnelDefaults.TcpGracefulTimeout); - await CloseStream(cancellationTokenSource.Token); - } - - if (!_leaveOpen) - await base.DisposeAsync(); - } -} \ No newline at end of file diff --git a/VpnHood.Tunneling/Channels/Streams/BinaryStreamStandard.cs b/VpnHood.Tunneling/Channels/Streams/BinaryStreamStandard.cs index 724edd0f4..e62ee287d 100644 --- a/VpnHood.Tunneling/Channels/Streams/BinaryStreamStandard.cs +++ b/VpnHood.Tunneling/Channels/Streams/BinaryStreamStandard.cs @@ -9,7 +9,6 @@ public class BinaryStreamStandard : ChunkStream { private const int ChunkHeaderLength = 4; private int _remainingChunkBytes; - private bool _disposed; private bool _finished; private bool _hasError; private readonly CancellationTokenSource _readCts = new(); @@ -280,18 +279,12 @@ private async Task CloseStream(CancellationToken cancellationToken) } } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - public override ValueTask DisposeAsync() - { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(); - return _disposeTask.Value; - } - - private async ValueTask DisposeAsyncCore() + private bool _disposed; + private readonly AsyncLock _disposeLock = new(); + public override async ValueTask DisposeAsync() { - // prevent other caller requests + using var lockResult = await _disposeLock.LockAsync(); + if (_disposed) return; _disposed = true; // close stream diff --git a/VpnHood.Tunneling/Channels/Streams/BinaryStreamType.cs b/VpnHood.Tunneling/Channels/Streams/BinaryStreamType.cs index 69e67226e..38ae617d2 100644 --- a/VpnHood.Tunneling/Channels/Streams/BinaryStreamType.cs +++ b/VpnHood.Tunneling/Channels/Streams/BinaryStreamType.cs @@ -7,6 +7,5 @@ public enum BinaryStreamType { Unknown, None, - Custom, Standard } \ No newline at end of file diff --git a/VpnHood.Tunneling/Channels/Streams/HttpStream.cs b/VpnHood.Tunneling/Channels/Streams/HttpStream.cs index 404dea2ea..44a2d9974 100644 --- a/VpnHood.Tunneling/Channels/Streams/HttpStream.cs +++ b/VpnHood.Tunneling/Channels/Streams/HttpStream.cs @@ -14,7 +14,6 @@ public class HttpStream : ChunkStream private int _remainingChunkBytes; private readonly byte[] _chunkHeaderBuffer = new byte[10]; private readonly byte[] _nextLineBuffer = new byte[2]; - private bool _disposed; private bool _isHttpHeaderSent; private bool _isHttpHeaderRead; private bool _isFinished; @@ -304,18 +303,12 @@ private async Task CloseStream(CancellationToken cancellationToken) } } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - public override ValueTask DisposeAsync() - { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(); - return _disposeTask.Value; - } - - private async ValueTask DisposeAsyncCore() + private bool _disposed; + private readonly AsyncLock _disposeLock = new(); + public override async ValueTask DisposeAsync() { - // prevent other caller requests + using var lockResult = await _disposeLock.LockAsync(); + if (_disposed) return; _disposed = true; // close stream diff --git a/VpnHood.Tunneling/Channels/UdpChannelTransmitter.cs b/VpnHood.Tunneling/Channels/UdpChannelTransmitter.cs index da870809f..a951b4459 100644 --- a/VpnHood.Tunneling/Channels/UdpChannelTransmitter.cs +++ b/VpnHood.Tunneling/Channels/UdpChannelTransmitter.cs @@ -121,11 +121,19 @@ private async Task ReadTask() var channelCryptorPosition = BitConverter.ToInt64(buffer, bufferIndex); bufferIndex += 8; - OnReceiveData(sessionId, udpResult.RemoteEndPoint, channelCryptorPosition, udpResult.Buffer, bufferIndex); + OnReceiveData(sessionId, udpResult.RemoteEndPoint, channelCryptorPosition, udpResult.Buffer, + bufferIndex); } catch (Exception ex) { - // break only for the first call + // finish if disposed + if (_disposed) + { + VhLogger.Instance.LogInformation(GeneralEventId.Essential, "UdpChannelTransmitter has been stopped."); + break; + } + + // break only for the first call and means that the local endpoint can not bind if (remoteEndPoint == null) { VhLogger.LogError(GeneralEventId.Essential, ex, "UdpChannelTransmitter has stopped reading."); diff --git a/VpnHood.Tunneling/ClientStreams/TcpClientStream.cs b/VpnHood.Tunneling/ClientStreams/TcpClientStream.cs index db3cd8ca0..0e833d554 100644 --- a/VpnHood.Tunneling/ClientStreams/TcpClientStream.cs +++ b/VpnHood.Tunneling/ClientStreams/TcpClientStream.cs @@ -13,7 +13,6 @@ public class TcpClientStream : IClientStream private readonly ReuseCallback? _reuseCallback; private string _clientStreamId; - public bool Disposed { get; private set; } public delegate Task ReuseCallback(IClientStream clientStream); public TcpClient TcpClient { get; } public Stream Stream { get; set; } @@ -64,17 +63,11 @@ public ValueTask DisposeAsync() return DisposeAsync(true); } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - public ValueTask DisposeAsync(bool graceful) - { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(graceful); - return _disposeTask.Value; - } - - private async ValueTask DisposeAsyncCore(bool graceful) + private readonly AsyncLock _disposeLock = new(); + public bool Disposed { get; private set; } + public async ValueTask DisposeAsync(bool graceful) { + using var lockResult = await _disposeLock.LockAsync(); if (Disposed) return; Disposed = true; diff --git a/VpnHood.Tunneling/Messaging/HelloRequest.cs b/VpnHood.Tunneling/Messaging/HelloRequest.cs index f7ada5219..6bd012dbf 100644 --- a/VpnHood.Tunneling/Messaging/HelloRequest.cs +++ b/VpnHood.Tunneling/Messaging/HelloRequest.cs @@ -8,6 +8,6 @@ public class HelloRequest() public required string TokenId { get; init; } public required ClientInfo ClientInfo { get; init; } public required byte[] EncryptedClientId { get; init; } - public string? RegionId { get; init; } + public string? ServerLocation { get; init; } // format: countryCode/region/city public bool AllowRedirect { get; init; } = true; } \ No newline at end of file diff --git a/VpnHood.Tunneling/Messaging/HelloResponse.cs b/VpnHood.Tunneling/Messaging/HelloResponse.cs index 464778df4..6fd1952eb 100644 --- a/VpnHood.Tunneling/Messaging/HelloResponse.cs +++ b/VpnHood.Tunneling/Messaging/HelloResponse.cs @@ -31,5 +31,6 @@ public class HelloResponse : SessionResponse public string? AccessKey { get; set; } // todo: deprecated in version 504 or later public bool IsAdRequired { get; set; } - public AdShow AdShow { get; set; } = AdShow.None; + public AdRequirement AdRequirement { get; set; } = AdRequirement.None; + public string? ServerLocation { get; set; } } \ No newline at end of file diff --git a/VpnHood.Tunneling/Messaging/RequestCode.cs b/VpnHood.Tunneling/Messaging/RequestCode.cs index c1cc19d8a..c2395b6ac 100644 --- a/VpnHood.Tunneling/Messaging/RequestCode.cs +++ b/VpnHood.Tunneling/Messaging/RequestCode.cs @@ -6,8 +6,9 @@ public enum RequestCode : byte Hello = 1, TcpDatagramChannel = 2, StreamProxyChannel = 3, - // SessionStatus = 4, + SessionStatus = 4, UdpPacket = 5, AdReward = 10, + ServerStatus = 20, Bye = 50 } \ No newline at end of file diff --git a/VpnHood.Tunneling/Messaging/ServerStatusRequest.cs b/VpnHood.Tunneling/Messaging/ServerStatusRequest.cs new file mode 100644 index 000000000..ae44a810c --- /dev/null +++ b/VpnHood.Tunneling/Messaging/ServerStatusRequest.cs @@ -0,0 +1,9 @@ +using VpnHood.Common.Messaging; + +namespace VpnHood.Tunneling.Messaging; + +public class ServerStatusRequest() + : ClientRequest((byte)Messaging.RequestCode.ServerStatus) +{ + public string? Message { get; init; } +} \ No newline at end of file diff --git a/VpnHood.Tunneling/Messaging/ServerStatusResponse.cs b/VpnHood.Tunneling/Messaging/ServerStatusResponse.cs new file mode 100644 index 000000000..8194a8e3d --- /dev/null +++ b/VpnHood.Tunneling/Messaging/ServerStatusResponse.cs @@ -0,0 +1,8 @@ +using VpnHood.Common.Messaging; + +namespace VpnHood.Tunneling.Messaging; + +public class ServerStatusResponse : SessionResponse +{ + public string? Message { get; init; } +} \ No newline at end of file diff --git a/VpnHood.Tunneling/Messaging/SessionStatusRequest.cs b/VpnHood.Tunneling/Messaging/SessionStatusRequest.cs new file mode 100644 index 000000000..18878c54b --- /dev/null +++ b/VpnHood.Tunneling/Messaging/SessionStatusRequest.cs @@ -0,0 +1,4 @@ +namespace VpnHood.Tunneling.Messaging; + +public class SessionStatusRequest() + : RequestBase(Messaging.RequestCode.SessionStatus); \ No newline at end of file diff --git a/VpnHood.Tunneling/PacketUtil.cs b/VpnHood.Tunneling/PacketUtil.cs index 5f26f5f16..b20dfc0cb 100644 --- a/VpnHood.Tunneling/PacketUtil.cs +++ b/VpnHood.Tunneling/PacketUtil.cs @@ -72,7 +72,8 @@ public static IPPacket CreateTcpResetReply(IPPacket ipPacket, bool updatePacket var tcpPacketOrg = ExtractTcp(ipPacket); TcpPacket resetTcpPacket = new(tcpPacketOrg.DestinationPort, tcpPacketOrg.SourcePort) { - Reset = true + Reset = true, + WindowSize = 0 }; if (tcpPacketOrg is { Synchronize: true, Acknowledgment: false }) diff --git a/VpnHood.Tunneling/Tunnel.cs b/VpnHood.Tunneling/Tunnel.cs index e7e1959c4..8f564c1b7 100644 --- a/VpnHood.Tunneling/Tunnel.cs +++ b/VpnHood.Tunneling/Tunnel.cs @@ -21,7 +21,6 @@ public class Tunnel : IJob, IAsyncDisposable private readonly HashSet _streamProxyChannels = []; private readonly List _datagramChannels = []; private readonly Timer _speedMonitorTimer; - private bool _disposed; private int _maxDatagramChannelCount; private Traffic _lastTraffic = new(); private readonly Traffic _trafficUsage = new(); @@ -404,17 +403,11 @@ public Task RunJob() } - private readonly object _disposeLock = new(); - private ValueTask? _disposeTask; - public ValueTask DisposeAsync() - { - lock (_disposeLock) - _disposeTask ??= DisposeAsyncCore(); - return _disposeTask.Value; - } - - private async ValueTask DisposeAsyncCore() + private readonly AsyncLock _disposeLock = new(); + private bool _disposed; + public async ValueTask DisposeAsync() { + using var lockResult = await _disposeLock.LockAsync(); if (_disposed) return; _disposed = true; diff --git a/VpnHood.Tunneling/VpnHood.Tunneling.csproj b/VpnHood.Tunneling/VpnHood.Tunneling.csproj index a96d31c65..37e66f91a 100644 --- a/VpnHood.Tunneling/VpnHood.Tunneling.csproj +++ b/VpnHood.Tunneling/VpnHood.Tunneling.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.4.506 + 4.5.520 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm"))