diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea817880..e1818ef7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# v4.6.544 +### Client +* Fix: App Filter does not work when no app is selected +* Fix: Exclude the ad tracker from the tunnel +* Fix: Exclude My Country +* Fix: Could not set MTU error +* Fix: Android: InApp Update +* Fix: Android: VPN Service remains in memory after disconnect +* Feature: Report unreachable servers +* Feature: Add domain filtering to engine +* Feature: Add log to Android logcat +* Feature: Try to find a reachable server among endpoints +* Feature: Implement ChartBoost ads +* Update: Improve performance and memory usage +* +### Server +* Feature: Support multiple redirect endpoints +* Update: Use Cloudflare for detecting the server's public IP +* Update: Improve performance and memory usage + # v4.5.535 ### Client * Improve: App Filter page diff --git a/Pub/PubVersion.json b/Pub/PubVersion.json index 53584a337..c07e0408a 100644 --- a/Pub/PubVersion.json +++ b/Pub/PubVersion.json @@ -1,6 +1,6 @@ { - "Version": "4.5.535", - "BumpTime": "2024-06-24T05:03:47.2884293Z", + "Version": "4.6.544", + "BumpTime": "2024-07-15T05:08:21.3508028Z", "Prerelease": false, "DeprecatedVersion": "4.0.00" } diff --git a/Tests/VpnHood.Test/AccessManagers/TestAccessManager.cs b/Tests/VpnHood.Test/AccessManagers/TestAccessManager.cs index ac419380d..0703f5980 100644 --- a/Tests/VpnHood.Test/AccessManagers/TestAccessManager.cs +++ b/Tests/VpnHood.Test/AccessManagers/TestAccessManager.cs @@ -19,6 +19,7 @@ public class TestAccessManager(string storagePath, FileAccessManagerOptions opti public ServerInfo? LastServerInfo { get; private set; } public ServerStatus? LastServerStatus { get; private set; } public IPEndPoint? RedirectHostEndPoint { get; set; } + public IPEndPoint[]? RedirectHostEndPoints { get; set; } public Dictionary ServerLocations { get; set; } = new(); public bool RejectAllAds { get; set; } @@ -69,6 +70,13 @@ public override async Task Session_Create(SessionRequestEx se ret.ErrorCode = SessionErrorCode.RedirectHost; } + // manage new redirects + if (RedirectHostEndPoints != null) + { + ret.RedirectHostEndPoints = RedirectHostEndPoints; + ret.ErrorCode = SessionErrorCode.RedirectHost; + } + // manage region if (sessionRequestEx.ServerLocation != null) { diff --git a/Tests/VpnHood.Test/Device/TestDevice.cs b/Tests/VpnHood.Test/Device/TestDevice.cs index da89479bc..8d5203f95 100644 --- a/Tests/VpnHood.Test/Device/TestDevice.cs +++ b/Tests/VpnHood.Test/Device/TestDevice.cs @@ -3,10 +3,8 @@ namespace VpnHood.Test.Device; -internal class TestDevice(TestDeviceOptions? options = default) : IDevice +internal class TestDevice(TestDeviceOptions options, bool useNullPacketCapture) : IDevice { - private readonly TestDeviceOptions _options = options ?? new TestDeviceOptions(); - #pragma warning disable CS0067 // The event 'TestDevice.StartedAsService' is never used public event EventHandler? StartedAsService; #pragma warning restore CS0067 // The event 'TestDevice.StartedAsService' is never used @@ -14,15 +12,17 @@ internal class TestDevice(TestDeviceOptions? options = default) : IDevice public string OsInfo => Environment.OSVersion + ", " + (Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit"); public bool IsExcludeAppsSupported => false; public bool IsIncludeAppsSupported => false; - public bool IsLogToConsoleSupported => true; public bool IsAlwaysOnSupported => false; public DeviceAppInfo[] InstalledApps => throw new NotSupportedException(); public Task CreatePacketCapture(IUiContext? uiContext) { - var res = new TestPacketCapture(_options); - return Task.FromResult((IPacketCapture)res); + IPacketCapture res = useNullPacketCapture + ? new NullPacketCapture() + : new TestPacketCapture(options); + + return Task.FromResult(res); } public void Dispose() { diff --git a/Tests/VpnHood.Test/Device/TestPacketCapture.cs b/Tests/VpnHood.Test/Device/TestPacketCapture.cs index 2f484ae18..06c2abb21 100644 --- a/Tests/VpnHood.Test/Device/TestPacketCapture.cs +++ b/Tests/VpnHood.Test/Device/TestPacketCapture.cs @@ -1,8 +1,6 @@ using System.Net; -using System.Net.Sockets; using PacketDotNet; using VpnHood.Client.Device.WinDivert; -using VpnHood.Test.Services; namespace VpnHood.Test.Device; @@ -25,7 +23,6 @@ public override IPAddress[]? DnsServers } public override bool CanSendPacketToOutbound => deviceOptions.CanSendPacketToOutbound; - public override bool CanProtectSocket => !deviceOptions.CanSendPacketToOutbound; protected override void ProcessPacketReceivedFromInbound(IPPacket ipPacket) { @@ -36,20 +33,10 @@ protected override void ProcessPacketReceivedFromInbound(IPPacket ipPacket) deviceOptions.CaptureDnsAddresses != null && deviceOptions.CaptureDnsAddresses.All(x => !x.Equals(ipPacket.DestinationAddress)); - ignore |= TestSocketProtector.IsProtectedPacket(ipPacket); - // ignore protected packets if (ignore) SendPacketToOutbound(ipPacket); else base.ProcessPacketReceivedFromInbound(ipPacket); } - - public override void ProtectSocket(Socket socket) - { - if (CanProtectSocket) - TestSocketProtector.ProtectSocket(socket); - else - base.ProtectSocket(socket); - } } \ No newline at end of file diff --git a/Tests/VpnHood.Test/Services/TestAdService.cs b/Tests/VpnHood.Test/Services/TestAdService.cs index 2f9173071..3146a2ea8 100644 --- a/Tests/VpnHood.Test/Services/TestAdService.cs +++ b/Tests/VpnHood.Test/Services/TestAdService.cs @@ -13,6 +13,7 @@ public class TestAdService(TestAccessManager accessManager) : IAppAdService public AppAdType AdType => AppAdType.InterstitialAd; public bool IsCountrySupported(string countryCode) => true; public DateTime? AdLoadedTime { get; private set; } + public TimeSpan AdLifeSpan { get; } = TimeSpan.FromMinutes(60); public Task LoadAd(IUiContext uiContext, CancellationToken cancellationToken) { diff --git a/Tests/VpnHood.Test/Services/TestNetFilter.cs b/Tests/VpnHood.Test/Services/TestNetFilter.cs index bd7628db2..c43ed796f 100644 --- a/Tests/VpnHood.Test/Services/TestNetFilter.cs +++ b/Tests/VpnHood.Test/Services/TestNetFilter.cs @@ -3,7 +3,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using PacketDotNet; using VpnHood.Server; -using VpnHood.Tunneling; +using VpnHood.Tunneling.Utils; namespace VpnHood.Test.Services; diff --git a/Tests/VpnHood.Test/Services/TestSocketFactory.cs b/Tests/VpnHood.Test/Services/TestSocketFactory.cs index f55457c3f..27949a15c 100644 --- a/Tests/VpnHood.Test/Services/TestSocketFactory.cs +++ b/Tests/VpnHood.Test/Services/TestSocketFactory.cs @@ -1,4 +1,5 @@ using System.Net.Sockets; +using VpnHood.Client.Device.WinDivert; using VpnHood.Tunneling.Factory; namespace VpnHood.Test.Services; @@ -8,14 +9,14 @@ public class TestSocketFactory : SocketFactory public override TcpClient CreateTcpClient(AddressFamily addressFamily) { var tcpClient = base.CreateTcpClient(addressFamily); - TestSocketProtector.ProtectSocket(tcpClient.Client); + tcpClient.Client.Ttl = WinDivertPacketCapture.ProtectedTtl; return tcpClient; } public override UdpClient CreateUdpClient(AddressFamily addressFamily) { var udpClient = base.CreateUdpClient(addressFamily); - TestSocketProtector.ProtectSocket(udpClient.Client); + udpClient.Client.Ttl = WinDivertPacketCapture.ProtectedTtl; return udpClient; } } \ No newline at end of file diff --git a/Tests/VpnHood.Test/Services/TestSocketProtector.cs b/Tests/VpnHood.Test/Services/TestSocketProtector.cs deleted file mode 100644 index 444cd90be..000000000 --- a/Tests/VpnHood.Test/Services/TestSocketProtector.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Net.Sockets; -using PacketDotNet; -using VpnHood.Tunneling; -using ProtocolType = PacketDotNet.ProtocolType; - -namespace VpnHood.Test.Services; - -internal static class TestSocketProtector -{ - private const int ProtectedTtl = 111; - private static readonly HashSet TcpProtected = []; - - public static void ProtectSocket(Socket socket) - { - socket.Ttl = ProtectedTtl; - } - - public static bool IsProtectedPacket(IPPacket ipPacket) - { - if (ipPacket.Protocol == ProtocolType.Tcp) - { - lock (TcpProtected) - { - var tcpPacket = PacketUtil.ExtractTcp(ipPacket); - if (tcpPacket.Synchronize) - { - if (ipPacket.TimeToLive == ProtectedTtl) - TcpProtected.Add(tcpPacket.SourcePort); - else - TcpProtected.Remove(tcpPacket.SourcePort); - } - - return TcpProtected.Contains(tcpPacket.SourcePort); - } - } - - return ipPacket.TimeToLive == ProtectedTtl; - } -} \ No newline at end of file diff --git a/Tests/VpnHood.Test/Services/TestTracker.cs b/Tests/VpnHood.Test/Services/TestTracker.cs new file mode 100644 index 000000000..53eda5ae0 --- /dev/null +++ b/Tests/VpnHood.Test/Services/TestTracker.cs @@ -0,0 +1,27 @@ +using Ga4.Trackers; + +namespace VpnHood.Test.Services; + +internal class TestTracker : ITracker +{ + public bool IsEnabled { get; set; } + public List TrackEvents { get; } = []; + + public Task Track(IEnumerable trackEvents) + { + return Task.WhenAll(trackEvents.Select(Track)); + } + + public Task Track(TrackEvent trackEvent) + { + TrackEvents.Add(trackEvent); + return Task.CompletedTask; + } + + public TrackEvent? FindEvent(string eventName, string parameterName, object parameterValue) + { + return TrackEvents.FirstOrDefault(e => + e.EventName == eventName && + e.Parameters.ContainsKey(parameterName) && e.Parameters[parameterName].Equals(parameterValue)); + } +} \ No newline at end of file diff --git a/Tests/VpnHood.Test/TestHelper.cs b/Tests/VpnHood.Test/TestHelper.cs index c66a176a8..63fd482d2 100644 --- a/Tests/VpnHood.Test/TestHelper.cs +++ b/Tests/VpnHood.Test/TestHelper.cs @@ -269,26 +269,26 @@ public static FileAccessManagerOptions CreateFileAccessManagerOptions(IPEndPoint } public static Task CreateServer( - IAccessManager? accessManager = null, + IAccessManager? accessManager = null, bool autoStart = true, TimeSpan? configureInterval = null, bool useHttpAccessManager = true) { - return CreateServer(accessManager, null, - autoStart: autoStart, - configureInterval: configureInterval, + return CreateServer(accessManager, null, + autoStart: autoStart, + configureInterval: configureInterval, useHttpAccessManager: useHttpAccessManager); } - public static Task CreateServer(FileAccessManagerOptions? options, bool autoStart = true, + public static Task CreateServer(FileAccessManagerOptions? options, bool autoStart = true, TimeSpan? configureInterval = null, bool useHttpAccessManager = true) { - return CreateServer(null, options, - autoStart: autoStart, - configureInterval: configureInterval, + return CreateServer(null, options, + autoStart: autoStart, + configureInterval: configureInterval, useHttpAccessManager: useHttpAccessManager); } - private static async Task CreateServer(IAccessManager? accessManager, FileAccessManagerOptions? fileAccessManagerOptions, - bool autoStart, TimeSpan? configureInterval = null, bool useHttpAccessManager = true) + private static async Task CreateServer(IAccessManager? accessManager, FileAccessManagerOptions? fileAccessManagerOptions, + bool autoStart, TimeSpan? configureInterval = null, bool useHttpAccessManager = true) { if (accessManager != null && fileAccessManagerOptions != null) throw new InvalidOperationException($"Could not set both {nameof(accessManager)} and {nameof(fileAccessManagerOptions)}."); @@ -329,11 +329,23 @@ private static async Task CreateServer(IAccessManager? accessMana return server; } + public static TestDeviceOptions CreateDeviceOptions() + { + return new TestDeviceOptions(); + } + public static IDevice CreateDevice(TestDeviceOptions? options = default) { - return new TestDevice(options); + options ??= new TestDeviceOptions(); + return new TestDevice(options, false); + } + + public static IDevice CreateNullDevice() + { + return new TestDevice(new TestDeviceOptions(), true); } + public static IPacketCapture CreatePacketCapture(TestDeviceOptions? options = default) { return CreateDevice(options).CreatePacketCapture(null).Result; @@ -343,8 +355,11 @@ public static ClientOptions CreateClientOptions(bool useUdp = false) { return new ClientOptions { + AllowAnonymousTracker = true, + AllowEndPointTracker = true, MaxDatagramChannelCount = 1, - UseUdpChannel = useUdp + UseUdpChannel = useUdp, + Tracker = new TestTracker() }; } @@ -384,15 +399,27 @@ public static async Task CreateClient(Token token, return client; } - public static AppOptions CreateClientAppOptions() + public static AppOptions CreateAppOptions() { + var tracker = new TestTracker(); var appOptions = new AppOptions { StorageFolderPath = Path.Combine(WorkingPath, "AppData_" + Guid.NewGuid()), SessionTimeout = TimeSpan.FromSeconds(2), + AppGa4MeasurementId = null, + Tracker = tracker, UseInternalLocationService = false, UseExternalLocationService = false, - LogVerbose = LogVerbose + AllowEndPointTracker = true, + LogVerbose = LogVerbose, + ServerQueryTimeout = TimeSpan.FromSeconds(2), + AutoDiagnose = false, + SingleLineConsoleLog = false, + AdOptions = new AppAdOptions + { + ShowAdPostDelay = TimeSpan.Zero, + LoadAdPostDelay = TimeSpan.Zero + } }; return appOptions; } @@ -400,16 +427,16 @@ public static AppOptions CreateClientAppOptions() public static VpnHoodApp CreateClientApp(TestDeviceOptions? deviceOptions = default, AppOptions? appOptions = default) { //create app - appOptions ??= CreateClientAppOptions(); + appOptions ??= CreateAppOptions(); - var device = CreateDevice(deviceOptions); + var device = deviceOptions != null ? CreateDevice(deviceOptions) : CreateNullDevice(); var clientApp = VpnHoodApp.Init(device, appOptions); clientApp.Diagnoser.HttpTimeout = 2000; clientApp.Diagnoser.NsTimeout = 2000; clientApp.UserSettings.PacketCaptureIncludeIpRanges = TestIpAddresses.Select(x => new IpRange(x)).ToArray(); clientApp.UserSettings.Logging.LogAnonymous = false; clientApp.TcpTimeout = TimeSpan.FromSeconds(2); - clientApp.UiContext = new TestAppUiContext(); + ActiveUiContext.Context = new TestAppUiContext(); return clientApp; } diff --git a/Tests/VpnHood.Test/Tests/AdTest.cs b/Tests/VpnHood.Test/Tests/AdTest.cs index 93dbff847..9b080f23a 100644 --- a/Tests/VpnHood.Test/Tests/AdTest.cs +++ b/Tests/VpnHood.Test/Tests/AdTest.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using VpnHood.Client.App; using VpnHood.Client.App.Exceptions; +using VpnHood.Client.Device; using VpnHood.Common.Messaging; using VpnHood.Common.Utils; using VpnHood.Test.Services; @@ -22,7 +23,7 @@ public async Task flexible_ad_should_not_close_session_if_load_ad_failed() accessItem.Token.ToAccessKey(); // create client app - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); var adService = new TestAdService(accessManager); appOptions.AdServices = [adService]; await using var app = TestHelper.CreateClientApp(appOptions: appOptions); @@ -46,11 +47,11 @@ public async Task flexible_ad_should_close_session_if_display_ad_failed() accessItem.Token.ToAccessKey(); // create client app - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); var adService = new TestAdService(accessManager); appOptions.AdServices = [adService]; await using var app = TestHelper.CreateClientApp(appOptions: appOptions); - app.UiContext = null; + ActiveUiContext.Context = null; //adService.FailShow = true; // connect @@ -71,11 +72,11 @@ public async Task Session_must_be_closed_after_few_minutes_if_ad_is_not_accepted accessItem.Token.ToAccessKey(); // create client app - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); var adService = new TestAdService(accessManager); appOptions.AdServices = [adService]; await using var app = TestHelper.CreateClientApp(appOptions: appOptions); - app.UiContext = null; + ActiveUiContext.Context = null; //adService.FailShow = true; // connect @@ -96,7 +97,7 @@ public async Task Session_expiration_must_increase_by_ad() accessItem.Token.ToAccessKey(); // create client app - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.AdServices = [new TestAdService(accessManager)]; await using var app = TestHelper.CreateClientApp(appOptions: appOptions); @@ -121,7 +122,7 @@ public async Task Session_exception_should_be_short_if_ad_is_not_accepted() testManager.RejectAllAds = true; // server will reject all ads // create client app - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.AdServices = [new TestAdService(testManager)]; await using var app = TestHelper.CreateClientApp(appOptions: appOptions); diff --git a/Tests/VpnHood.Test/Tests/CheckNewVersionTest.cs b/Tests/VpnHood.Test/Tests/CheckNewVersionTest.cs index d59b503fe..fe9f18600 100644 --- a/Tests/VpnHood.Test/Tests/CheckNewVersionTest.cs +++ b/Tests/VpnHood.Test/Tests/CheckNewVersionTest.cs @@ -33,7 +33,7 @@ private static void SetNewRelease(Version version, DateTime releaseDate, TimeSpa [TestMethod] public async Task Remote_is_unknown_if_remote_is_unreachable() { - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.UpdateInfoUrl = new Uri("https://localhost:39999"); await using var app = TestHelper.CreateClientApp(appOptions: appOptions); @@ -47,7 +47,7 @@ public async Task Current_is_latest() { SetNewRelease(CurrentAppVersion, DateTime.UtcNow, TimeSpan.Zero); - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.UpdateInfoUrl = TestHelper.WebServer.FileHttpUrl1; appOptions.VersionCheckInterval = TimeSpan.FromMilliseconds(500); await using var app = TestHelper.CreateClientApp(appOptions: appOptions); @@ -61,7 +61,7 @@ public async Task Current_is_deprecated() SetNewRelease(new Version(CurrentAppVersion.Major, CurrentAppVersion.Minor, CurrentAppVersion.Build + 1), DateTime.UtcNow, TimeSpan.Zero, CurrentAppVersion); - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.UpdateInfoUrl = TestHelper.WebServer.FileHttpUrl1; appOptions.VersionCheckInterval = TimeSpan.FromMilliseconds(500); await using var app = TestHelper.CreateClientApp(appOptions: appOptions); @@ -75,7 +75,7 @@ public async Task Current_is_old_by_job() SetNewRelease(CurrentAppVersion, DateTime.UtcNow, TimeSpan.Zero); // create client - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.UpdateInfoUrl = TestHelper.WebServer.FileHttpUrl1; appOptions.VersionCheckInterval = TimeSpan.FromMilliseconds(500); await using var app = TestHelper.CreateClientApp(appOptions: appOptions); @@ -95,7 +95,7 @@ public async Task Current_is_old_but_wait_for_notification_delay() SetNewRelease(new Version(CurrentAppVersion.Major, CurrentAppVersion.Minor, CurrentAppVersion.Build + 1), DateTime.UtcNow, TimeSpan.FromSeconds(2)); - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.UpdateInfoUrl = TestHelper.WebServer.FileHttpUrl1; appOptions.VersionCheckInterval = TimeSpan.FromMilliseconds(500); await using var app = TestHelper.CreateClientApp(appOptions: appOptions); @@ -110,7 +110,7 @@ public async Task Current_is_old_before_connect_to_vpn() SetNewRelease(new Version(CurrentAppVersion.Major, CurrentAppVersion.Minor, CurrentAppVersion.Build + 1), DateTime.UtcNow, TimeSpan.Zero); - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.UpdateInfoUrl = TestHelper.WebServer.FileHttpUrl1; appOptions.VersionCheckInterval = TimeSpan.FromMilliseconds(500); await using var app = TestHelper.CreateClientApp(appOptions: appOptions); @@ -129,7 +129,7 @@ public async Task Current_is_old_after_connect_to_vpn() TestHelper.WebServer.FileContent1 = Guid.NewGuid().ToString(); // create client app - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.UpdateInfoUrl = TestHelper.WebServer.FileHttpUrl1; await using var app = TestHelper.CreateClientApp(appOptions: appOptions); var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); diff --git a/Tests/VpnHood.Test/Tests/ClientAppTest.cs b/Tests/VpnHood.Test/Tests/ClientAppTest.cs index ced09089c..d18c41cee 100644 --- a/Tests/VpnHood.Test/Tests/ClientAppTest.cs +++ b/Tests/VpnHood.Test/Tests/ClientAppTest.cs @@ -15,6 +15,7 @@ using VpnHood.Common.Net; using VpnHood.Common.Utils; using VpnHood.Test.Device; +// ReSharper disable DisposeOnUsingVariable namespace VpnHood.Test.Tests; @@ -51,7 +52,7 @@ private Token CreateToken() [TestMethod] public async Task BuiltIn_AccessKeys_initialization() { - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); var tokens = new[] { CreateToken(), CreateToken() }; appOptions.AccessKeys = tokens.Select(x => x.ToAccessKey()).ToArray(); @@ -60,7 +61,8 @@ public async Task BuiltIn_AccessKeys_initialization() 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) @@ -76,7 +78,7 @@ public async Task BuiltIn_AccessKeys_initialization() [TestMethod] public async Task BuiltIn_AccessKeys_RemoveOldKeys() { - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); var tokens1 = new[] { CreateToken(), CreateToken() }; appOptions.AccessKeys = tokens1.Select(x => x.ToAccessKey()).ToArray(); @@ -101,7 +103,7 @@ private static async Task UpdateIp2LocationFile() // update current ipLocation in app project after a week var solutionFolder = TestHelper.GetParentDirectory(Directory.GetCurrentDirectory(), 5); var ipLocationFile = Path.Combine(solutionFolder, "VpnHood.Client.App", "Resources", "IpLocations.zip"); - if (File.GetCreationTime(ipLocationFile) <= DateTime.Now - TimeSpan.FromDays(7)) + if (File.GetCreationTime(ipLocationFile) > DateTime.Now - TimeSpan.FromDays(7)) return; // find token @@ -112,8 +114,8 @@ private static async Task UpdateIp2LocationFile() // copy zip to memory var httpClient = new HttpClient(); // ReSharper disable once StringLiteralTypo - await using var ipLocationZipNetStream = await httpClient.GetStreamAsync( - $"https://www.ip2location.com/download/?token={ip2LocationToken}&file=DB1LITECSVIPV6"); + var url = $"https://www.ip2location.com/download/?token={ip2LocationToken}&file=DB1LITECSVIPV6"; + await using var ipLocationZipNetStream = await httpClient.GetStreamAsync(url); using var ipLocationZipStream = new MemoryStream(); await ipLocationZipNetStream.CopyToAsync(ipLocationZipStream); ipLocationZipStream.Position = 0; @@ -129,13 +131,16 @@ public async Task IpLocations_must_be_loaded() { await UpdateIp2LocationFile(); - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.UseInternalLocationService = true; await using var app = TestHelper.CreateClientApp(appOptions: appOptions); var ipGroupsManager = await app.GetIpGroupManager(); var ipGroupIds = await ipGroupsManager.GetIpGroupIds(); Assert.IsTrue(ipGroupIds.Any(x => x == "us"), "Countries has not been extracted."); + + // make sure GetIpRange works + Assert.IsTrue((await ipGroupsManager.GetIpRanges("US")).Any()); } [TestMethod] @@ -230,7 +235,8 @@ public async Task ClientProfiles_CRUD() 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"); + Assert.AreEqual(token1.TokenId, clientProfile1.Token.TokenId, + "invalid tokenId has been assigned to clientProfile"); // ************ // *** TEST ***: AddAccessKey with new accessKey should add another clientProfile @@ -271,7 +277,8 @@ public async Task ClientProfiles_CRUD() // ************ // *** TEST ***: RemoveClientProfile app.ClientProfileService.Remove(clientProfile1.ClientProfileId); - Assert.IsNull(app.ClientProfileService.FindById(clientProfile1.ClientProfileId), "ClientProfile has not been removed!"); + Assert.IsNull(app.ClientProfileService.FindById(clientProfile1.ClientProfileId), + "ClientProfile has not been removed!"); } [TestMethod] @@ -288,11 +295,12 @@ public async Task Save_load_clientProfiles() var clientProfiles = app1.ClientProfileService.List(); await app1.DisposeAsync(); - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); 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!"); + Assert.AreEqual(clientProfiles.Length, app2.ClientProfileService.List().Length, + "ClientProfiles count are not same!"); Assert.IsNotNull(app2.ClientProfileService.FindById(clientProfile1.ClientProfileId)); Assert.IsNotNull(app2.ClientProfileService.FindById(clientProfile2.ClientProfileId)); Assert.IsNotNull(app2.ClientProfileService.GetToken(token1.TokenId)); @@ -374,11 +382,11 @@ public async Task State_Waiting() await using var server1 = await TestHelper.CreateServer(accessManager); // create app & connect - var appOptions = TestHelper.CreateClientAppOptions(); + var appOptions = TestHelper.CreateAppOptions(); appOptions.SessionTimeout = TimeSpan.FromSeconds(20); appOptions.ReconnectTimeout = TimeSpan.FromSeconds(1); appOptions.AutoWaitTimeout = TimeSpan.FromSeconds(2); - await using var app = TestHelper.CreateClientApp(appOptions: appOptions); + await using var app = TestHelper.CreateClientApp(appOptions: appOptions, deviceOptions: TestHelper.CreateDeviceOptions()); var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); await app.Connect(clientProfile.ClientProfileId); await TestHelper.WaitForAppState(app, AppConnectionState.Connected); @@ -408,7 +416,7 @@ public async Task Set_DnsServer_to_packetCapture() var token = TestHelper.CreateAccessToken(server); // create app - using var packetCapture = TestHelper.CreatePacketCapture(new TestDeviceOptions { IsDnsServerSupported = true }); + using var packetCapture = new TestNullPacketCapture(); Assert.IsTrue(packetCapture.DnsServers == null || packetCapture.DnsServers.Length == 0); await using var client = await TestHelper.CreateClient(token, packetCapture); @@ -568,6 +576,7 @@ public static async Task IpFilters_TestExclude(VpnHoodApp app, bool testUdp, boo { Assert.AreEqual(nameof(PingException), ex.GetType().Name); } + Assert.AreEqual(oldReceivedByteCount, app.State.SessionTraffic.Received); // ping @@ -590,6 +599,7 @@ public static async Task IpFilters_TestExclude(VpnHoodApp app, bool testUdp, boo { Assert.AreEqual(nameof(OperationCanceledException), ex.GetType().Name); } + Assert.AreEqual(oldReceivedByteCount, app.State.SessionTraffic.Received); // UDP @@ -650,7 +660,7 @@ public async Task update_server_token_url_from_server() // Update ServerTokenUrl after token creation const string newTokenUrl = "http://127.0.0.100:6000"; accessManager.ServerConfig.ServerTokenUrl = newTokenUrl; - accessManager.ServerConfig.ServerSecret = VhUtil.GenerateKey(); + accessManager.ServerConfig.ServerSecret = VhUtil.GenerateKey(); // It can not be changed in new version accessManager.ClearCache(); // create server and app @@ -662,8 +672,10 @@ public async Task update_server_token_url_from_server() await app.Connect(clientProfile1.ClientProfileId); await TestHelper.WaitForAppState(app, AppConnectionState.Connected); - Assert.AreEqual(accessManager.ServerConfig.ServerTokenUrl, app.ClientProfileService.GetToken(token.TokenId).ServerToken.Url); - CollectionAssert.AreEqual(accessManager.ServerConfig.ServerSecret, app.ClientProfileService.GetToken(token.TokenId).ServerToken.Secret); + Assert.AreEqual(accessManager.ServerConfig.ServerTokenUrl, + app.ClientProfileService.GetToken(token.TokenId).ServerToken.Url); + CollectionAssert.AreEqual(accessManager.ServerConfig.ServerSecret, + app.ClientProfileService.GetToken(token.TokenId).ServerToken.Secret); } [TestMethod] @@ -686,7 +698,8 @@ public async Task update_server_token_from_server_token_url() // create server 2 await Task.Delay(1100); // wait for new CreatedTime fileAccessManagerOptions1.TcpEndPoints = [VhUtil.GetFreeTcpEndPoint(IPAddress.Loopback, tcpEndPoint.Port + 1)]; - var accessManager2 = TestHelper.CreateAccessManager(storagePath: accessManager1.StoragePath, options: fileAccessManagerOptions1); + var accessManager2 = TestHelper.CreateAccessManager(storagePath: accessManager1.StoragePath, + options: fileAccessManagerOptions1); await using var server2 = await TestHelper.CreateServer(accessManager2); var token2 = TestHelper.CreateAccessToken(server2); @@ -706,7 +719,8 @@ public async Task update_server_token_from_server_token_url() Assert.IsTrue(isTokenRetrieved); Assert.AreNotEqual(token1.ServerToken.CreatedTime, token2.ServerToken.CreatedTime); - Assert.AreEqual(token2.ServerToken.CreatedTime, app.ClientProfileService.GetToken(token1.TokenId).ServerToken.CreatedTime); + Assert.AreEqual(token2.ServerToken.CreatedTime, + app.ClientProfileService.GetToken(token1.TokenId).ServerToken.CreatedTime); Assert.AreEqual(AppConnectionState.Connected, app.State.ConnectionState); } @@ -733,4 +747,60 @@ public async Task Change_server_while_connected() Assert.AreEqual(AppConnectionState.Connected, app.State.ConnectionState, "Client connection has not been changed!"); } + + [TestMethod] + public async Task IncludeDomains() + { + // Create Server + await using var server = await TestHelper.CreateServer(); + + // create app + var appOptions = TestHelper.CreateAppOptions(); + await using var app = TestHelper.CreateClientApp(appOptions: appOptions, deviceOptions: TestHelper.CreateDeviceOptions()); + app.UserSettings.DomainFilter.Includes = [TestConstants.HttpsUri1.Host]; + + // connect + var token = TestHelper.CreateAccessToken(server); + var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + await app.Connect(clientProfile.ClientProfileId, diagnose: true); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); + + // text include + var oldReceivedByteCount = app.State.SessionTraffic.Received; + await TestHelper.Test_Https(uri: TestConstants.HttpsUri1); + Assert.AreNotEqual(oldReceivedByteCount, app.State.SessionTraffic.Received); + + // text exclude + oldReceivedByteCount = app.State.SessionTraffic.Received; + await TestHelper.Test_Https(uri: TestConstants.HttpsUri2); + Assert.AreEqual(oldReceivedByteCount, app.State.SessionTraffic.Received); + } + + [TestMethod] + public async Task ExcludeDomains() + { + // Create Server + await using var server = await TestHelper.CreateServer(); + + // create app + var appOptions = TestHelper.CreateAppOptions(); + await using var app = TestHelper.CreateClientApp(appOptions: appOptions, deviceOptions: TestHelper.CreateDeviceOptions()); + app.UserSettings.DomainFilter.Excludes = [TestConstants.HttpsUri1.Host]; + + // connect + var token = TestHelper.CreateAccessToken(server); + var clientProfile = app.ClientProfileService.ImportAccessKey(token.ToAccessKey()); + await app.Connect(clientProfile.ClientProfileId, diagnose: true); + await TestHelper.WaitForAppState(app, AppConnectionState.Connected); + + // text include + var oldReceivedByteCount = app.State.SessionTraffic.Received; + await TestHelper.Test_Https(uri: TestConstants.HttpsUri2); + Assert.AreNotEqual(oldReceivedByteCount, app.State.SessionTraffic.Received); + + // text exclude + oldReceivedByteCount = app.State.SessionTraffic.Received; + await TestHelper.Test_Https(uri: TestConstants.HttpsUri1); + Assert.AreEqual(oldReceivedByteCount, app.State.SessionTraffic.Received); + } } \ No newline at end of file diff --git a/Tests/VpnHood.Test/Tests/ClientServerTest.cs b/Tests/VpnHood.Test/Tests/ClientServerTest.cs index b4e078e09..08364a2de 100644 --- a/Tests/VpnHood.Test/Tests/ClientServerTest.cs +++ b/Tests/VpnHood.Test/Tests/ClientServerTest.cs @@ -41,7 +41,7 @@ public async Task Redirect_Server() // Create Client var token1 = TestHelper.CreateAccessToken(accessManager1); - await using var client = await TestHelper.CreateClient(token1); + await using var client = await TestHelper.CreateClient(token1, deviceOptions: TestHelper.CreateDeviceOptions()); await TestHelper.Test_Https(); Assert.AreEqual(serverEndPoint2, client.HostTcpEndPoint); @@ -83,14 +83,13 @@ public async Task Client_must_update_ServerLocation_from_access_manager() var fileAccessManagerOptions1 = TestHelper.CreateFileAccessManagerOptions(); using var accessManager1 = TestHelper.CreateAccessManager(fileAccessManagerOptions1, serverLocation: "us/california"); await using var server1 = await TestHelper.CreateServer(accessManager1); - + // create client var token1 = TestHelper.CreateAccessToken(accessManager1); await using var client = await TestHelper.CreateClient(token1, packetCapture: new TestNullPacketCapture()); Assert.AreEqual("us/california", client.Stat.ServerLocationInfo?.ServerLocation); } - [TestMethod] public async Task TcpChannel() { @@ -159,11 +158,13 @@ public async Task MaxDatagramChannels() // -------- // Check: Client MaxDatagramChannelCount larger than server // -------- - await using var client = await TestHelper.CreateClient(token, clientOptions: new ClientOptions - { - UseUdpChannel = false, - MaxDatagramChannelCount = 6 - }); + await using var client = await TestHelper.CreateClient(token, + deviceOptions: TestHelper.CreateDeviceOptions(), + clientOptions: new ClientOptions + { + UseUdpChannel = false, + MaxDatagramChannelCount = 6 + }); // let channel be created gradually for (var i = 0; i < 6; i++) @@ -205,7 +206,9 @@ public async Task UnsupportedClient() var token = TestHelper.CreateAccessToken(server); // Create Client - await using var client = await TestHelper.CreateClient(token, clientOptions: new ClientOptions { UseUdpChannel = true }); + await using var client = await TestHelper.CreateClient(token, + packetCapture: new TestNullPacketCapture(), + clientOptions: new ClientOptions { UseUdpChannel = true }); } [TestMethod] @@ -216,11 +219,14 @@ public async Task DatagramChannel_Stream() var token = TestHelper.CreateAccessToken(server); // Create Client - await using var client = await TestHelper.CreateClient(token, clientOptions: new ClientOptions - { - UseUdpChannel = false, - MaxDatagramChannelCount = 4 - }); + await using var client = await TestHelper.CreateClient(token, + deviceOptions: TestHelper.CreateDeviceOptions(), + clientOptions: new ClientOptions + { + UseUdpChannel = false, + MaxDatagramChannelCount = 4 + + }); var tasks = new List(); for (var i = 0; i < 50; i++) @@ -239,10 +245,12 @@ public async Task DatagramChannel_Udp() var token = TestHelper.CreateAccessToken(server); // Create Client - await using var client = await TestHelper.CreateClient(token, clientOptions: new ClientOptions - { - UseUdpChannel = true - }); + await using var client = await TestHelper.CreateClient(token, + deviceOptions: TestHelper.CreateDeviceOptions(), + clientOptions: new ClientOptions + { + UseUdpChannel = true + }); var tasks = new List(); for (var i = 0; i < 50; i++) @@ -294,8 +302,10 @@ public async Task UdpChannel_custom_udp_port() var token = TestHelper.CreateAccessToken(server); // Create Client - await using var client = await TestHelper.CreateClient(token, clientOptions: new ClientOptions { UseUdpChannel = true }); - await TestTunnel(server, client); + await using var client = await TestHelper.CreateClient(token, + packetCapture: new TestNullPacketCapture(), + clientOptions: new ClientOptions { UseUdpChannel = true }); + Assert.IsTrue(fileAccessManagerOptions.UdpEndPoints.Any(x => x.Port == client.HostUdpEndPoint?.Port)); } @@ -386,7 +396,7 @@ public async Task Client_must_dispose_after_device_closed() await using var server = await TestHelper.CreateServer(); var token = TestHelper.CreateAccessToken(server); - using var packetCapture = TestHelper.CreatePacketCapture(); + using var packetCapture = new TestNullPacketCapture(); await using var client = await TestHelper.CreateClient(token, packetCapture); packetCapture.StopCapture(); @@ -401,7 +411,9 @@ public async Task Client_must_dispose_after_server_stopped() // create client await using var client = await TestHelper.CreateClient(token, + deviceOptions: TestHelper.CreateDeviceOptions(), clientOptions: new ClientOptions { SessionTimeout = TimeSpan.FromSeconds(1) }); + await TestHelper.Test_Https(); await server.DisposeAsync(); @@ -441,7 +453,7 @@ public async Task Datagram_channel_after_client_reconnection() var token = TestHelper.CreateAccessToken(server); // create client - await using (await TestHelper.CreateClient(token)) + await using (await TestHelper.CreateClient(token, deviceOptions: TestHelper.CreateDeviceOptions())) { // test Icmp & Udp await TestHelper.Test_Ping(ping); @@ -449,7 +461,7 @@ public async Task Datagram_channel_after_client_reconnection() } // create client - await using (await TestHelper.CreateClient(token)) + await using (await TestHelper.CreateClient(token, deviceOptions: TestHelper.CreateDeviceOptions())) { // test Icmp & Udp await TestHelper.Test_Ping(ping); @@ -492,7 +504,7 @@ public async Task Disconnect_if_session_expired() var token = TestHelper.CreateAccessToken(server); // connect - await using var client = await TestHelper.CreateClient(token); + await using var client = await TestHelper.CreateClient(token, deviceOptions: TestHelper.CreateDeviceOptions()); Assert.AreEqual(ClientState.Connected, client.State); // close session @@ -644,13 +656,13 @@ public async Task Server_limit_by_Max_TcpChannel() using var tcpClient3 = new TcpClient(); using var tcpClient4 = new TcpClient(); - await tcpClient1.ConnectAsync(TestConstants.HttpsUri1.Host, 443); + await tcpClient1.ConnectAsync(TestConstants.TcpEndPoint1); await Task.Delay(300); - await tcpClient2.ConnectAsync(TestConstants.HttpsUri1.Host, 443); + await tcpClient2.ConnectAsync(TestConstants.TcpEndPoint1); await Task.Delay(300); - await tcpClient3.ConnectAsync(TestConstants.HttpsUri2.Host, 443); + await tcpClient3.ConnectAsync(TestConstants.TcpEndPoint2); await Task.Delay(300); - await tcpClient4.ConnectAsync(TestConstants.HttpsUri2.Host, 443); + await tcpClient4.ConnectAsync(TestConstants.TcpEndPoint2); await Task.Delay(300); var session = server.SessionManager.GetSessionById(client.SessionId); @@ -735,7 +747,10 @@ public async Task IsUdpChannelSupported_must_be_false_when_server_return_udp_por var token = TestHelper.CreateAccessToken(server); // Create Client - await using var client = await TestHelper.CreateClient(token, clientOptions: TestHelper.CreateClientOptions(useUdp: true)); + await using var client = await TestHelper.CreateClient(token, + packetCapture: new TestNullPacketCapture(), + clientOptions: TestHelper.CreateClientOptions(useUdp: true)); + Assert.IsFalse(client.Stat.IsUdpChannelSupported); } } \ No newline at end of file diff --git a/Tests/VpnHood.Test/Tests/DiagnoserTest.cs b/Tests/VpnHood.Test/Tests/DiagnoserTest.cs index 1d0897237..24bd3a95f 100644 --- a/Tests/VpnHood.Test/Tests/DiagnoserTest.cs +++ b/Tests/VpnHood.Test/Tests/DiagnoserTest.cs @@ -15,7 +15,9 @@ public async Task NormalConnect_NoInternet() token.ServerToken.HostEndPoints = [TestConstants.InvalidEp]; // create client - await using var clientApp = TestHelper.CreateClientApp(); + var appOptions = TestHelper.CreateAppOptions(); + appOptions.AutoDiagnose = true; + await using var clientApp = TestHelper.CreateClientApp(appOptions: appOptions); var clientProfile = clientApp.ClientProfileService.ImportAccessKey(token.ToAccessKey()); // ************ diff --git a/Tests/VpnHood.Test/Tests/DnsConfigurationTest.cs b/Tests/VpnHood.Test/Tests/DnsConfigurationTest.cs index db68fd6bb..5b30b1575 100644 --- a/Tests/VpnHood.Test/Tests/DnsConfigurationTest.cs +++ b/Tests/VpnHood.Test/Tests/DnsConfigurationTest.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using VpnHood.Common.Net; using VpnHood.Server.Access.Configurations; +using VpnHood.Test.Device; namespace VpnHood.Test.Tests; @@ -18,7 +19,7 @@ public async Task Server_specify_dns_servers() // create client var token = TestHelper.CreateAccessToken(server); - await using var client = await TestHelper.CreateClient(token); + await using var client = await TestHelper.CreateClient(token, packetCapture: new TestNullPacketCapture()); CollectionAssert.AreEqual(fileAccessManagerOptions.DnsServers, client.DnsServers); Assert.IsTrue(client.Stat.IsDnsServersAccepted); @@ -36,7 +37,7 @@ public async Task Client_specify_dns_servers() var token = TestHelper.CreateAccessToken(server); var clientOptions = TestHelper.CreateClientOptions(); clientOptions.DnsServers = [IPAddress.Parse("200.0.0.1"), IPAddress.Parse("200.0.0.2")]; - await using var client = await TestHelper.CreateClient(token, clientOptions: clientOptions); + await using var client = await TestHelper.CreateClient(token, clientOptions: clientOptions, packetCapture: new TestNullPacketCapture()); CollectionAssert.AreEqual(clientOptions.DnsServers, client.DnsServers); Assert.IsTrue(client.Stat.IsDnsServersAccepted); @@ -60,7 +61,9 @@ public async Task Server_override_dns_servers() var token = TestHelper.CreateAccessToken(server); var clientOptions = TestHelper.CreateClientOptions(); clientOptions.DnsServers = clientDnsServers; - await using var client = await TestHelper.CreateClient(token, clientOptions: clientOptions); + await using var client = await TestHelper.CreateClient(token, + clientOptions: clientOptions, + packetCapture: new TestNullPacketCapture()); CollectionAssert.AreEqual(fileAccessManagerOptions.DnsServers, client.DnsServers); Assert.IsFalse(client.Stat.IsDnsServersAccepted); @@ -82,8 +85,7 @@ public async Task Server_should_not_block_own_dns_servers() // create client var token = TestHelper.CreateAccessToken(server); - var clientOptions = TestHelper.CreateClientOptions(); - await using var client = await TestHelper.CreateClient(token, clientOptions: clientOptions); + await using var client = await TestHelper.CreateClient(token, packetCapture: new TestNullPacketCapture()); foreach (var serverDnsServer in serverDnsServers) Assert.IsFalse(server.SessionManager.NetFilter.BlockedIpRanges.IsInRange(serverDnsServer)); diff --git a/Tests/VpnHood.Test/Tests/NatTest.cs b/Tests/VpnHood.Test/Tests/NatTest.cs index 986fe358d..ad9b4681f 100644 --- a/Tests/VpnHood.Test/Tests/NatTest.cs +++ b/Tests/VpnHood.Test/Tests/NatTest.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using PacketDotNet; using VpnHood.Tunneling; +using VpnHood.Tunneling.Utils; namespace VpnHood.Test.Tests; diff --git a/Tests/VpnHood.Test/Tests/ServerFinderTest.cs b/Tests/VpnHood.Test/Tests/ServerFinderTest.cs new file mode 100644 index 000000000..b163b505b --- /dev/null +++ b/Tests/VpnHood.Test/Tests/ServerFinderTest.cs @@ -0,0 +1,123 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using VpnHood.Client; +using VpnHood.Server; +using VpnHood.Test.AccessManagers; +using VpnHood.Test.Device; +using VpnHood.Test.Services; + +namespace VpnHood.Test.Tests; + +[TestClass] +public class ServerFinderTest +{ + [TestMethod] + public async Task Find_reachable() + { + var storageFolder = TestHelper.CreateAccessManagerWorkingDir(); + var servers = new List(); + var accessManagers = new List(); + + try + { + // create servers + for (var i = 0; i < 5; i++) + { + var accessManager = TestHelper.CreateAccessManager(storagePath: storageFolder); + var server = await TestHelper.CreateServer(accessManager); + servers.Add(server); + accessManagers.Add(accessManager); + } + + // create token + var token = TestHelper.CreateAccessToken(servers[0]); + token.ServerToken.HostEndPoints = servers.SelectMany(x => x.ServerHost.TcpEndPoints).ToArray(); + + // stop server1 and server2 + await servers[0].DisposeAsync(); + await servers[1].DisposeAsync(); + + // connect + var client = await TestHelper.CreateClient(token, packetCapture: new TestNullPacketCapture()); + await TestHelper.WaitForClientState(client, ClientState.Connected); + + Assert.IsTrue( + servers[2].ServerHost.TcpEndPoints.First().Equals(client.HostTcpEndPoint) || + servers[3].ServerHost.TcpEndPoints.First().Equals(client.HostTcpEndPoint) || + servers[4].ServerHost.TcpEndPoints.First().Equals(client.HostTcpEndPoint) + ); + } + finally + { + await Task.WhenAll(servers.Select(x => x.DisposeAsync().AsTask())); + foreach (var accessManager in accessManagers) + accessManager.Dispose(); + } + } + + [TestMethod] + public async Task Find_reachable_for_redirect_in_order() + { + var storageFolder = TestHelper.CreateAccessManagerWorkingDir(); + var servers = new List(); + var accessManagers = new List(); + + try + { + // create servers + for (var i = 0; i < 10; i++) + { + var accessManager = TestHelper.CreateAccessManager(storagePath: storageFolder); + var server = await TestHelper.CreateServer(accessManager); + servers.Add(server); + accessManagers.Add(accessManager); + } + var serverEndPoints = servers.Select(x => x.ServerHost.TcpEndPoints.First()).ToArray(); + + accessManagers[2].RedirectHostEndPoints = + [ + serverEndPoints[4], + serverEndPoints[5], + serverEndPoints[6], + serverEndPoints[7] + ]; + + // create token by server[2] + var token = TestHelper.CreateAccessToken(servers[0]); + token.ServerToken.HostEndPoints = [serverEndPoints[1], serverEndPoints[2]]; + + // stop some servers + await servers[0].DisposeAsync(); + await servers[1].DisposeAsync(); + await servers[3].DisposeAsync(); + await servers[4].DisposeAsync(); + await servers[6].DisposeAsync(); + + // connect + var clientOptions = TestHelper.CreateClientOptions(); + var client = await TestHelper.CreateClient(token, packetCapture: new TestNullPacketCapture(), clientOptions: clientOptions); + await TestHelper.WaitForClientState(client, ClientState.Connected); + + Assert.AreEqual(servers[5].ServerHost.TcpEndPoints.First(), client.HostTcpEndPoint); + + // tracker should report unreachable servers + var testTracker = (TestTracker?)clientOptions.Tracker; + Assert.IsNotNull(testTracker); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[0])?.Parameters["available"] is null or false); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[1])?.Parameters["available"] is null or false); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[2])?.Parameters["available"] is true); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[3]) is null); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[4])?.Parameters["available"] is false); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[5])?.Parameters["available"] is true); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[6])?.Parameters["available"] is null or false); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[7])?.Parameters["available"] is null or true); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[8]) is null); + Assert.IsTrue(testTracker.FindEvent("vh_endpoint_status", "ep", serverEndPoints[9]) is null); + } + finally + { + await Task.WhenAll(servers.Select(x => x.DisposeAsync().AsTask())); + foreach (var accessManager in accessManagers) + accessManager.Dispose(); + } + } +} \ No newline at end of file diff --git a/Tests/VpnHood.Test/Tests/ServerNetFilterConfigTest.cs b/Tests/VpnHood.Test/Tests/ServerNetFilterConfigTest.cs index 81baee00a..0121daf0f 100644 --- a/Tests/VpnHood.Test/Tests/ServerNetFilterConfigTest.cs +++ b/Tests/VpnHood.Test/Tests/ServerNetFilterConfigTest.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using VpnHood.Client; using VpnHood.Common.Net; +using VpnHood.Test.Device; namespace VpnHood.Test.Tests; @@ -20,7 +21,7 @@ public async Task PacketCapture_Include() // create client await using var client = - new VpnHoodClient(TestHelper.CreatePacketCapture(), Guid.NewGuid(), token, new ClientOptions + new VpnHoodClient(new TestNullPacketCapture(), Guid.NewGuid(), token, new ClientOptions { PacketCaptureIncludeIpRanges = new IpRangeOrderedList([IpRange.Parse("230.0.0.0-230.0.0.200")]) }); @@ -49,7 +50,7 @@ public async Task PacketCapture_Exclude() // create client await using var client = - new VpnHoodClient(TestHelper.CreatePacketCapture(), Guid.NewGuid(), token, new ClientOptions + new VpnHoodClient(new TestNullPacketCapture(), Guid.NewGuid(), token, new ClientOptions { PacketCaptureIncludeIpRanges = new IpRangeOrderedList([IpRange.Parse("230.0.0.0-230.0.0.200")]) }); @@ -80,7 +81,7 @@ public async Task PacketCapture_Include_Exclude_LocalNetwork() var token = TestHelper.CreateAccessToken(server); // create client - await using var client = new VpnHoodClient(TestHelper.CreatePacketCapture(), Guid.NewGuid(), token, new ClientOptions()); + await using var client = new VpnHoodClient(new TestNullPacketCapture(), Guid.NewGuid(), token, new ClientOptions()); await client.Connect(); Assert.IsFalse(client.PacketCaptureIncludeIpRanges.IsInRange(IPAddress.Parse("192.168.0.100")), "LocalNetWorks failed"); @@ -107,7 +108,7 @@ public async Task IpRange_Include_Exclude() var token = TestHelper.CreateAccessToken(server); // create client - await using var client = new VpnHoodClient(TestHelper.CreatePacketCapture(), Guid.NewGuid(), token, new ClientOptions()); + await using var client = new VpnHoodClient(new TestNullPacketCapture(), Guid.NewGuid(), token, new ClientOptions()); await client.Connect(); Assert.IsFalse(client.IncludeIpRanges.IsInRange(IPAddress.Parse("230.0.0.110")), "Excludes failed"); diff --git a/Tests/VpnHood.Test/Tests/ServerTest.cs b/Tests/VpnHood.Test/Tests/ServerTest.cs index 506e98c86..439b94b9c 100644 --- a/Tests/VpnHood.Test/Tests/ServerTest.cs +++ b/Tests/VpnHood.Test/Tests/ServerTest.cs @@ -9,7 +9,9 @@ using VpnHood.Common.Net; using VpnHood.Common.Utils; using VpnHood.Server.Access.Configurations; +using VpnHood.Test.Device; using VpnHood.Tunneling; +// ReSharper disable DisposeOnUsingVariable namespace VpnHood.Test.Tests; @@ -146,7 +148,7 @@ public async Task Close_session_by_client_disconnect() // create client var token = TestHelper.CreateAccessToken(server); - await using var client = await TestHelper.CreateClient(token); + await using var client = await TestHelper.CreateClient(token, packetCapture: new TestNullPacketCapture()); Assert.IsTrue(accessManager.SessionController.Sessions.TryGetValue(client.SessionId, out var session)); await client.DisposeAsync(); diff --git a/Tests/VpnHood.Test/Tests/TcpDatagramTest.cs b/Tests/VpnHood.Test/Tests/TcpDatagramTest.cs index 0c083ab75..9ff506a12 100644 --- a/Tests/VpnHood.Test/Tests/TcpDatagramTest.cs +++ b/Tests/VpnHood.Test/Tests/TcpDatagramTest.cs @@ -7,6 +7,7 @@ using VpnHood.Tunneling.Channels; using VpnHood.Tunneling.ClientStreams; using VpnHood.Tunneling.DatagramMessaging; +using VpnHood.Tunneling.Utils; namespace VpnHood.Test.Tests; diff --git a/Tests/VpnHood.Test/Tests/TunnelTest.cs b/Tests/VpnHood.Test/Tests/TunnelTest.cs index ba82148ce..79c644812 100644 --- a/Tests/VpnHood.Test/Tests/TunnelTest.cs +++ b/Tests/VpnHood.Test/Tests/TunnelTest.cs @@ -10,6 +10,7 @@ using VpnHood.Tunneling; using VpnHood.Tunneling.Channels; using VpnHood.Tunneling.Channels.Streams; +using VpnHood.Tunneling.Utils; using ProtocolType = PacketDotNet.ProtocolType; namespace VpnHood.Test.Tests; diff --git a/Tests/VpnHood.Test/Tests/UdpProxyTest.cs b/Tests/VpnHood.Test/Tests/UdpProxyTest.cs index 785b909fc..755892a53 100644 --- a/Tests/VpnHood.Test/Tests/UdpProxyTest.cs +++ b/Tests/VpnHood.Test/Tests/UdpProxyTest.cs @@ -5,6 +5,7 @@ using VpnHood.Client; using VpnHood.Test.Services; using VpnHood.Tunneling; +using VpnHood.Tunneling.Utils; using ProtocolType = PacketDotNet.ProtocolType; @@ -157,7 +158,7 @@ public async Task Multiple_EndPointEx() [TestMethod] public async Task Max_UdpClients() { - var maxUdpCount = 3; + const int maxUdpCount = 3; // Create Server var accessManagerOptions = TestHelper.CreateFileAccessManagerOptions(); diff --git a/VpnHood.Client.App.Abstractions/IAppAdService.cs b/VpnHood.Client.App.Abstractions/IAppAdService.cs index 1a7d2d97d..1487b725a 100644 --- a/VpnHood.Client.App.Abstractions/IAppAdService.cs +++ b/VpnHood.Client.App.Abstractions/IAppAdService.cs @@ -10,4 +10,5 @@ public interface IAppAdService : IDisposable Task ShowAd(IUiContext uiContext, string? customData, CancellationToken cancellationToken); bool IsCountrySupported(string countryCode); DateTime? AdLoadedTime { get; } + TimeSpan AdLifeSpan { get; } } \ No newline at end of file diff --git a/VpnHood.Client.App.Abstractions/VpnHood.Client.App.Abstractions.csproj b/VpnHood.Client.App.Abstractions/VpnHood.Client.App.Abstractions.csproj index ad4ba7987..edbf14c51 100644 --- a/VpnHood.Client.App.Abstractions/VpnHood.Client.App.Abstractions.csproj +++ b/VpnHood.Client.App.Abstractions/VpnHood.Client.App.Abstractions.csproj @@ -19,7 +19,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Android.Ads.AdMob/AdMobAppOpenAdService.cs b/VpnHood.Client.App.Android.Ads.AdMob/AdMobAppOpenAdService.cs index a0492d3a7..b54fa649e 100644 --- a/VpnHood.Client.App.Android.Ads.AdMob/AdMobAppOpenAdService.cs +++ b/VpnHood.Client.App.Android.Ads.AdMob/AdMobAppOpenAdService.cs @@ -15,7 +15,8 @@ public class AdMobAppOpenAdService(string adUnitId, bool hasVideo) : IAppAdServi private AppOpenAd? _loadedAd; public string NetworkName => "AdMob"; public AppAdType AdType => AppAdType.AppOpenAd; - public DateTime? AdLoadedTime {get; private set; } + public DateTime? AdLoadedTime { get; private set; } + public TimeSpan AdLifeSpan => AdMobUtil.DefaultAdTimeSpan; public static AdMobAppOpenAdService Create(string adUnitId, bool hasVideo) { @@ -25,10 +26,12 @@ public static AdMobAppOpenAdService Create(string adUnitId, bool hasVideo) public bool IsCountrySupported(string countryCode) { + countryCode = countryCode.Trim().ToUpper(); + // these countries are not supported at all if (countryCode == "CN") - return false; - + return false; + // these countries video ad is not supported if (hasVideo) return countryCode != "IR"; @@ -44,6 +47,9 @@ public async Task LoadAd(IUiContext uiContext, CancellationToken cancellationTok if (activity.IsDestroyed) throw new LoadAdException("MainActivity has been destroyed before loading the ad."); + // initialize + await AdMobUtil.Initialize(activity, cancellationToken); + // reset the last loaded ad AdLoadedTime = null; _loadedAd = null; diff --git a/VpnHood.Client.App.Android.Ads.AdMob/AdMobInterstitialAdService.cs b/VpnHood.Client.App.Android.Ads.AdMob/AdMobInterstitialAdService.cs index cff1dbc8a..5f0f06bf5 100644 --- a/VpnHood.Client.App.Android.Ads.AdMob/AdMobInterstitialAdService.cs +++ b/VpnHood.Client.App.Android.Ads.AdMob/AdMobInterstitialAdService.cs @@ -15,6 +15,7 @@ public class AdMobInterstitialAdService(string adUnitId, bool hasVideo) : IAppAd public string NetworkName => "AdMob"; public AppAdType AdType => AppAdType.InterstitialAd; public DateTime? AdLoadedTime {get; private set; } + public TimeSpan AdLifeSpan => AdMobUtil.DefaultAdTimeSpan; public static AdMobInterstitialAdService Create(string adUnitId, bool hasVideo) { @@ -46,12 +47,16 @@ public async Task LoadAd(IUiContext uiContext, CancellationToken cancellationTok if (activity.IsDestroyed) throw new LoadAdException("MainActivity has been destroyed before loading the ad."); + // initialize + await AdMobUtil.Initialize(activity, cancellationToken); + // reset the last loaded ad AdLoadedTime = null; _loadedAd = null; var adLoadCallback = new MyInterstitialAdLoadCallback(); var adRequest = new AdRequest.Builder().Build(); + // AdMob load ad must call from main thread activity.RunOnUiThread(() => InterstitialAd.Load(activity, adUnitId, adRequest, adLoadCallback)); var cancellationTask = new TaskCompletionSource(); diff --git a/VpnHood.Client.App.Android.Ads.AdMob/AdMobRewardedAdService.cs b/VpnHood.Client.App.Android.Ads.AdMob/AdMobRewardedAdService.cs index 1d7dfa712..ec27939c8 100644 --- a/VpnHood.Client.App.Android.Ads.AdMob/AdMobRewardedAdService.cs +++ b/VpnHood.Client.App.Android.Ads.AdMob/AdMobRewardedAdService.cs @@ -14,6 +14,7 @@ public class AdMobRewardedAdService(string adUnitId) : IAppAdService public string NetworkName => "AdMob"; public AppAdType AdType => AppAdType.RewardedAd; public DateTime? AdLoadedTime { get; private set; } + public TimeSpan AdLifeSpan => AdMobUtil.DefaultAdTimeSpan; public static AdMobRewardedAdService Create(string adUnitId) { @@ -37,6 +38,9 @@ public async Task LoadAd(IUiContext uiContext, CancellationToken cancellationTok if (activity.IsDestroyed) throw new LoadAdException("MainActivity has been destroyed before loading the ad."); + // initialize + await AdMobUtil.Initialize(activity, cancellationToken); + // reset the last loaded ad AdLoadedTime = null; _loadedAd = null; diff --git a/VpnHood.Client.App.Android.Ads.AdMob/AdMobUtil.cs b/VpnHood.Client.App.Android.Ads.AdMob/AdMobUtil.cs new file mode 100644 index 000000000..fe6f19bc0 --- /dev/null +++ b/VpnHood.Client.App.Android.Ads.AdMob/AdMobUtil.cs @@ -0,0 +1,48 @@ +using Android.Content; +using Android.Gms.Ads; +using Android.Gms.Ads.Initialization; +using VpnHood.Common.Exceptions; +using VpnHood.Common.Utils; + +namespace VpnHood.Client.App.Droid.Ads.VhAdMob; + +public class AdMobUtil +{ + private static readonly AsyncLock InitLock = new(); + public static bool IsInitialized { get; private set; } + public static TimeSpan DefaultAdTimeSpan { get; } = TimeSpan.FromMinutes(45); + public static async Task Initialize(Context context, CancellationToken cancellationToken) + { + using var lockAsync = await InitLock.LockAsync(cancellationToken); + if (IsInitialized) + return; + + var initializeListener = new MyOnInitializationCompleteListener(); + MobileAds.Initialize(context, initializeListener); + await initializeListener.Task; + IsInitialized = true; + } + + private class MyOnInitializationCompleteListener : Java.Lang.Object, IOnInitializationCompleteListener + { + private readonly TaskCompletionSource _loadedCompletionSource = new(); + public Task Task => _loadedCompletionSource.Task; + public void OnInitializationComplete(IInitializationStatus initializationStatus) + { + // no adapter + if (initializationStatus.AdapterStatusMap.Keys.Count == 0) + throw new AdException("Could not find any ad adapter."); + + // at-least one ok + if (initializationStatus.AdapterStatusMap.Values.Any(value => + value.InitializationState == AdapterStatusState.Ready)) + { + _loadedCompletionSource.TrySetResult(); + return; + } + + // not success + _loadedCompletionSource.TrySetException(new AdException("Could not initialize any ad adapter.")); + } + } +} \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Ads.AdMob/Properties/AssemblyInfo.cs b/VpnHood.Client.App.Android.Ads.AdMob/Properties/AssemblyInfo.cs deleted file mode 100644 index 83d5d891f..000000000 --- a/VpnHood.Client.App.Android.Ads.AdMob/Properties/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: UsesPermission(Name = "com.google.android.gms.permission.AD_ID")] // required for AD diff --git a/VpnHood.Client.App.Android.Ads.AdMob/VpnHood.Client.App.Android.Ads.AdMob.csproj b/VpnHood.Client.App.Android.Ads.AdMob/VpnHood.Client.App.Android.Ads.AdMob.csproj index 3bcc8e864..6d4af2bd5 100644 --- a/VpnHood.Client.App.Android.Ads.AdMob/VpnHood.Client.App.Android.Ads.AdMob.csproj +++ b/VpnHood.Client.App.Android.Ads.AdMob/VpnHood.Client.App.Android.Ads.AdMob.csproj @@ -21,7 +21,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Android.Common/Activities/AndroidAppMainActivityHandler.cs b/VpnHood.Client.App.Android.Common/Activities/AndroidAppMainActivityHandler.cs index 13c575248..77c7a5d24 100644 --- a/VpnHood.Client.App.Android.Common/Activities/AndroidAppMainActivityHandler.cs +++ b/VpnHood.Client.App.Android.Common/Activities/AndroidAppMainActivityHandler.cs @@ -2,6 +2,7 @@ using Android.Content.Res; using Android.Runtime; using Android.Views; +using VpnHood.Client.Device; using VpnHood.Client.Device.Droid; using VpnHood.Client.Device.Droid.ActivityEvents; using Permission = Android.Content.PM.Permission; @@ -13,33 +14,30 @@ public class AndroidAppMainActivityHandler private readonly string[] _accessKeySchemes; private readonly string[] _accessKeyMimes; protected IActivityEvent ActivityEvent { get; } - protected virtual bool CheckForUpdateOnCreate { get; } public AndroidAppMainActivityHandler(IActivityEvent activityEvent, AndroidMainActivityOptions options) { ActivityEvent = activityEvent; _accessKeySchemes = options.AccessKeySchemes; _accessKeyMimes = options.AccessKeyMimes; - CheckForUpdateOnCreate = options.CheckForUpdateOnCreate; activityEvent.CreateEvent += (_, args) => OnCreate(args.SavedInstanceState); activityEvent.NewIntentEvent += (_, args) => OnNewIntent(args.Intent); activityEvent.RequestPermissionsResultEvent += (_, args) => OnRequestPermissionsResult(args.RequestCode, args.Permissions, args.GrantResults); activityEvent.ActivityResultEvent += (_, args) => OnActivityResult(args.RequestCode, args.ResultCode, args.Data); activityEvent.KeyDownEvent += (_, args) => args.IsHandled = OnKeyDown(args.KeyCode, args.KeyEvent); + activityEvent.PauseEvent += (_, _) => OnPause(); + activityEvent.ResumeEvent += (_, _) => OnResume(); activityEvent.DestroyEvent += (_, _) => OnDestroy(); activityEvent.ConfigurationChangedEvent += (_, args) => OnConfigurationChanged(args); } protected virtual void OnCreate(Bundle? savedInstanceState) { - VpnHoodApp.Instance.UiContext = new AndroidUiContext(ActivityEvent); + ActiveUiContext.Context = new AndroidUiContext(ActivityEvent); // process intent ProcessIntent(ActivityEvent.Activity.Intent); - - if (CheckForUpdateOnCreate) - _ = VpnHoodApp.Instance.VersionCheck(); } protected virtual bool OnNewIntent(Intent? intent) @@ -125,8 +123,16 @@ protected virtual void OnConfigurationChanged(Configuration args) VpnHoodApp.Instance.UpdateUi(); } + protected virtual void OnResume() + { + } + + protected virtual void OnPause() + { + } + protected virtual void OnDestroy() { - VpnHoodApp.Instance.UiContext = null; + ActiveUiContext.Context = null; } } \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Common/Activities/AndroidAppWebViewMainActivityHandler.cs b/VpnHood.Client.App.Android.Common/Activities/AndroidAppWebViewMainActivityHandler.cs index a82e7dd45..db73addaf 100644 --- a/VpnHood.Client.App.Android.Common/Activities/AndroidAppWebViewMainActivityHandler.cs +++ b/VpnHood.Client.App.Android.Common/Activities/AndroidAppWebViewMainActivityHandler.cs @@ -1,4 +1,5 @@ -using Android.Runtime; +using Android.Content.Res; +using Android.Runtime; using Android.Views; using Android.Webkit; using VpnHood.Client.App.WebServer; @@ -11,17 +12,15 @@ public class AndroidAppWebViewMainActivityHandler( AndroidMainActivityWebViewOptions options) : AndroidAppMainActivityHandler(activityEvent, options) { - private readonly bool _checkForUpdateOnCreate = options.CheckForUpdateOnCreate; private bool _isWeViewVisible; - public WebView? WebView { get; private set; } - protected override bool CheckForUpdateOnCreate => false; // parent should not do this + private WebView? WebView { get; set; } protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); // initialize web view - InitSplashScreen(); + InitLoadingPage(); // Initialize UI if (!VpnHoodAppWebServer.IsInit) @@ -34,29 +33,34 @@ protected override void OnCreate(Bundle? savedInstanceState) InitWebUi(); } - private void InitSplashScreen() + private void InitLoadingPage() { - var imageView = new ImageView(ActivityEvent.Activity); - var appInfo = Application.Context.ApplicationInfo ?? throw new Exception("Could not retrieve app info"); - var backgroundColor = VpnHoodApp.Instance.Resource.Colors.WindowBackgroundColor?.ToAndroidColor(); - - // set splash screen background color - var icon = appInfo.LoadIcon(Application.Context.PackageManager); - imageView.SetImageDrawable(icon); - imageView.LayoutParameters = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent); - imageView.SetScaleType(ImageView.ScaleType.CenterInside); - if (backgroundColor != null) imageView.SetBackgroundColor(backgroundColor.Value); - ActivityEvent.Activity.SetContentView(imageView); - + ActivityEvent.Activity.SetContentView(_Microsoft.Android.Resource.Designer.Resource.Layout.progressbar); + // set window background color - if (backgroundColor != null) + var linearLayout = ActivityEvent.Activity.FindViewById(_Microsoft.Android.Resource.Designer.Resource.Id.myLayout); + var backgroundColor = VpnHoodApp.Instance.Resource.Colors.WindowBackgroundColor?.ToAndroidColor(); + if (linearLayout != null && backgroundColor != null) { - try { ActivityEvent.Activity.Window?.SetStatusBarColor(backgroundColor.Value); } + try { linearLayout.SetBackgroundColor(backgroundColor.Value); } catch { /* ignore */ } + try { ActivityEvent.Activity.Window?.SetStatusBarColor(backgroundColor.Value); } + catch { /* ignore */ } + try { ActivityEvent.Activity.Window?.SetNavigationBarColor(backgroundColor.Value); } catch { /* ignore */ } } + + // set progressbar color + var progressBarColor = VpnHoodApp.Instance.Resource.Colors.ProgressBarColor?.ToAndroidColor(); + var progressBar = ActivityEvent.Activity.FindViewById(_Microsoft.Android.Resource.Designer.Resource.Id.progressBar); + if (progressBar != null && progressBarColor != null) + { + try + { progressBar.IndeterminateTintList = ColorStateList.ValueOf(progressBarColor.Value); } + catch { /* ignore */ } + } } private void InitWebUi() @@ -90,10 +94,6 @@ private void WebViewClient_PageLoaded(object? sender, EventArgs e) if (VpnHoodApp.Instance.Resource.Colors.NavigationBarColor != null) ActivityEvent.Activity.Window?.SetNavigationBarColor(VpnHoodApp.Instance.Resource.Colors.NavigationBarColor.Value.ToAndroidColor()); - - // request features after loading the webview, so SPA can update the localize the resources - if (_checkForUpdateOnCreate) - _ = VpnHoodApp.Instance.VersionCheck(); } protected override bool OnKeyDown([GeneratedEnum] Keycode keyCode, KeyEvent? e) diff --git a/VpnHood.Client.App.Android.Common/Activities/AndroidMainActivityOptions.cs b/VpnHood.Client.App.Android.Common/Activities/AndroidMainActivityOptions.cs index eca4aa8e5..bef138d3f 100644 --- a/VpnHood.Client.App.Android.Common/Activities/AndroidMainActivityOptions.cs +++ b/VpnHood.Client.App.Android.Common/Activities/AndroidMainActivityOptions.cs @@ -5,5 +5,4 @@ public class AndroidMainActivityOptions { public string[] AccessKeySchemes { get; init; } = []; public string[] AccessKeyMimes { get; init; } = []; - public bool CheckForUpdateOnCreate { get; init; } = true; } \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Common/Resources/layout/progressbar.xml b/VpnHood.Client.App.Android.Common/Resources/layout/progressbar.xml new file mode 100644 index 000000000..8a19bbf94 --- /dev/null +++ b/VpnHood.Client.App.Android.Common/Resources/layout/progressbar.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file 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 67ec7bc1c..bd923eada 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Android.Connect/AnalyticsTracker.cs b/VpnHood.Client.App.Android.Connect/AnalyticsTracker.cs new file mode 100644 index 000000000..4a7d21883 --- /dev/null +++ b/VpnHood.Client.App.Android.Connect/AnalyticsTracker.cs @@ -0,0 +1,33 @@ +using Firebase.Analytics; +using Ga4.Trackers; + +namespace VpnHood.Client.App.Droid.Connect; + +public class AnalyticsTracker(FirebaseAnalytics analytics) : ITracker +{ + public bool IsEnabled { get; set; } + public Task Track(IEnumerable trackEvents) + { + foreach (var trackEvent in trackEvents) + TrackInternal(trackEvent); + + return Task.CompletedTask; + } + + public Task Track(TrackEvent trackEvent) + { + TrackInternal(trackEvent); + return Task.CompletedTask; + } + + private void TrackInternal(TrackEvent trackEvent) + { + if (!IsEnabled) + return; + + var bundle = new Bundle(); + foreach (var parameter in trackEvent.Parameters) + bundle.PutString(parameter.Key, parameter.Value.ToString()); + analytics.LogEvent(trackEvent.EventName, bundle); + } +} \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/App.cs b/VpnHood.Client.App.Android.Connect/App.cs index 1abd02399..21cb11c1e 100644 --- a/VpnHood.Client.App.Android.Connect/App.cs +++ b/VpnHood.Client.App.Android.Connect/App.cs @@ -1,8 +1,9 @@ using System.Drawing; using Android.Runtime; using Firebase.Crashlytics; -using Firebase; +using Firebase.Analytics; using VpnHood.Client.App.Droid.Ads.VhAdMob; +using VpnHood.Client.App.Droid.Ads.VhChartboost; using VpnHood.Client.App.Droid.Common; using VpnHood.Client.App.Droid.GooglePlay; using VpnHood.Client.App.Resources; @@ -18,24 +19,26 @@ namespace VpnHood.Client.App.Droid.Connect; SupportsRtl = true, AllowBackup = true)] [MetaData("com.google.android.gms.ads.APPLICATION_ID", Value = AppSettings.AdMobApplicationId)] +[MetaData("com.google.android.gms.ads.flag.OPTIMIZE_INITIALIZATION", Value = "true")] +[MetaData("com.google.android.gms.ads.flag.OPTIMIZE_AD_LOADING", Value = "true")] + public class App(IntPtr javaReference, JniHandleOwnership transfer) : VpnHoodAndroidApp(javaReference, transfer) { + private FirebaseAnalytics? _analytics; protected override AppOptions CreateAppOptions() { - var appSettings = AppSettings.Create(); - if (appSettings is { FirebaseProjectId: not null, FirebaseApplicationId: not null, FirebaseApiKey: not null }) - InitFirebaseCrashlytics(appSettings); + // initialize Firebase services + try { _analytics = FirebaseAnalytics.GetInstance(this); } catch { /* ignored*/ } + try { FirebaseCrashlytics.Instance.SetCrashlyticsCollectionEnabled(true); } catch { /* ignored */ } + // load app settings and resources var storageFolderPath = AppOptions.DefaultStorageFolderPath; - var googlePlayAuthenticationService = new GooglePlayAuthenticationService(appSettings.GoogleSignInClientId); - var authenticationService = new StoreAuthenticationService(storageFolderPath, appSettings.StoreBaseUri, appSettings.StoreAppId, googlePlayAuthenticationService, appSettings.StoreIgnoreSslVerification); - var googlePlayBillingService = new GooglePlayBillingService(authenticationService); - var accountService = new StoreAccountService(authenticationService, googlePlayBillingService, appSettings.StoreAppId); - + var appSettings = AppSettings.Create(); var resources = DefaultAppResource.Resource; - resources.Colors.NavigationBarColor = Color.FromArgb(100, 32, 25, 81); - resources.Colors.WindowBackgroundColor = Color.FromArgb(100, 32, 25, 81); + resources.Colors.NavigationBarColor = Color.FromArgb(21, 14, 61); + resources.Colors.WindowBackgroundColor = Color.FromArgb(21, 14, 61); + resources.Colors.ProgressBarColor = Color.FromArgb(231, 180, 129); return new AppOptions { @@ -47,31 +50,44 @@ protected override AppOptions CreateAppOptions() IsAddAccessKeySupported = false, UpdaterService = new GooglePlayAppUpdaterService(), CultureService = AndroidAppAppCultureService.CreateIfSupported(), - AccountService = accountService, + AccountService = CreateAppAccountService(appSettings, storageFolderPath), + AllowEndPointTracker = appSettings.AllowEndPointTracker, + Tracker = _analytics != null ? new AnalyticsTracker(_analytics) : null, AdServices = [ AdMobInterstitialAdService.Create(appSettings.AdMobInterstitialAdUnitId, true), AdMobInterstitialAdService.Create(appSettings.AdMobInterstitialNoVideoAdUnitId, false), + ChartboostService.Create(appSettings.ChartboostAppId, appSettings.ChartboostAppSignature, appSettings.ChartboostAdLocation) ], - UiService = new AndroidAppUiService() + UiService = new AndroidAppUiService(), + LogAnonymous = !AppSettings.IsDebugMode, + AdOptions = new AppAdOptions + { + PreloadAd = true + } }; } - private void InitFirebaseCrashlytics(AppSettings appSettings) + // Set the clientId as userId to the analytics + public override void OnCreate() + { + base.OnCreate(); + _analytics?.SetUserId(VpnHoodApp.Instance.Settings.ClientId.ToString()); + } + + private static StoreAccountService? CreateAppAccountService(AppSettings appSettings, string storageFolderPath) { try { - var firebaseOptions = new FirebaseOptions.Builder() - .SetProjectId(appSettings.FirebaseProjectId) - .SetApplicationId(appSettings.FirebaseApplicationId) - .SetApiKey(appSettings.FirebaseApiKey) - .Build(); - FirebaseApp.InitializeApp(this, firebaseOptions); - FirebaseCrashlytics.Instance.SetCrashlyticsCollectionEnabled(true); + var googlePlayAuthenticationService = new GooglePlayAuthenticationService(appSettings.GoogleSignInClientId); + var authenticationService = new StoreAuthenticationService(storageFolderPath, appSettings.StoreBaseUri, appSettings.StoreAppId, googlePlayAuthenticationService, appSettings.StoreIgnoreSslVerification); + var googlePlayBillingService = new GooglePlayBillingService(authenticationService); + var accountService = new StoreAccountService(authenticationService, googlePlayBillingService, appSettings.StoreAppId); + return accountService; } - catch + catch (Exception) { // ignored + return null; } } -} - +} \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/AppSettings.cs b/VpnHood.Client.App.Android.Connect/AppSettings.cs index 3c36b1149..e23167ce2 100644 --- a/VpnHood.Client.App.Android.Connect/AppSettings.cs +++ b/VpnHood.Client.App.Android.Connect/AppSettings.cs @@ -1,5 +1,6 @@ using VpnHood.Common.Utils; // ReSharper disable StringLiteralTypo +// ReSharper disable CommentTypo namespace VpnHood.Client.App.Droid.Connect; @@ -7,6 +8,7 @@ internal class AppSettings : Singleton { public Uri? UpdateInfoUrl { get; init; } public bool ListenToAllIps { get; init; } = IsDebugMode; + public bool AllowEndPointTracker { get; init; } public int? DefaultSpaPort { get; init; } = IsDebugMode ? 9571 : 9570; // This is a test access key, you should replace it with your own access key. @@ -16,11 +18,6 @@ internal class AppSettings : Singleton // Google sign-in (It is created through Firebase) public string GoogleSignInClientId { get; init; } = "000000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"; //YOUR_FIREBASE_CLIENT_ID - // Firebase Crashlytics - public string? FirebaseProjectId { get; init; } //YOUR_FIREBASE_PROJECT_ID "client-xxxxx" - public string? FirebaseApplicationId { get; init; } //YOUR_FIREBASE_APPLICATION_ID "0:000000000000:android:0000000000000000000000" - public string? FirebaseApiKey { get; init; } //YOUR_FIREBASE_API_KEY "xxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxx" - // VpnHood Store server public Uri StoreBaseUri { get; init; } = new ("https://store-api.vpnhood.com"); public Guid StoreAppId { get; init; } = Guid.Parse("00000000-0000-0000-0000-000000000000"); //YOUR_VPNHOOD_STORE_APP_ID @@ -32,13 +29,12 @@ internal class AppSettings : Singleton public const string AdMobApplicationId = "ca-app-pub-8662231806304184~1740102860"; //YOUR_ADMOB_APP_ID public string AdMobInterstitialAdUnitId { get; init; } = "ca-app-pub-3940256099942544/8691691433"; public string AdMobInterstitialNoVideoAdUnitId { get; init; } = "ca-app-pub-3940256099942544/1033173712"; - public string AdMobRewardedAdUnitId { get; init; } = "ca-app-pub-3940256099942544/5224354917"; - public string AdMobAppOpenAdUnitId { get; init; } = "ca-app-pub-3940256099942544/9257395921"; - // UnityAd - public string UnityAdGameId { get; init; } = "YOUR_UNITY_AD_UNIT_GAME_ID"; - public string UnityAdInterstitialPlacementId { get; init; } = "YOUR_UNITY_INTERSTITIAL_AD_UNIT_PLACEMENT_ID"; - + // Chartboost + public string ChartboostAppId { get; init; } = "000000000000000000000000"; //YOUR_CHATBOOST_APP_ID + public string ChartboostAppSignature { get; init; } = "0000000000000000000000000000000000000000"; //YOUR_CHATBOOST_APP_SIGNATURE + public string ChartboostAdLocation { get; init; } = "YOUR_CHARTBOOST_AD_LOCATION"; + public static AppSettings Create() { var appSettingsJson = VhUtil.GetAssemblyMetadata(typeof(AppSettings).Assembly, "AppSettings", ""); diff --git a/VpnHood.Client.App.Android.Connect/MainActivity.cs b/VpnHood.Client.App.Android.Connect/MainActivity.cs index f47c2133b..b1997b646 100644 --- a/VpnHood.Client.App.Android.Connect/MainActivity.cs +++ b/VpnHood.Client.App.Android.Connect/MainActivity.cs @@ -20,6 +20,8 @@ namespace VpnHood.Client.App.Droid.Connect; [IntentFilter([Intent.ActionMain], Categories = [Intent.CategoryLauncher, Intent.CategoryLeanbackLauncher])] [IntentFilter([TileService.ActionQsTilePreferences])] + +// ReSharper disable once UnusedMember.Global public class MainActivity : AndroidAppMainActivity { protected override AndroidAppMainActivityHandler CreateMainActivityHandler() @@ -27,7 +29,7 @@ protected override AndroidAppMainActivityHandler CreateMainActivityHandler() return new AndroidAppWebViewMainActivityHandler(this, new AndroidMainActivityWebViewOptions { DefaultSpaPort = AppSettings.Instance.DefaultSpaPort, - ListenToAllIps = AppSettings.Instance.ListenToAllIps + ListenToAllIps = AppSettings.Instance.ListenToAllIps // if true it will cause crash in network change }); } } \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/Properties/AndroidManifest.xml b/VpnHood.Client.App.Android.Connect/Properties/AndroidManifest.xml index da3d1ef6b..2a4e45dad 100644 --- a/VpnHood.Client.App.Android.Connect/Properties/AndroidManifest.xml +++ b/VpnHood.Client.App.Android.Connect/Properties/AndroidManifest.xml @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/Properties/AssemblyInfo.cs b/VpnHood.Client.App.Android.Connect/Properties/AssemblyInfo.cs index c8cace328..afc21dbba 100644 --- a/VpnHood.Client.App.Android.Connect/Properties/AssemblyInfo.cs +++ b/VpnHood.Client.App.Android.Connect/Properties/AssemblyInfo.cs @@ -4,9 +4,5 @@ // and how to customise this process see: https://aka.ms/assembly-info-properties // ReSharper disable StringLiteralTypo - -using VpnHood.Common.Utils; - [assembly: UsesFeature("android.software.leanback", Required = false)] -[assembly: UsesFeature("android.hardware.touchscreen", Required = false)] - +[assembly: UsesFeature("android.hardware.touchscreen", Required = false)] \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/Properties/google-services.json b/VpnHood.Client.App.Android.Connect/Properties/google-services.json new file mode 100644 index 000000000..7eb4cd273 --- /dev/null +++ b/VpnHood.Client.App.Android.Connect/Properties/google-services.json @@ -0,0 +1,63 @@ +{ + "project_info": { + "project_number": "866504584967", + "project_id": "vpnhood-connect", + "storage_bucket": "vpnhood-connect.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:866504584967:android:d399a6c7cd406cc1ad5fa2", + "android_client_info": { + "package_name": "com.vpnhood.connect.android" + } + }, + "oauth_client": [ + { + "client_id": "866504584967-27dmiqabde6m6o7eao22o2svn3rv7bk4.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.vpnhood.connect.android", + "certificate_hash": "417565c5d672ae981a5de687ac44cc645ac2bd3e" + } + }, + { + "client_id": "866504584967-immiavpqi0ed6iddu58263qhkqip5ie3.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.vpnhood.connect.android", + "certificate_hash": "8351933c075258a0c056e224d2905bd07dfe4158" + } + }, + { + "client_id": "866504584967-lipctg311ui54ot4edrr64hs51jg8gf2.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.vpnhood.connect.android", + "certificate_hash": "11f9af16bf6eee9467d5137950a1eb9a27fb7d61" + } + }, + { + "client_id": "866504584967-mqa61vv13fg70n2gvkojp9vr00bhgagk.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBFZ_IP7peUZ2G8HAhP3RwXPKB_MfRa4yw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "866504584967-mqa61vv13fg70n2gvkojp9vr00bhgagk.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-anydpi-v26/ic_banner.xml b/VpnHood.Client.App.Android.Connect/Resources/mipmap-anydpi-v26/ic_banner.xml new file mode 100644 index 000000000..a12ee0f95 --- /dev/null +++ b/VpnHood.Client.App.Android.Connect/Resources/mipmap-anydpi-v26/ic_banner.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-anydpi-v26/ic_channel.xml b/VpnHood.Client.App.Android.Connect/Resources/mipmap-anydpi-v26/ic_channel.xml new file mode 100644 index 000000000..ab76b7ae7 --- /dev/null +++ b/VpnHood.Client.App.Android.Connect/Resources/mipmap-anydpi-v26/ic_channel.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-hdpi/banner.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-hdpi/banner.png deleted file mode 100644 index e440ca32f..000000000 Binary files a/VpnHood.Client.App.Android.Connect/Resources/mipmap-hdpi/banner.png and /dev/null differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-mdpi/banner.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-mdpi/banner.png deleted file mode 100644 index e8a190c91..000000000 Binary files a/VpnHood.Client.App.Android.Connect/Resources/mipmap-mdpi/banner.png and /dev/null differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/banner.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/banner.png index 891e90012..7bad67677 100644 Binary files a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/banner.png and b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/banner.png differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_banner_background.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_banner_background.png new file mode 100644 index 000000000..46bd90e4f Binary files /dev/null and b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_banner_background.png differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_banner_foreground.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_banner_foreground.png new file mode 100644 index 000000000..67f6e69fb Binary files /dev/null and b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_banner_foreground.png differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel.png new file mode 100644 index 000000000..dc8e6101f Binary files /dev/null and b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel.png differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel_background.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel_background.png new file mode 100644 index 000000000..71ee74093 Binary files /dev/null and b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel_background.png differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel_foreground.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel_foreground.png new file mode 100644 index 000000000..7a7d561cb Binary files /dev/null and b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xhdpi/ic_channel_foreground.png differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xxhdpi/banner.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xxhdpi/banner.png deleted file mode 100644 index b2ac7c4ac..000000000 Binary files a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xxhdpi/banner.png and /dev/null differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xxxhdpi/banner.png b/VpnHood.Client.App.Android.Connect/Resources/mipmap-xxxhdpi/banner.png deleted file mode 100644 index 0a31ace13..000000000 Binary files a/VpnHood.Client.App.Android.Connect/Resources/mipmap-xxxhdpi/banner.png and /dev/null differ diff --git a/VpnHood.Client.App.Android.Connect/Resources/values/ic_launcher_background.xml b/VpnHood.Client.App.Android.Connect/Resources/values/ic_launcher_background.xml index a08eb411e..9e0fbd92a 100644 --- a/VpnHood.Client.App.Android.Connect/Resources/values/ic_launcher_background.xml +++ b/VpnHood.Client.App.Android.Connect/Resources/values/ic_launcher_background.xml @@ -1,5 +1,5 @@ - #ffffff + #1b1449 \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/Resources/xml/locales_config.xml b/VpnHood.Client.App.Android.Connect/Resources/xml/locales_config.xml new file mode 100644 index 000000000..f118fe46d --- /dev/null +++ b/VpnHood.Client.App.Android.Connect/Resources/xml/locales_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/VpnHood.Client.App.Android.Connect/xml/network_security_config.xml b/VpnHood.Client.App.Android.Connect/Resources/xml/network_security_config.xml similarity index 100% rename from VpnHood.Client.App.Android.Connect/xml/network_security_config.xml rename to VpnHood.Client.App.Android.Connect/Resources/xml/network_security_config.xml 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 56f3e5298..8f372531a 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 - 534 - 4.5.534 + 539 + 4.6.539 23.0 @@ -30,7 +30,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.5.534 + 4.6.539 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -50,11 +50,10 @@ - - - - - + + Designer + MSBuild:UpdateGeneratedFiles + @@ -68,7 +67,13 @@ - + + + + + + + \ No newline at end of file diff --git a/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayAppUpdaterService.cs b/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayAppUpdaterService.cs index 5873298ba..2c11e9547 100644 --- a/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayAppUpdaterService.cs +++ b/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayAppUpdaterService.cs @@ -1,15 +1,12 @@ -using Java.Lang; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using VpnHood.Client.App.Abstractions; +using VpnHood.Client.App.Droid.GooglePlay.Utils; using VpnHood.Client.Device; using VpnHood.Client.Device.Droid; using VpnHood.Common.Logging; using VpnHood.Common.Utils; using Xamarin.Google.Android.Play.Core.AppUpdate; -using Xamarin.Google.Android.Play.Core.Install; using Xamarin.Google.Android.Play.Core.Install.Model; -using Exception = System.Exception; -using Object = Java.Lang.Object; namespace VpnHood.Client.App.Droid.GooglePlay; @@ -17,99 +14,31 @@ public class GooglePlayAppUpdaterService : IAppUpdaterService { public async Task Update(IUiContext uiContext) { - var appUiContext = (AndroidUiContext)uiContext; - using var appUpdateManager = AppUpdateManagerFactory.Create(appUiContext.Activity); try { - var appUpdateInfo = await new GooglePlayTaskCompleteListener(appUpdateManager.AppUpdateInfo).Task.VhConfigureAwait(); - var updateAvailability = appUpdateInfo.UpdateAvailability(); + var appUiContext = (AndroidUiContext)uiContext; + using var appUpdateManager = AppUpdateManagerFactory.Create(appUiContext.Activity); + using var appUpdateInfo = await appUpdateManager.AppUpdateInfo.AsTask().VhConfigureAwait() ?? throw new Exception("Could not retrieve AppUpdateInfo"); // play set UpdateAvailability.UpdateNotAvailable even when there is no connection to google // So we return false if there is UpdateNotAvailable to let the alternative way works - if (updateAvailability != UpdateAvailability.UpdateAvailable || !appUpdateInfo.IsUpdateTypeAllowed(AppUpdateType.Flexible)) + var updateAvailability = appUpdateInfo.UpdateAvailability(); + if (updateAvailability != UpdateAvailability.UpdateAvailable || + !appUpdateInfo.IsUpdateTypeAllowed(AppUpdateType.Immediate)) return false; - // Set download listener - using var googlePlayDownloadStateListener = new GooglePlayDownloadCompleteListener(appUpdateManager); - // Show Google Play update dialog - var updateFlowPlayTask = appUpdateManager.StartUpdateFlow(appUpdateInfo, appUiContext.Activity, AppUpdateOptions.NewBuilder(AppUpdateType.Flexible).Build()); - var updateFlowResult = await new GooglePlayTaskCompleteListener(updateFlowPlayTask).Task.VhConfigureAwait(); - if (updateFlowResult.IntValue() != -1) - throw new Exception("Could not start update flow."); - - // Wait for download complete - await googlePlayDownloadStateListener.WaitForCompletion().VhConfigureAwait(); - - // Start install downloaded update - var installUpdateTask = appUpdateManager.CompleteUpdate(); - var installUpdateStatus = await new GooglePlayTaskCompleteListener(installUpdateTask).Task.VhConfigureAwait(); - - // Could not start install - if (installUpdateStatus.IntValue() != -1) - throw new Exception("Could not complete update."); + using var updateFlowPlayTask = appUpdateManager.StartUpdateFlow(appUpdateInfo, appUiContext.Activity, AppUpdateOptions.NewBuilder(AppUpdateType.Immediate).Build()); + await updateFlowPlayTask.AsTask().VhConfigureAwait(); return true; } catch (Exception ex) { - VhLogger.Instance.LogWarning(ex, "Could update the app using Google Play."); + // return false to allow the alternative way + // google play does not throw exception if user cancel exception + VhLogger.Instance.LogWarning(ex, "Could not update the app using Google Play."); return false; } } - - public class GooglePlayDownloadCompleteListener - : Object, IInstallStateUpdatedListener - { - private readonly TaskCompletionSource _taskCompletionSource = new(); - private readonly IAppUpdateManager _appUpdateManager; - - public Task WaitForCompletion() - { - return _taskCompletionSource.Task; - } - - public GooglePlayDownloadCompleteListener(IAppUpdateManager appUpdateManager) - { - appUpdateManager.RegisterListener(this); - _appUpdateManager = appUpdateManager; - } - - public void OnStateUpdate(InstallState state) - { - var status = state.InstallStatus(); - switch (status) - { - case InstallStatus.Installed: - case InstallStatus.Downloaded: - _taskCompletionSource.TrySetResult(); - break; - - case InstallStatus.Canceled: - _taskCompletionSource.TrySetCanceled(); - break; - - case InstallStatus.Failed: - _taskCompletionSource.TrySetException(new Exception("Download failed.")); - break; - - case InstallStatus.Unknown: - _taskCompletionSource.TrySetException(new Exception("Unknown status for download.")); - break; - } - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (!_taskCompletionSource.Task.IsCompleted) - _taskCompletionSource.TrySetCanceled(); - - _appUpdateManager.UnregisterListener(this); - } - - base.Dispose(disposing); - } - } } \ No newline at end of file diff --git a/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayInAppReviewService.cs b/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayInAppReviewService.cs new file mode 100644 index 000000000..6a93dbb09 --- /dev/null +++ b/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayInAppReviewService.cs @@ -0,0 +1,40 @@ +using VpnHood.Client.App.Droid.GooglePlay.Utils; +using VpnHood.Client.Device; +using VpnHood.Client.Device.Droid; +using VpnHood.Common.Utils; +using Xamarin.Google.Android.Play.Core.Review; +using Xamarin.Google.Android.Play.Core.Review.Testing; +using Exception = System.Exception; + +namespace VpnHood.Client.App.Droid.GooglePlay; + +public class GooglePlayInAppReviewService +{ + private GooglePlayInAppReviewService() + { + } + + public static GooglePlayInAppReviewService Create() + { + var ret = new GooglePlayInAppReviewService(); + return ret; + } + + public async Task InitializeReview(IUiContext uiContext) + { + try + { + var appUiContext = (AndroidUiContext)uiContext; + //var reviewManager = ReviewManagerFactory.Create(appUiContext.Activity); + using var reviewManager = new FakeReviewManager(appUiContext.Activity); + using var reviewInfo = await reviewManager.RequestReviewFlow().AsTask().VhConfigureAwait(); + await reviewManager.LaunchReviewFlow(appUiContext.Activity, reviewInfo).AsTask().VhConfigureAwait(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + } +} \ No newline at end of file diff --git a/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayTaskCompleteListener.cs b/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayTaskCompleteListener.cs deleted file mode 100644 index 20b806862..000000000 --- a/VpnHood.Client.App.Android.GooglePlay.Core/GooglePlayTaskCompleteListener.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Xamarin.Google.Android.Play.Core.Tasks; -using Exception = Java.Lang.Exception; -using Object = Java.Lang.Object; -using Task = Xamarin.Google.Android.Play.Core.Tasks.Task; - -namespace VpnHood.Client.App.Droid.GooglePlay; - -public class GooglePlayTaskCompleteListener : Object, - IOnSuccessListener, - IOnFailureListener, - IOnCompleteListener -{ - private readonly TaskCompletionSource _taskCompletionSource; - public Task Task => _taskCompletionSource.Task; - - public GooglePlayTaskCompleteListener(Task googlePlayTask) - { - _taskCompletionSource = new TaskCompletionSource(); - googlePlayTask.AddOnSuccessListener(this); - googlePlayTask.AddOnFailureListener(this); - } - - public void OnSuccess(Object? obj) - { - if (obj is T result) - _taskCompletionSource.TrySetResult(result); - else - _taskCompletionSource.TrySetException(new System.Exception($"Unexpected type: {obj?.GetType()}.")); - } - - public void OnFailure(Exception ex) - { - _taskCompletionSource.TrySetException(ex); - } - - public void OnComplete(Task p0) - { - - } -} \ No newline at end of file diff --git a/VpnHood.Client.App.Android.GooglePlay.Core/Utils/GooglePlayExtensions.cs b/VpnHood.Client.App.Android.GooglePlay.Core/Utils/GooglePlayExtensions.cs new file mode 100644 index 000000000..039176f79 --- /dev/null +++ b/VpnHood.Client.App.Android.GooglePlay.Core/Utils/GooglePlayExtensions.cs @@ -0,0 +1,15 @@ +namespace VpnHood.Client.App.Droid.GooglePlay.Utils; + +public static class GooglePlayExtensions +{ + public static Task AsTask(this Xamarin.Google.Android.Play.Core.Tasks.Task googlePlayTask) where T : class + { + return GooglePlayTaskCompleteListener.Create(googlePlayTask); + } + + public static Task AsTask(this Xamarin.Google.Android.Play.Core.Tasks.Task googlePlayTask) + { + return GooglePlayTaskCompleteListener.Create(googlePlayTask); + } + +} \ No newline at end of file diff --git a/VpnHood.Client.App.Android.GooglePlay.Core/Utils/GooglePlayTaskCompleteListener.cs b/VpnHood.Client.App.Android.GooglePlay.Core/Utils/GooglePlayTaskCompleteListener.cs new file mode 100644 index 000000000..151af6fa1 --- /dev/null +++ b/VpnHood.Client.App.Android.GooglePlay.Core/Utils/GooglePlayTaskCompleteListener.cs @@ -0,0 +1,50 @@ +namespace VpnHood.Client.App.Droid.GooglePlay.Utils; + +public class GooglePlayTaskCompleteListener : Java.Lang.Object, + Xamarin.Google.Android.Play.Core.Tasks.IOnSuccessListener, + Xamarin.Google.Android.Play.Core.Tasks.IOnFailureListener, + Xamarin.Google.Android.Play.Core.Tasks.IOnCompleteListener +{ + private readonly TaskCompletionSource _taskCompletionSource; + public Task Task => _taskCompletionSource.Task; + private GooglePlayTaskCompleteListener(Xamarin.Google.Android.Play.Core.Tasks.Task googlePlayTask) + { + _taskCompletionSource = new TaskCompletionSource(); + googlePlayTask.AddOnSuccessListener(this); + googlePlayTask.AddOnFailureListener(this); + } + + public static Task Create(Xamarin.Google.Android.Play.Core.Tasks.Task googlePlayTask) + { + var listener = new GooglePlayTaskCompleteListener(googlePlayTask); + return listener.Task; + } + public void OnSuccess(Java.Lang.Object? obj) + { + switch (obj) + { + case null: + _taskCompletionSource.TrySetResult(default); + break; + + case T result: + _taskCompletionSource.TrySetResult(result); + break; + + default: + _taskCompletionSource.TrySetException(new Exception( + $"Unexpected type in GooglePlayTaskCompleteListener. Expected: {typeof(T)}, Actual: {obj.GetType()}.")); + break; + } + } + + public void OnFailure(Java.Lang.Exception ex) + { + _taskCompletionSource.TrySetException(ex); + } + + public void OnComplete(Xamarin.Google.Android.Play.Core.Tasks.Task p0) + { + + } +} \ No newline at end of file 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 7fb031753..04372d9b7 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) 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 0e63b147b..65fff831b 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -33,13 +33,17 @@ + + + - - + + + diff --git a/VpnHood.Client.App.Android/App.cs b/VpnHood.Client.App.Android/App.cs index cb264b1d9..99ca259b0 100644 --- a/VpnHood.Client.App.Android/App.cs +++ b/VpnHood.Client.App.Android/App.cs @@ -22,6 +22,7 @@ public class App(IntPtr javaReference, JniHandleOwnership transfer) IsAddAccessKeySupported = true, UpdaterService = AssemblyInfo.CreateUpdaterService(), CultureService = AndroidAppAppCultureService.CreateIfSupported(), - UiService = new AndroidAppUiService() + UiService = new AndroidAppUiService(), + LogAnonymous = !AssemblyInfo.IsDebugMode }; } \ No newline at end of file diff --git a/VpnHood.Client.App.Android/MainActivity.cs b/VpnHood.Client.App.Android/MainActivity.cs index 95c049f01..f70f7835a 100644 --- a/VpnHood.Client.App.Android/MainActivity.cs +++ b/VpnHood.Client.App.Android/MainActivity.cs @@ -13,9 +13,8 @@ namespace VpnHood.Client.App.Droid; MainLauncher = true, Exported = true, WindowSoftInputMode = SoftInput.AdjustResize, // resize app when keyboard is shown - // AlwaysRetainTaskState = true, //todo: looks not required // LaunchMode = LaunchMode.SingleInstance, if set; then open the app after minimize will not show ad activity - ScreenOrientation = ScreenOrientation.Unspecified, + ScreenOrientation = ScreenOrientation.Portrait, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.LayoutDirection | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden | ConfigChanges.FontScale | ConfigChanges.Locale | ConfigChanges.Navigation | ConfigChanges.UiMode)] diff --git a/VpnHood.Client.App.Android/Resources/xml/locales_config.xml b/VpnHood.Client.App.Android/Resources/xml/locales_config.xml index 65101fabe..69cd9ba57 100644 --- a/VpnHood.Client.App.Android/Resources/xml/locales_config.xml +++ b/VpnHood.Client.App.Android/Resources/xml/locales_config.xml @@ -1,6 +1,13 @@  + + + - + + + + + \ No newline at end of file diff --git a/VpnHood.Client.App.Android/VpnHood.Client.App.Android.csproj b/VpnHood.Client.App.Android/VpnHood.Client.App.Android.csproj index 36b0fe123..c3b35e999 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 - 535 - 4.5.535 + 544 + 4.6.544 23.0 @@ -30,7 +30,7 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -39,6 +39,13 @@ $(DefineConstants);GOOGLE_PLAY + + + Designer + MSBuild:UpdateGeneratedFiles + + + diff --git a/VpnHood.Client.App.Maui.Common/Platforms/Android/MauiActivityEvent.cs b/VpnHood.Client.App.Maui.Common/Platforms/Android/MauiActivityEvent.cs index bd290617b..807a75c8a 100644 --- a/VpnHood.Client.App.Maui.Common/Platforms/Android/MauiActivityEvent.cs +++ b/VpnHood.Client.App.Maui.Common/Platforms/Android/MauiActivityEvent.cs @@ -17,6 +17,8 @@ public class MauiActivityEvent : MauiAppCompatActivity, IActivityEvent public event EventHandler? NewIntentEvent; public event EventHandler? RequestPermissionsResultEvent; public event EventHandler? KeyDownEvent; + public event EventHandler? PauseEvent; + public event EventHandler? ResumeEvent; public event EventHandler? DestroyEvent; public event EventHandler? ConfigurationChangedEvent; public Activity Activity => this; @@ -74,6 +76,18 @@ protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result base.OnActivityResult(requestCode, resultCode, data); } + protected override void OnResume() + { + base.OnResume(); + ResumeEvent?.Invoke(this, EventArgs.Empty); + } + + protected override void OnPause() + { + PauseEvent?.Invoke(this, EventArgs.Empty); + base.OnPause(); + } + protected override void OnDestroy() { DestroyEvent?.Invoke(this, EventArgs.Empty); 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 a2e3eb9b0..2f62e2b05 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Resources/DefaultAppResource.cs b/VpnHood.Client.App.Resources/DefaultAppResource.cs index e7abf1f6d..c1f0a6099 100644 --- a/VpnHood.Client.App.Resources/DefaultAppResource.cs +++ b/VpnHood.Client.App.Resources/DefaultAppResource.cs @@ -10,7 +10,8 @@ public static class DefaultAppResource Colors = new AppResource.AppColors { NavigationBarColor = Color.FromArgb(18, 34, 114), - WindowBackgroundColor = Color.FromArgb(0x19, 0x40, 0xb0) + WindowBackgroundColor = Color.FromArgb(0x19, 0x40, 0xb0), + ProgressBarColor = Color.FromArgb(35, 201, 157), }, Icons = new AppResource.AppIcons { diff --git a/VpnHood.Client.App.Resources/Resources/SPA.zip b/VpnHood.Client.App.Resources/Resources/SPA.zip index 102240374..2ab2ea2cb 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 b3e6407b1..fd4476e18 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Store/StoreAccountService.cs b/VpnHood.Client.App.Store/StoreAccountService.cs index 67dacc065..30eaafe45 100644 --- a/VpnHood.Client.App.Store/StoreAccountService.cs +++ b/VpnHood.Client.App.Store/StoreAccountService.cs @@ -75,8 +75,6 @@ public async Task GetAccessKeys(string subscriptionId) { var httpClient = Authentication.HttpClient; var currentVpnUserClient = new CurrentVpnUserClient(httpClient); - - // todo: add includeAccessKey parameter and return accessKey in accessToken var accessTokens = await currentVpnUserClient.ListAccessTokensAsync(_storeAppId, subscriptionId: Guid.Parse(subscriptionId)).VhConfigureAwait(); var accessKeyList = new List(); diff --git a/VpnHood.Client.App.Store/StoreAuthenticationService.cs b/VpnHood.Client.App.Store/StoreAuthenticationService.cs index d591dd7d9..c557be99e 100644 --- a/VpnHood.Client.App.Store/StoreAuthenticationService.cs +++ b/VpnHood.Client.App.Store/StoreAuthenticationService.cs @@ -180,7 +180,7 @@ public class HttpClientHandlerAuth(StoreAuthenticationService accountService) : { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var apiKey = await accountService.TryGetApiKey(VpnHoodApp.Instance.UiContext).VhConfigureAwait(); + var apiKey = await accountService.TryGetApiKey(ActiveUiContext.Context).VhConfigureAwait(); request.Headers.Authorization = apiKey != null ? new AuthenticationHeaderValue(apiKey.AccessToken.Scheme, apiKey.AccessToken.Value) : null; return await base.SendAsync(request, cancellationToken).VhConfigureAwait(); } diff --git a/VpnHood.Client.App.Store/VpnHood.Client.App.Store.csproj b/VpnHood.Client.App.Store/VpnHood.Client.App.Store.csproj index 2a97fcb33..2b480c48e 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.Swagger/Controllers/AccountController.cs b/VpnHood.Client.App.Swagger/Controllers/AccountController.cs index 0b74a4d4a..97cc9627b 100644 --- a/VpnHood.Client.App.Swagger/Controllers/AccountController.cs +++ b/VpnHood.Client.App.Swagger/Controllers/AccountController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using VpnHood.Client.App.Abstractions; +using VpnHood.Client.App.Swagger.Exceptions; using VpnHood.Client.App.WebServer.Api; namespace VpnHood.Client.App.Swagger.Controllers; @@ -11,37 +12,37 @@ public class AccountController : ControllerBase, IAccountController [HttpGet] public Task Get() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("refresh")] public Task Refresh() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpGet("is-signin-with-google-supported")] public bool IsSigninWithGoogleSupported() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("signin-with-google")] public Task SignInWithGoogle() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("sign-out")] public new Task SignOut() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpGet("subscriptions/{subscriptionId}/access-keys")] public Task GetAccessKeys(string subscriptionId) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } } \ No newline at end of file diff --git a/VpnHood.Client.App.Swagger/Controllers/AppController.cs b/VpnHood.Client.App.Swagger/Controllers/AppController.cs index eff0b2c24..8591d8c61 100644 --- a/VpnHood.Client.App.Swagger/Controllers/AppController.cs +++ b/VpnHood.Client.App.Swagger/Controllers/AppController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using VpnHood.Client.App.ClientProfiles; using VpnHood.Client.App.Settings; +using VpnHood.Client.App.Swagger.Exceptions; using VpnHood.Client.App.WebServer.Api; using VpnHood.Client.Device; @@ -14,115 +15,115 @@ public class AppController : ControllerBase, IAppController [HttpPatch("configure")] public Task Configure(ConfigParams configParams) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpGet("config")] public Task GetConfig() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpGet("state")] public Task GetState() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("connect")] public Task Connect(Guid? clientProfileId = null, string? serverLocation = null) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("diagnose")] public Task Diagnose(Guid? clientProfileId = null, string? serverLocation = null) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("disconnect")] public Task Disconnect() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("clear-last-error")] public void ClearLastError() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPut("user-settings")] public Task SetUserSettings(UserSettings userSettings) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpGet("log.txt")] [Produces(MediaTypeNames.Text.Plain)] public Task Log() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpGet("installed-apps")] public Task GetInstalledApps() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpGet("ip-groups")] public Task GetIpGroups() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("version-check")] public Task VersionCheck() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("version-check-postpone")] public void VersionCheckPostpone() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPut("access-keys")] public Task AddAccessKey(string accessKey) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPatch("client-profiles/{clientProfileId:guid}")] public Task UpdateClientProfile(Guid clientProfileId, ClientProfileUpdateParams updateParams) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpDelete("client-profiles/{clientProfileId:guid}")] public Task DeleteClientProfile(Guid clientProfileId) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("settings/open-always-on-page")] public void OpenAlwaysOnPage() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("settings/request-quick-launch")] public Task RequestQuickLaunch() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("settings/request-notification")] public Task RequestNotification() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } } diff --git a/VpnHood.Client.App.Swagger/Controllers/BillingController.cs b/VpnHood.Client.App.Swagger/Controllers/BillingController.cs index 7ac4e5d6b..c79d02222 100644 --- a/VpnHood.Client.App.Swagger/Controllers/BillingController.cs +++ b/VpnHood.Client.App.Swagger/Controllers/BillingController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using VpnHood.Client.App.Abstractions; +using VpnHood.Client.App.Swagger.Exceptions; using VpnHood.Client.App.WebServer.Api; namespace VpnHood.Client.App.Swagger.Controllers; @@ -11,12 +12,12 @@ public class BillingController : ControllerBase, IBillingController [HttpGet("subscription-plans")] public Task GetSubscriptionPlans() { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } [HttpPost("purchase")] public Task Purchase(string planId) { - throw new NotImplementedException(); + throw new SwaggerOnlyException(); } } \ No newline at end of file diff --git a/VpnHood.Client.App.Swagger/Exceptions/SwaggerOnlyException.cs b/VpnHood.Client.App.Swagger/Exceptions/SwaggerOnlyException.cs new file mode 100644 index 000000000..a75860a2f --- /dev/null +++ b/VpnHood.Client.App.Swagger/Exceptions/SwaggerOnlyException.cs @@ -0,0 +1,3 @@ +namespace VpnHood.Client.App.Swagger.Exceptions; + +internal class SwaggerOnlyException() : Exception("This method is intended exclusively for use by Swagger and should not be called directly."); \ No newline at end of file diff --git a/VpnHood.Client.App.WebServer/Controllers/AccountController.cs b/VpnHood.Client.App.WebServer/Controllers/AccountController.cs index 1e1a10d51..6a95fe314 100644 --- a/VpnHood.Client.App.WebServer/Controllers/AccountController.cs +++ b/VpnHood.Client.App.WebServer/Controllers/AccountController.cs @@ -3,6 +3,7 @@ using EmbedIO.WebApi; using VpnHood.Client.App.Abstractions; using VpnHood.Client.App.WebServer.Api; +using VpnHood.Client.Device; namespace VpnHood.Client.App.WebServer.Controllers; @@ -35,13 +36,13 @@ public Task SignInWithGoogle() if (!AccountService.Authentication.IsSignInWithGoogleSupported) throw new NotSupportedException("Sign in with Google is not supported."); - return AccountService.Authentication.SignInWithGoogle(VpnHoodApp.Instance.RequiredUiContext); + return AccountService.Authentication.SignInWithGoogle(ActiveUiContext.RequiredContext); } [Route(HttpVerbs.Post, "/sign-out")] public Task SignOut() { - return AccountService.Authentication.SignOut(VpnHoodApp.Instance.RequiredUiContext); + return AccountService.Authentication.SignOut(ActiveUiContext.RequiredContext); } [Route(HttpVerbs.Get, "/subscriptions/{subscriptionId}/access-keys")] diff --git a/VpnHood.Client.App.WebServer/Controllers/AppController.cs b/VpnHood.Client.App.WebServer/Controllers/AppController.cs index e2d1898d6..b53230638 100644 --- a/VpnHood.Client.App.WebServer/Controllers/AppController.cs +++ b/VpnHood.Client.App.WebServer/Controllers/AppController.cs @@ -118,6 +118,7 @@ public async Task Log() await using var streamWriter = new StreamWriter(stream); var log = await App.LogService.GetLog().VhConfigureAwait(); await streamWriter.WriteAsync(log).VhConfigureAwait(); + HttpContext.SetHandled(); return ""; } @@ -155,19 +156,19 @@ public async Task DeleteClientProfile(Guid clientProfileId) [Route(HttpVerbs.Post, "/settings/open-always-on-page")] public void OpenAlwaysOnPage() { - App.Services.UiService.OpenAlwaysOnPage(App.RequiredUiContext); + App.Services.UiService.OpenAlwaysOnPage(ActiveUiContext.RequiredContext); } [Route(HttpVerbs.Post, "/settings/request-quick-launch")] public Task RequestQuickLaunch() { - return App.Services.UiService.RequestQuickLaunch(App.RequiredUiContext, CancellationToken.None); + return App.Services.UiService.RequestQuickLaunch(ActiveUiContext.RequiredContext, CancellationToken.None); } [Route(HttpVerbs.Post, "/settings/request-notification")] public Task RequestNotification() { - return App.Services.UiService.RequestNotification(App.RequiredUiContext, CancellationToken.None); + return App.Services.UiService.RequestNotification(ActiveUiContext.RequiredContext, CancellationToken.None); } } \ No newline at end of file diff --git a/VpnHood.Client.App.WebServer/Controllers/BillingController.cs b/VpnHood.Client.App.WebServer/Controllers/BillingController.cs index 6ccc58b6e..7be206016 100644 --- a/VpnHood.Client.App.WebServer/Controllers/BillingController.cs +++ b/VpnHood.Client.App.WebServer/Controllers/BillingController.cs @@ -3,6 +3,7 @@ using EmbedIO.WebApi; using VpnHood.Client.App.Abstractions; using VpnHood.Client.App.WebServer.Api; +using VpnHood.Client.Device; namespace VpnHood.Client.App.WebServer.Controllers; @@ -20,7 +21,7 @@ public Task GetSubscriptionPlans() [Route(HttpVerbs.Post, "/purchase")] public Task Purchase([QueryField] string planId) { - return BillingService.Purchase(VpnHoodApp.Instance.RequiredUiContext, planId); + return BillingService.Purchase(ActiveUiContext.RequiredContext, planId); } } \ No newline at end of file diff --git a/VpnHood.Client.App.WebServer/VpnHood.Client.App.WebServer.csproj b/VpnHood.Client.App.WebServer/VpnHood.Client.App.WebServer.csproj index a3c6863c9..8f7db78bd 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App.WebServer/VpnHoodAppWebServer.cs b/VpnHood.Client.App.WebServer/VpnHoodAppWebServer.cs index 91d58b743..bcee1e6fa 100644 --- a/VpnHood.Client.App.WebServer/VpnHoodAppWebServer.cs +++ b/VpnHood.Client.App.WebServer/VpnHoodAppWebServer.cs @@ -149,10 +149,12 @@ await text.WriteAsync(JsonSerializer.Serialize(data, // manage SPA fallback private Task HandleMappingFailed(IHttpContext context, MappedResourceInfo? info) { + if (context.IsHandled) return Task.CompletedTask; if (_indexHtml == null) throw new InvalidOperationException($"{nameof(_indexHtml)} is not initialized"); if (string.IsNullOrEmpty(Path.GetExtension(context.Request.Url.LocalPath))) return context.SendStringAsync(_indexHtml, "text/html", Encoding.UTF8); + throw HttpException.NotFound(); } 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 1003ff926..2fa60a22b 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -32,7 +32,7 @@ - + diff --git a/VpnHood.Client.App.Win.Setup/VpnHood.Client.App.Win.Setup.back(21.0.1).aip b/VpnHood.Client.App.Win.Setup/VpnHood.Client.App.Win.Setup.back(21.0.1).aip deleted file mode 100644 index 7d841d8da..000000000 --- a/VpnHood.Client.App.Win.Setup/VpnHood.Client.App.Win.Setup.back(21.0.1).aip +++ /dev/null @@ -1,1918 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/VpnHood.Client.App.Win.Setup/VpnHood.Client.App.Win.Setup.back.aip b/VpnHood.Client.App.Win.Setup/VpnHood.Client.App.Win.Setup.back.aip deleted file mode 100644 index 3543fcc4a..000000000 --- a/VpnHood.Client.App.Win.Setup/VpnHood.Client.App.Win.Setup.back.aip +++ /dev/null @@ -1,1886 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/VpnHood.Client.App.Win/App.xaml.cs b/VpnHood.Client.App.Win/App.xaml.cs index a2936e978..ad2a443bf 100644 --- a/VpnHood.Client.App.Win/App.xaml.cs +++ b/VpnHood.Client.App.Win/App.xaml.cs @@ -27,8 +27,10 @@ protected override void OnStartup(StartupEventArgs e) Resource = DefaultAppResource.Resource, UpdateInfoUrl = new Uri("https://github.com/vpnhood/VpnHood/releases/latest/download/VpnHoodClient-win-x64.json"), IsAddAccessKeySupported = true, - UpdaterService = new WinAppUpdaterService() - }); + UpdaterService = new WinAppUpdaterService(), + SingleLineConsoleLog = false, + LogAnonymous = !IsDebugMode + }); // initialize SPA ArgumentNullException.ThrowIfNull(DefaultAppResource.Resource.SpaZipData); @@ -70,4 +72,16 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); } + + public static bool IsDebugMode + { + get + { +#if DEBUG + return true; +#else + return false; +#endif + } + } } \ No newline at end of file diff --git a/VpnHood.Client.App.Win/MainWindow.xaml.cs b/VpnHood.Client.App.Win/MainWindow.xaml.cs index 50cfc5886..1eaadb01f 100644 --- a/VpnHood.Client.App.Win/MainWindow.xaml.cs +++ b/VpnHood.Client.App.Win/MainWindow.xaml.cs @@ -8,6 +8,7 @@ using Microsoft.Web.WebView2.Wpf; using VpnHood.Client.App.WebServer; using VpnHood.Client.App.Win.Common; +using VpnHood.Client.Device; using VpnHood.Client.Device.WinDivert; namespace VpnHood.Client.App.Win; @@ -42,7 +43,7 @@ public MainWindow() // initialize tray icon VpnHoodApp.Instance.ConnectionStateChanged += (_, _) => Dispatcher.Invoke(UpdateIcon); - VpnHoodApp.Instance.UiContext = new WinUiContext(); + ActiveUiContext.Context = new WinUiContext(); } diff --git a/VpnHood.Client.App.Win/VpnHood.Client.App.Win.csproj b/VpnHood.Client.App.Win/VpnHood.Client.App.Win.csproj index 99592efdf..66dc1cc91 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App/AppAdOptions.cs b/VpnHood.Client.App/AppAdOptions.cs new file mode 100644 index 000000000..f334b6f08 --- /dev/null +++ b/VpnHood.Client.App/AppAdOptions.cs @@ -0,0 +1,9 @@ +namespace VpnHood.Client.App; + +public class AppAdOptions +{ + public TimeSpan ShowAdPostDelay { get; set; } = TimeSpan.FromSeconds(3); + public TimeSpan LoadAdPostDelay { get; set; } = TimeSpan.FromSeconds(1); + public TimeSpan LoadAdTimeout { get; set; } = TimeSpan.FromSeconds(20); + public bool PreloadAd { get; set; } +} \ No newline at end of file diff --git a/VpnHood.Client.App/AppFeatures.cs b/VpnHood.Client.App/AppFeatures.cs index 1ff138804..d006dab10 100644 --- a/VpnHood.Client.App/AppFeatures.cs +++ b/VpnHood.Client.App/AppFeatures.cs @@ -14,4 +14,6 @@ public class AppFeatures public required bool IsQuickLaunchSupported { get; init; } public required bool IsNotificationSupported { get; init; } public required bool IsAlwaysOnSupported { get; init; } + public required string? GaMeasurementId { get; init; } + public required string ClientId { get; init; } } \ No newline at end of file diff --git a/VpnHood.Client.App/AppLogService.cs b/VpnHood.Client.App/AppLogService.cs index 5f0411bdd..92e02f07a 100644 --- a/VpnHood.Client.App/AppLogService.cs +++ b/VpnHood.Client.App/AppLogService.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using VpnHood.Client.App.Settings; using VpnHood.Common.Logging; using VpnHood.Tunneling; @@ -8,16 +7,18 @@ namespace VpnHood.Client.App; public class AppLogService : IDisposable { + private readonly bool _singleLineConsole; private StreamLogger? _streamLogger; + public string LogFilePath { get; } + public string[] LogEvents { get; private set; } = []; - public AppLogService(string logFilePath) + public AppLogService(string logFilePath, bool singleLineConsole) { + _singleLineConsole = singleLineConsole; LogFilePath = logFilePath; VhLogger.TcpCloseEventId = GeneralEventId.TcpLife; } - public string LogFilePath { get; } - public Task GetLog() { return File.ReadAllTextAsync(LogFilePath); @@ -26,49 +27,41 @@ public Task GetLog() public void Start(AppLogSettings logSettings) { VhLogger.IsAnonymousMode = logSettings.LogAnonymous; - VhLogger.IsDiagnoseMode = logSettings.LogVerbose; - VhLogger.Instance = NullLogger.Instance; - VhLogger.Instance = CreateLogger( - addToConsole: logSettings.LogToConsole, - addToFile: logSettings.LogToFile, - verbose: logSettings.LogVerbose, - removeLastFile: true); + VhLogger.IsDiagnoseMode = logSettings.LogEventNames.Contains("*"); + VhLogger.Instance = CreateLogger(logSettings, removeLastFile: true); + LogEvents = logSettings.LogEventNames; } public void Stop() { - VhLogger.Instance = NullLogger.Instance; - VhLogger.Instance = CreateLogger(addToConsole: false, addToFile: false, verbose: false, removeLastFile: false); - VhLogger.IsDiagnoseMode = false; + _streamLogger?.Dispose(); + LogEvents = []; } - private ILogger CreateLogger(bool addToConsole, bool addToFile, bool verbose, bool removeLastFile) + private ILogger CreateLogger(AppLogSettings logSettings, bool removeLastFile) { - var logger = CreateLoggerInternal(addToConsole, addToFile, verbose, removeLastFile); + var logger = CreateLoggerInternal( + logToConsole: logSettings.LogToConsole, + logToFile: logSettings.LogToFile, + logLevel: logSettings.LogLevel, + removeLastFile: removeLastFile); + logger = new SyncLogger(logger); logger = new FilterLogger(logger, eventId => { - if (eventId == GeneralEventId.Session) return true; - if (eventId == GeneralEventId.Tcp) return verbose; - if (eventId == GeneralEventId.Ping) return verbose; - if (eventId == GeneralEventId.Nat) return verbose; - if (eventId == GeneralEventId.Dns) return verbose; - if (eventId == GeneralEventId.Udp) return verbose; - if (eventId == GeneralEventId.TcpLife) return verbose; - if (eventId == GeneralEventId.Packet) return verbose; - if (eventId == GeneralEventId.StreamProxyChannel) return verbose; - if (eventId == GeneralEventId.DatagramChannel) return true; - return true; + if (logSettings.LogEventNames.Contains(eventId.Name, StringComparer.OrdinalIgnoreCase)) + return true; + + return eventId.Id == 0 || logSettings.LogEventNames.Contains("*"); }); return logger; } - private ILogger CreateLoggerInternal(bool addToConsole, bool addToFile, bool verbose, bool removeLastFile) + private ILogger CreateLoggerInternal(bool logToConsole, bool logToFile, LogLevel logLevel, bool removeLastFile) { // file logger, close old stream _streamLogger?.Dispose(); - _streamLogger = null; // delete last lgo if (removeLastFile && File.Exists(LogFilePath)) @@ -77,30 +70,39 @@ private ILogger CreateLoggerInternal(bool addToConsole, bool addToFile, bool ver using var loggerFactory = LoggerFactory.Create(builder => { // console - if (addToConsole) - { - builder.AddSimpleConsole(configure => - { - configure.TimestampFormat = "[HH:mm:ss.ffff] "; - configure.IncludeScopes = true; - configure.SingleLine = false; - }); - } + if (logToConsole) // AddSimpleConsole does not support event id + builder.AddProvider(new VhConsoleLogger(includeScopes: true, singleLine: _singleLineConsole)); - if (addToFile) + if (logToFile) { var fileStream = new FileStream(LogFilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); _streamLogger = new StreamLogger(fileStream); builder.AddProvider(_streamLogger); } - builder.SetMinimumLevel(verbose ? LogLevel.Trace : LogLevel.Information); + builder.SetMinimumLevel(logLevel); }); var logger = loggerFactory.CreateLogger(""); return new SyncLogger(logger); } + public static string[] GetLogEventNames(bool verbose, string? debugCommand, string[] defaults) + { + if (verbose) return ["*"]; + debugCommand ??= ""; + if (!defaults.Any()) defaults = [GeneralEventId.Session.Name!]; + + // Extract all event names from debugData that contains "log:EventName1,EventName2 + var names = new List(); + var parts = debugCommand.Split(' ').Where(x => x.Contains("/log:", StringComparison.OrdinalIgnoreCase)); + foreach (var part in parts) + names.AddRange(part[5..].Split(',')); + + // use user settings + return names.Count > 0 ? names.ToArray() : defaults; + } + public void Dispose() { Stop(); diff --git a/VpnHood.Client.App/AppOptions.cs b/VpnHood.Client.App/AppOptions.cs index 8108cbc47..05bba1d34 100644 --- a/VpnHood.Client.App/AppOptions.cs +++ b/VpnHood.Client.App/AppOptions.cs @@ -1,4 +1,5 @@ -using VpnHood.Client.App.Abstractions; +using Ga4.Trackers; +using VpnHood.Client.App.Abstractions; using VpnHood.Tunneling.Factory; namespace VpnHood.Client.App; @@ -23,9 +24,14 @@ public class AppOptions public IAppUpdaterService? UpdaterService { get; set; } public IAppAccountService? AccountService { get; set; } public IAppAdService[] AdServices { get; set; } = []; + public ITracker? Tracker { get; set; } public TimeSpan ReconnectTimeout { get; set; } = ClientOptions.Default.ReconnectTimeout; public TimeSpan AutoWaitTimeout { get; set; } = ClientOptions.Default.AutoWaitTimeout; - public TimeSpan AdLoadTimeout { get; set; } = TimeSpan.FromSeconds(20); - public bool? LogVerbose { get; set; } + public bool LogVerbose { get; set; } public bool? LogAnonymous { get; set; } + public TimeSpan ServerQueryTimeout { get; set; } = ClientOptions.Default.ServerQueryTimeout; + public bool SingleLineConsoleLog { get; set; } = true; + public bool AutoDiagnose { get; set; } = true; + public AppAdOptions AdOptions { get; set; } = new (); + public bool AllowEndPointTracker { get; set; } } \ No newline at end of file diff --git a/VpnHood.Client.App/AppResource.cs b/VpnHood.Client.App/AppResource.cs index 576d29e7f..ad212aa78 100644 --- a/VpnHood.Client.App/AppResource.cs +++ b/VpnHood.Client.App/AppResource.cs @@ -22,7 +22,6 @@ public class AppStrings public string MsgAccessKeyUpdated { get; set; } = Resource.MsgAccessKeyUpdated; public string MsgCantReadAccessKey { get; set; } = Resource.MsgCantReadAccessKey; public string MsgUnsupportedContent { get; set; } = Resource.MsgUnsupportedContent; - public string MsgCantShowAd { get; set; } = Resource.MsgCantShowAd; public string Open { get; set; } = Resource.Open; public string OpenInBrowser { get; set; } = Resource.OpenInBrowser; } @@ -31,6 +30,7 @@ public class AppColors { public Color? NavigationBarColor { get; set; } public Color? WindowBackgroundColor { get; set; } + public Color? ProgressBarColor { get; set; } } public class AppIcons diff --git a/VpnHood.Client.App/AppServices.cs b/VpnHood.Client.App/AppServices.cs index 97716ed4e..a425979de 100644 --- a/VpnHood.Client.App/AppServices.cs +++ b/VpnHood.Client.App/AppServices.cs @@ -1,4 +1,5 @@ -using VpnHood.Client.App.Abstractions; +using Ga4.Trackers; +using VpnHood.Client.App.Abstractions; namespace VpnHood.Client.App; @@ -9,4 +10,5 @@ public class AppServices public required IAppAdService[] AdServices { get; init; } = []; public required IAppUiService UiService { get; init; } public required IAppCultureService AppCultureService { get; init;} + public required ITracker? Tracker { get; set;} } \ No newline at end of file diff --git a/VpnHood.Client.App/ClientProfiles/ClientProfileService.cs b/VpnHood.Client.App/ClientProfiles/ClientProfileService.cs index eaea1cb68..308cdaf9f 100644 --- a/VpnHood.Client.App/ClientProfiles/ClientProfileService.cs +++ b/VpnHood.Client.App/ClientProfiles/ClientProfileService.cs @@ -144,7 +144,7 @@ internal ClientProfile[] ImportBuiltInAccessKeys(string[] accessKeys) var clientProfiles = accessTokens.Select(token => ImportAccessToken(token, overwriteNewer: false, allowOverwriteBuiltIn: true, isBuiltIn: true)); // remove old built-in client profiles that does not exist in the new list - if (_clientProfiles.RemoveAll(x => x.IsBuiltIn && clientProfiles.All(y => y.ClientProfileId != x.ClientProfileId))>0) + if (_clientProfiles.RemoveAll(x => x.IsBuiltIn && clientProfiles.All(y => y.ClientProfileId != x.ClientProfileId)) > 0) Save(); return clientProfiles.ToArray(); diff --git a/VpnHood.Client.App/IpGroupManager.cs b/VpnHood.Client.App/IpGroupManager.cs index f32170e09..4c0c067a0 100644 --- a/VpnHood.Client.App/IpGroupManager.cs +++ b/VpnHood.Client.App/IpGroupManager.cs @@ -51,7 +51,7 @@ private async Task GetIpRangesInternal(string ipGroupId) try { - await using var stream = _zipArchive.GetEntry($"{ipGroupId}.ips")?.Open() ?? throw new NotExistsException(); + await using var stream = _zipArchive.GetEntry($"{ipGroupId.ToLower()}.ips")?.Open() ?? throw new NotExistsException(); return IpRangeOrderedList.Deserialize(stream); } catch (Exception ex) diff --git a/VpnHood.Client.App/Resources/IpLocations.zip b/VpnHood.Client.App/Resources/IpLocations.zip index b81149a4a..b4f949815 100644 Binary files a/VpnHood.Client.App/Resources/IpLocations.zip and b/VpnHood.Client.App/Resources/IpLocations.zip differ diff --git a/VpnHood.Client.App/Services/AppInternalAdService.cs b/VpnHood.Client.App/Services/AppInternalAdService.cs new file mode 100644 index 000000000..d2b835be9 --- /dev/null +++ b/VpnHood.Client.App/Services/AppInternalAdService.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Logging; +using System.Globalization; +using VpnHood.Client.App.Abstractions; +using VpnHood.Client.App.Exceptions; +using VpnHood.Client.Device; +using VpnHood.Client.Device.Exceptions; +using VpnHood.Common.Exceptions; +using VpnHood.Common.Logging; +using VpnHood.Common.Utils; + +namespace VpnHood.Client.App.Services; + +internal class AppInternalAdService(IAppAdService[] adServices, AppAdOptions adOptions) +{ + public IAppAdService[] AdServices => adServices; + public IAppAdService? LoadedAdService { get; internal set; } + public bool IsPreloadApEnabled => adOptions.PreloadAd; + + public bool ShouldLoadAd() + { + return LoadedAdService?.AdLoadedTime == null || + (LoadedAdService.AdLoadedTime + LoadedAdService.AdLifeSpan) < DateTime.UtcNow; + } + + private readonly AsyncLock _loadAdLock = new(); + + public async Task LoadAd(IUiContext uiContext, string? countryCode, bool forceReload, + CancellationToken cancellationToken) + { + using var lockAsync = await _loadAdLock.LockAsync(cancellationToken); + if (!forceReload && !ShouldLoadAd()) + return; + + LoadedAdService = null; + + // filter ad services by country code + var filteredAdServices = adServices + .Where(x => countryCode is null || x.IsCountrySupported(countryCode)); + + var noFillAdNetworks = new List(); + foreach (var adService in filteredAdServices) + { + cancellationToken.ThrowIfCancellationRequested(); + + // find first successful ad network + try + { + if (noFillAdNetworks.Contains(adService.NetworkName)) + continue; + + using var timeoutCts = new CancellationTokenSource(adOptions.LoadAdTimeout); + using var linkedCts = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + await adService.LoadAd(uiContext, linkedCts.Token).VhConfigureAwait(); + await Task.Delay(adOptions.LoadAdPostDelay, cancellationToken); + LoadedAdService = adService; + return; + } + catch (NoFillAdException) + { + noFillAdNetworks.Add(adService.NetworkName); + } + catch (Exception ex) when (ex is UiContextNotAvailableException || ActiveUiContext.Context != uiContext) + { + throw new ShowAdNoUiException(); + } + + // do not catch if parent cancel the operation + catch (Exception ex) + { + VhLogger.Instance.LogWarning(ex, "Could not load any ad. Network: {Network}.", adService.NetworkName); + } + } + + throw new LoadAdException($"Could not load any AD. Country: {GetCountryName(countryCode)}"); + } + + public async Task ShowAd(IUiContext uiContext, string? customData, CancellationToken cancellationToken) + { + if (LoadedAdService == null) + throw new LoadAdException("Could not load any ad."); + + // show the ad + try + { + await LoadedAdService.ShowAd(uiContext, customData, cancellationToken).VhConfigureAwait(); + await Task.Delay(adOptions.ShowAdPostDelay, cancellationToken); //wait for finishing trackers + if (ActiveUiContext.Context != uiContext) // some ad provider may not raise exception on minimize + throw new ShowAdNoUiException(); + } + catch (Exception ex) + { + if (ActiveUiContext.Context != uiContext) + throw new ShowAdNoUiException(); + + // let's treat unknown error same as LoadException in this version + throw new LoadAdException("Could not show any ad.", ex); + } + finally + { + LoadedAdService = null; + } + } + + public static string GetCountryName(string? countryCode) + { + if (string.IsNullOrEmpty(countryCode)) return "n/a"; + try { return new RegionInfo(countryCode).Name; } + catch { return countryCode; } + } +} diff --git a/VpnHood.Client.App/Settings/AppLogSettings.cs b/VpnHood.Client.App/Settings/AppLogSettings.cs index e1b25fedc..202d89aca 100644 --- a/VpnHood.Client.App/Settings/AppLogSettings.cs +++ b/VpnHood.Client.App/Settings/AppLogSettings.cs @@ -1,9 +1,15 @@ -namespace VpnHood.Client.App.Settings; +using Microsoft.Extensions.Logging; +using System.Text.Json.Serialization; + +namespace VpnHood.Client.App.Settings; public class AppLogSettings { public bool LogToConsole { get; set; } = true; public bool LogToFile { get; set; } - public bool LogVerbose { get; set; } public bool LogAnonymous { get; set; } = true; + public string[] LogEventNames { get; set; } = []; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public LogLevel LogLevel { get; set; } = LogLevel.Information; } \ No newline at end of file diff --git a/VpnHood.Client.App/Settings/UserSettings.cs b/VpnHood.Client.App/Settings/UserSettings.cs index 2d1bfba91..a0c611c92 100644 --- a/VpnHood.Client.App/Settings/UserSettings.cs +++ b/VpnHood.Client.App/Settings/UserSettings.cs @@ -2,6 +2,7 @@ using System.Text.Json.Serialization; using VpnHood.Common.Converters; using VpnHood.Common.Net; +using VpnHood.Tunneling.DomainFiltering; namespace VpnHood.Client.App.Settings; @@ -35,6 +36,7 @@ public class UserSettings public IpRange[] PacketCaptureExcludeIpRanges { get; set; } = IpNetwork.None.ToIpRanges().ToArray(); public bool AllowAnonymousTracker { get; set; } = DefaultClientOptions.AllowAnonymousTracker; public IPAddress[]? DnsServers { get; set; } + public DomainFilter DomainFilter { get; set; } = new(); public string? DebugData1 { get; set; } public string? DebugData2 { get; set; } } \ No newline at end of file diff --git a/VpnHood.Client.App/VpnHood.Client.App.csproj b/VpnHood.Client.App/VpnHood.Client.App.csproj index a5c70b640..97972ee4a 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.App/VpnHoodApp.cs b/VpnHood.Client.App/VpnHoodApp.cs index 673c3b54c..48a93d526 100644 --- a/VpnHood.Client.App/VpnHoodApp.cs +++ b/VpnHood.Client.App/VpnHoodApp.cs @@ -1,10 +1,12 @@ using System.Globalization; using System.IO.Compression; using System.Net; +using System.Runtime.CompilerServices; using System.Text.Json; +using Ga4.Trackers; +using Ga4.Trackers.Ga4Tags; using Microsoft.Extensions.Logging; using VpnHood.Client.Abstractions; -using VpnHood.Client.App.Abstractions; using VpnHood.Client.App.ClientProfiles; using VpnHood.Client.App.Exceptions; using VpnHood.Client.App.Services; @@ -20,6 +22,7 @@ using VpnHood.Common.Logging; using VpnHood.Common.Messaging; using VpnHood.Common.Net; +using VpnHood.Common.Trackers; using VpnHood.Common.Utils; using VpnHood.Tunneling; using VpnHood.Tunneling.Factory; @@ -55,14 +58,17 @@ public class VpnHoodApp : Singleton, private readonly AppPersistState _appPersistState; private readonly TimeSpan _reconnectTimeout; private readonly TimeSpan _autoWaitTimeout; + private readonly TimeSpan _serverQueryTimeout; private CancellationTokenSource? _connectCts; private ClientProfile? _currentClientProfile; private VersionCheckResult? _versionCheckResult; private VpnHoodClient? _client; - private readonly bool? _logVerbose; + private readonly bool _logVerbose; private readonly bool? _logAnonymous; private UserSettings _oldUserSettings; - private readonly TimeSpan _adLoadTimeout; + private readonly bool _autoDiagnose; + private readonly AppInternalAdService? _internalAdService; + private readonly bool _allowEndPointTracker; private SessionStatus? LastSessionStatus => _client?.SessionStatus ?? _lastSessionStatus; private string VersionCheckFilePath => Path.Combine(StorageFolderPath, "version.json"); public string TempFolderPath => Path.Combine(StorageFolderPath, "Temp"); @@ -78,25 +84,21 @@ public class VpnHoodApp : Singleton, public ClientProfileService ClientProfileService { get; } public IDevice Device { get; } public JobSection JobSection { get; } - public TimeSpan TcpTimeout { get; set; } = new ClientOptions().ConnectTimeout; + public TimeSpan TcpTimeout { get; set; } = ClientOptions.Default.ConnectTimeout; 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 UiContextNotAvailableException(); private VpnHoodApp(IDevice device, AppOptions? options = default) { options ??= new AppOptions(); + device.StartedAsService += DeviceOnStartedAsService; Directory.CreateDirectory(options.StorageFolderPath); //make sure directory exists Resource = options.Resource; Device = device; - device.StartedAsService += DeviceOnStartedAsService; - - StorageFolderPath = options.StorageFolderPath ?? - throw new ArgumentNullException(nameof(options.StorageFolderPath)); + StorageFolderPath = options.StorageFolderPath ?? throw new ArgumentNullException(nameof(options.StorageFolderPath)); Settings = AppSettings.Load(Path.Combine(StorageFolderPath, FileNameSettings)); Settings.BeforeSave += SettingsBeforeSave; ClientProfileService = new ClientProfileService(Path.Combine(StorageFolderPath, FolderNameProfiles)); @@ -113,28 +115,32 @@ private VpnHoodApp(IDevice device, AppOptions? options = default) _versionCheckResult = VhUtil.JsonDeserializeFile(VersionCheckFilePath); _logVerbose = options.LogVerbose; _logAnonymous = options.LogAnonymous; - _adLoadTimeout = options.AdLoadTimeout; + _autoDiagnose = options.AutoDiagnose; + _serverQueryTimeout = options.ServerQueryTimeout; + _internalAdService = options.AdServices.Length > 0 ? new AppInternalAdService(options.AdServices, options.AdOptions) : null; + _allowEndPointTracker = options.AllowEndPointTracker; Diagnoser.StateChanged += (_, _) => FireConnectionStateChanged(); - LogService = new AppLogService(Path.Combine(StorageFolderPath, FileNameLog)); + LogService = new AppLogService(Path.Combine(StorageFolderPath, FileNameLog), options.SingleLineConsoleLog); + ActiveUiContext.OnChanged += ActiveUiContext_OnChanged; // configure update job section JobSection = new JobSection(new JobOptions { Interval = options.VersionCheckInterval, DueTime = options.VersionCheckInterval > TimeSpan.FromSeconds(5) - ? TimeSpan.FromSeconds(3) + ? TimeSpan.FromSeconds(2) // start immediately : options.VersionCheckInterval, Name = "VersionCheck" }); // create start up logger - if (!device.IsLogToConsoleSupported) UserSettings.Logging.LogToConsole = false; LogService.Start(new AppLogSettings { - LogVerbose = options.LogVerbose ?? Settings.UserSettings.Logging.LogVerbose, + LogEventNames = AppLogService.GetLogEventNames(options.LogVerbose, UserSettings.DebugData1, UserSettings.Logging.LogEventNames), LogAnonymous = options.LogAnonymous ?? Settings.UserSettings.Logging.LogAnonymous, LogToConsole = UserSettings.Logging.LogToConsole, - LogToFile = UserSettings.Logging.LogToFile + LogToFile = UserSettings.Logging.LogToFile, + LogLevel = options.LogVerbose ? LogLevel.Trace : LogLevel.Information }); // add default test public server if not added yet @@ -160,7 +166,9 @@ private VpnHoodApp(IDevice device, AppOptions? options = default) IsBillingSupported = options.AccountService?.Billing != null, IsQuickLaunchSupported = uiService.IsQuickLaunchSupported, IsNotificationSupported = uiService.IsNotificationSupported, - IsAlwaysOnSupported = device.IsAlwaysOnSupported + IsAlwaysOnSupported = device.IsAlwaysOnSupported, + GaMeasurementId = options.AppGa4MeasurementId, + ClientId = Settings.ClientId.ToString() }; // initialize services @@ -170,7 +178,8 @@ private VpnHoodApp(IDevice device, AppOptions? options = default) AdServices = options.AdServices, AccountService = options.AccountService != null ? new AppAccountService(this, options.AccountService) : null, UpdaterService = options.UpdaterService, - UiService = uiService + UiService = uiService, + Tracker = options.Tracker }; // Clear last update status if version has changed @@ -180,11 +189,91 @@ private VpnHoodApp(IDevice device, AppOptions? options = default) File.Delete(VersionCheckFilePath); } - // initialize - InitCulture(); + // Apply settings but no error on start up + ApplySettings(); + + // schedule job JobRunner.Default.Add(this); } + private void ApplySettings() + { + try + { + var state = State; + var client = _client; // it may be null + var disconnectRequired = false; + if (client != null) + { + client.UseUdpChannel = UserSettings.UseUdpChannel; + client.DropUdpPackets = UserSettings.DebugData1?.Contains("/drop-udp") == true || UserSettings.DropUdpPackets; + + // check is disconnect required + disconnectRequired = + (_oldUserSettings.TunnelClientCountry != UserSettings.TunnelClientCountry) || + (_activeClientProfileId != null && UserSettings.ClientProfileId != _activeClientProfileId) || //ClientProfileId has been changed + (_activeServerLocation != state.ClientServerLocationInfo?.ServerLocation) || //ClientProfileId has been changed + (UserSettings.IncludeLocalNetwork != client.IncludeLocalNetwork); // IncludeLocalNetwork has been changed + } + + // Enable trackers + if (Services.Tracker != null) + Services.Tracker.IsEnabled = Settings.UserSettings.AllowAnonymousTracker; + + //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(); + _oldUserSettings = VhUtil.JsonClone(UserSettings); + + // disconnect + if (state.CanDisconnect && disconnectRequired) + _ = Disconnect(true); + } + catch (Exception ex) + { + ReportError(ex, "Could not apply settings."); + } + } + + private ITracker CreateBuildInTracker(string? userAgent) + { + if (string.IsNullOrEmpty(_appGa4MeasurementId)) + throw new InvalidOperationException("AppGa4MeasurementId is required to create a built-in tracker."); + + var tracker = new Ga4TagTracker + { + MeasurementId = _appGa4MeasurementId, + SessionCount = 1, + ClientId = Settings.ClientId.ToString(), + SessionId = Guid.NewGuid().ToString(), + UserProperties = new Dictionary { { "client_version", Features.Version.ToString(3) } } + }; + + if (!string.IsNullOrEmpty(userAgent)) + tracker.UserAgent = userAgent; + + _ = tracker.Track(new TrackEvent { EventName = TrackEventNames.SessionStart }); + + return tracker; + } + + private void ActiveUiContext_OnChanged(object sender, EventArgs e) + { + var uiContext = ActiveUiContext.Context; + if (IsIdle && _internalAdService?.IsPreloadApEnabled == true && uiContext != null) + _ = LoadAd(uiContext, CancellationToken.None); + } + public ClientProfile? CurrentClientProfile { get @@ -226,7 +315,7 @@ is AppConnectionState.Connected or AppConnectionState.Connecting IsWaitingForAd = client?.Stat.IsWaitingForAd is true, ConnectRequestTime = _connectRequestTime, IsUdpChannelSupported = client?.Stat.IsUdpChannelSupported, - CurrentUiCultureInfo = new UiCultureInfo(CultureInfo.DefaultThreadCurrentUICulture), + CurrentUiCultureInfo = new UiCultureInfo(CultureInfo.DefaultThreadCurrentUICulture ?? SystemUiCulture), SystemUiCultureInfo = new UiCultureInfo(SystemUiCulture), VersionStatus = _versionCheckResult?.VersionStatus ?? VersionStatus.Unknown, PurchaseState = Services.AccountService?.Billing?.PurchaseState, @@ -265,7 +354,14 @@ private void FireConnectionStateChanged() var connectionState = ConnectionState; if (connectionState == _lastConnectionState) return; _lastConnectionState = connectionState; - ConnectionStateChanged?.Invoke(this, EventArgs.Empty); + try + { + ConnectionStateChanged?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + ReportError(ex, "Could not FireConnectionStateChanged"); + } } public async ValueTask DisposeAsync() @@ -274,6 +370,7 @@ public async ValueTask DisposeAsync() Device.Dispose(); LogService.Dispose(); DisposeSingleton(); + ActiveUiContext.OnChanged -= ActiveUiContext_OnChanged; } public static VpnHoodApp Init(IDevice device, AppOptions? options = default) @@ -314,6 +411,10 @@ public async Task Connect(Guid? clientProfileId = null, string? serverLocation = if (!IsIdle) await Disconnect(true).VhConfigureAwait(); + // initialize built-in tracker after acquire userAgent + if (Services.Tracker == null && UserSettings.AllowAnonymousTracker && !string.IsNullOrEmpty(_appGa4MeasurementId)) + Services.Tracker = CreateBuildInTracker(userAgent); + // request features for the first time await RequestFeatures(cancellationToken).VhConfigureAwait(); @@ -345,13 +446,13 @@ public async Task Connect(Guid? clientProfileId = null, string? serverLocation = FireConnectionStateChanged(); LogService.Start(new AppLogSettings { - LogVerbose = _logVerbose ?? Settings.UserSettings.Logging.LogVerbose | diagnose, + LogEventNames = AppLogService.GetLogEventNames(_logVerbose, UserSettings.DebugData1, UserSettings.Logging.LogEventNames), LogAnonymous = _logAnonymous ?? Settings.UserSettings.Logging.LogAnonymous, LogToConsole = UserSettings.Logging.LogToConsole, - LogToFile = UserSettings.Logging.LogToFile | diagnose + LogToFile = UserSettings.Logging.LogToFile | diagnose, + LogLevel = _logVerbose || diagnose ? LogLevel.Trace : LogLevel.Information }); - // log general info VhLogger.Instance.LogInformation("AppVersion: {AppVersion}", GetType().Assembly.GetName().Version); VhLogger.Instance.LogInformation("Time: {Time}", DateTime.UtcNow.ToString("u", new CultureInfo("en-US"))); @@ -362,7 +463,7 @@ public async Task Connect(Guid? clientProfileId = null, string? serverLocation = // log country name if (diagnose) - VhLogger.Instance.LogInformation("Country: {Country}", GetCountryName(await GetClientCountryCode().VhConfigureAwait())); + VhLogger.Instance.LogInformation("Country: {Country}", GetCountryName(await GetClientCountryCode(cancellationToken).VhConfigureAwait())); VhLogger.Instance.LogInformation("VpnHood Client is Connecting ..."); @@ -376,12 +477,12 @@ public async Task Connect(Guid? clientProfileId = null, string? serverLocation = } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Could not connect to any server within the given time."); + ReportError(ex, "Could not connect."); //user may disconnect before connection closed if (!_hasDisconnectedByUser) _appPersistState.LastError = ex is OperationCanceledException - ? new ApiError(new Exception("Could not connect to any server within the given time.", ex)) + ? new ApiError(new Exception("Could not connect to any server.", ex)) : new ApiError(ex); // don't wait for disconnect, it may cause deadlock @@ -412,7 +513,7 @@ private async Task CreatePacketCapture() } // create packet capture - var packetCapture = await Device.CreatePacketCapture(UiContext).VhConfigureAwait(); + var packetCapture = await Device.CreatePacketCapture(ActiveUiContext.Context).VhConfigureAwait(); // init packet capture if (packetCapture.IsMtuSupported) @@ -429,7 +530,7 @@ private async Task CreatePacketCapture() } private async Task ConnectInternal(Token token, string? serverLocationInfo, string? userAgent, - bool allowUpdateToken, CancellationToken cancellationToken) + bool allowUpdateToken, CancellationToken cancellationToken) { // show token info VhLogger.Instance.LogInformation("TokenId: {TokenId}, SupportId: {SupportId}", @@ -455,19 +556,26 @@ private async Task ConnectInternal(Token token, string? serverLocationInfo, stri PacketCaptureIncludeIpRanges = packetCaptureIpRanges, MaxDatagramChannelCount = UserSettings.MaxDatagramChannelCount, ConnectTimeout = TcpTimeout, - AllowAnonymousTracker = UserSettings.AllowAnonymousTracker, + ServerQueryTimeout = _serverQueryTimeout, DropUdpPackets = UserSettings.DebugData1?.Contains("/drop-udp") == true || UserSettings.DropUdpPackets, - AppGa4MeasurementId = _appGa4MeasurementId, ServerLocation = serverLocationInfo == ServerLocationInfo.Auto.ServerLocation ? null : serverLocationInfo, - UseUdpChannel = UserSettings.UseUdpChannel + UseUdpChannel = UserSettings.UseUdpChannel, + DomainFilter = UserSettings.DomainFilter, + ForceLogSni = LogService.LogEvents.Contains(nameof(GeneralEventId.Sni), StringComparer.OrdinalIgnoreCase), + AllowAnonymousTracker = UserSettings.AllowAnonymousTracker, + AllowEndPointTracker = UserSettings.AllowAnonymousTracker && _allowEndPointTracker, + Tracker = Services.Tracker }; 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().VhConfigureAwait(); + + VpnHoodClient? client = null; try @@ -478,8 +586,10 @@ private async Task ConnectInternal(Token token, string? serverLocationInfo, stri if (_hasDiagnoseStarted) await Diagnoser.Diagnose(client, cancellationToken).VhConfigureAwait(); - else + else if (_autoDiagnose) await Diagnoser.Connect(client, cancellationToken).VhConfigureAwait(); + else + await client.Connect(cancellationToken).VhConfigureAwait(); // set connected time ConnectedTime = DateTime.Now; @@ -518,36 +628,36 @@ await ClientProfileService.UpdateServerTokenByUrl(token).VhConfigureAwait()) private async Task RequestFeatures(CancellationToken cancellationToken) { // QuickLaunch - if (UiContext != null && + if (ActiveUiContext.Context != null && Services.UiService.IsQuickLaunchSupported && Settings.IsQuickLaunchEnabled is null) { try { Settings.IsQuickLaunchEnabled = - await Services.UiService.RequestQuickLaunch(RequiredUiContext, cancellationToken).VhConfigureAwait(); + await Services.UiService.RequestQuickLaunch(ActiveUiContext.RequiredContext, cancellationToken).VhConfigureAwait(); } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Could not add QuickLaunch."); + ReportError(ex, "Could not add QuickLaunch."); } Settings.Save(); } // Notification - if (UiContext != null && + if (ActiveUiContext.Context != null && Services.UiService.IsNotificationSupported && Settings.IsNotificationEnabled is null) { try { Settings.IsNotificationEnabled = - await Services.UiService.RequestNotification(RequiredUiContext, cancellationToken).VhConfigureAwait(); + await Services.UiService.RequestNotification(ActiveUiContext.RequiredContext, cancellationToken).VhConfigureAwait(); } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Could not enable Notification."); + ReportError(ex, "Could not enable Notification."); } Settings.Save(); @@ -571,39 +681,7 @@ private void InitCulture() private void SettingsBeforeSave(object sender, EventArgs e) { - var state = State; - var client = _client; // it may get null - if (client != null) - { - client.UseUdpChannel = UserSettings.UseUdpChannel; - client.DropUdpPackets = UserSettings.DebugData1?.Contains("/drop-udp") == true || UserSettings.DropUdpPackets; - - // check is disconnect required - var disconnectRequired = - (_oldUserSettings.TunnelClientCountry != UserSettings.TunnelClientCountry) || - (_activeClientProfileId != null && UserSettings.ClientProfileId != _activeClientProfileId) || //ClientProfileId has been changed - (_activeServerLocation != state.ClientServerLocationInfo?.ServerLocation) || //ClientProfileId has been changed - (UserSettings.IncludeLocalNetwork != client.IncludeLocalNetwork); // IncludeLocalNetwork has been changed - - // 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(); - _oldUserSettings = VhUtil.JsonClone(UserSettings); + ApplySettings(); } public static string GetCountryName(string countryCode) @@ -612,10 +690,23 @@ public static string GetCountryName(string countryCode) catch { return countryCode; } } - public async Task GetClientCountryCode() + public async Task GetClientCountryCode(CancellationToken cancellationToken) { _isFindingCountryCode = true; + if (_appPersistState.ClientCountryCode == null && _useExternalLocationService) + { + try + { + _appPersistState.ClientCountryCode = await IPAddressUtil.GetCountryCodeByCloudflare(cancellationToken: cancellationToken); + } + catch (Exception ex) + { + ReportError(ex, "Could not get country code from Cloudflare service."); + } + } + + if (_appPersistState.ClientCountryCode == null && _useExternalLocationService) { try @@ -627,7 +718,7 @@ public async Task GetClientCountryCode() } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Could not get country code from IpApi service."); + ReportError(ex, "Could not get country code from IpApi service."); } } @@ -641,7 +732,7 @@ public async Task GetClientCountryCode() } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Could not find country code."); + ReportError(ex, "Could not find country code."); } } @@ -650,69 +741,31 @@ public async Task GetClientCountryCode() return _appPersistState.ClientCountryCode ?? RegionInfo.CurrentRegion.Name; } + public async Task LoadAd(IUiContext uiContext, CancellationToken cancellationToken) + { + if (_internalAdService == null) + throw new Exception("AdService has not been initialized."); + + var countryCode = await GetClientCountryCode(cancellationToken); + await _internalAdService.LoadAd(uiContext, countryCode: countryCode, forceReload: false, cancellationToken); + } + public async Task ShowAd(string sessionId, CancellationToken cancellationToken) { - if (!Services.AdServices.Any()) throw new Exception("AdService has not been initialized."); - var countryCode = await GetClientCountryCode(); + if (_internalAdService == null) + throw new Exception("AdService has not been initialized."); var adData = $"sid:{sessionId};ad:{Guid.NewGuid()}"; - var adServices = Services.AdServices.Where(x => - x.AdType == AppAdType.InterstitialAd && x.IsCountrySupported(countryCode)); - - var noFillAdNetworks = new List(); - foreach (var adService in adServices) + try { - cancellationToken.ThrowIfCancellationRequested(); - - // find first successful ad network - try - { - if (noFillAdNetworks.Contains(adService.NetworkName)) - continue; - - using var timeoutCts = new CancellationTokenSource(_adLoadTimeout); - using var linkedCts = - CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); - await adService.LoadAd(RequiredUiContext, linkedCts.Token).VhConfigureAwait(); - } - catch (UiContextNotAvailableException) - { - throw new ShowAdNoUiException(); - } - catch (NoFillAdException) - { - noFillAdNetworks.Add(adService.NetworkName); - continue; - } - // do not catch if parent cancel the operation - catch (Exception ex) - { - VhLogger.Instance.LogWarning(ex, "Could not load any ad. Network: {Network}.", adService.NetworkName); - continue; - } - - - // show the ad - try - { - await adService.ShowAd(RequiredUiContext, adData, cancellationToken).VhConfigureAwait(); - if (UiContext == null) - throw new ShowAdNoUiException(); - } - catch (Exception ex) - { - if (UiContext == null) - throw new ShowAdNoUiException(); - - // let's treat unknown error same as LoadException in thi version - throw new LoadAdException("Could not show any ad.", ex); - } - + await LoadAd(ActiveUiContext.RequiredContext, cancellationToken); + await _internalAdService.ShowAd(ActiveUiContext.RequiredContext, adData, cancellationToken); return adData; } - - // could not load any ad - throw new LoadAdException($"Could not load any AD. Country: {_appPersistState.ClientCountryName}"); + catch (UiContextNotAvailableException) + { + throw new ShowAdNoUiException(); + } } private void Client_StateChanged(object sender, EventArgs e) @@ -765,7 +818,7 @@ public async Task Disconnect(bool byUser = false) } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Error in disconnecting."); + ReportError(ex, "Error in disconnecting."); } finally { @@ -804,15 +857,18 @@ public void VersionCheckPostpone() _appPersistState.UpdateIgnoreTime = DateTime.Now; } + private readonly AsyncLock _versionCheckLock = new(); public async Task VersionCheck(bool force = false) { + using var lockAsync = await _versionCheckLock.LockAsync().VhConfigureAwait(); if (!force && _appPersistState.UpdateIgnoreTime + _versionCheckInterval > DateTime.Now) return; // check version by app container try { - if (UiContext != null && Services.UpdaterService != null && await Services.UpdaterService.Update(UiContext).VhConfigureAwait()) + if (ActiveUiContext.Context != null && Services.UpdaterService != null && + await Services.UpdaterService.Update(ActiveUiContext.RequiredContext).VhConfigureAwait()) { VersionCheckPostpone(); return; @@ -820,7 +876,7 @@ public async Task VersionCheck(bool force = false) } catch (Exception ex) { - VhLogger.Instance.LogWarning(ex, "Could not check version by VersionCheck."); + ReportWarning(ex, "Could not check version by VersionCheck."); } // check version by UpdateInfoUrl @@ -880,12 +936,12 @@ public async Task VersionCheck(bool force = false) } catch (Exception ex) { - VhLogger.Instance.LogWarning(ex, "Could not retrieve the latest publish info information."); + ReportWarning(ex, "Could not retrieve the latest publish info information."); return null; // could not retrieve the latest publish info. try later } } - public async Task GetIncludeIpRanges(IPAddress clientIp) + public async Task GetIncludeIpRanges(IPAddress clientIp, CancellationToken cancellationToken) { // calculate packetCaptureIpRanges var ipRanges = IpNetwork.All.ToIpRanges(); @@ -898,7 +954,7 @@ public async Task VersionCheck(bool force = false) try { - var lastCountryCode = await GetClientCountryCode(); + var lastCountryCode = await GetClientCountryCode(cancellationToken).VhConfigureAwait(); VhLogger.Instance.LogTrace("Finding Country IPs for split tunneling. LastCountry: {Country}", GetCountryName(lastCountryCode)); _isLoadingIpGroup = true; FireConnectionStateChanged(); @@ -915,7 +971,7 @@ public async Task VersionCheck(bool force = false) } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Could not get ip locations of your country."); + ReportError(ex, "Could not get ip locations of your country."); if (!UserSettings.TunnelClientCountry) { UserSettings.TunnelClientCountry = true; @@ -971,6 +1027,18 @@ public async Task RefreshAccount(bool updateCurrentClientProfile = false) } } + private void ReportError(Exception ex, string message, [CallerMemberName] string action = "n/a") + { + Services.Tracker?.VhTrackErrorAsync(ex, message, action); + VhLogger.Instance.LogError(ex, message); + } + + private void ReportWarning(Exception ex, string message, [CallerMemberName] string action = "n/a") + { + Services.Tracker?.VhTrackWarningAsync(ex, message, action); + VhLogger.Instance.LogWarning(ex, message); + } + public Task RunJob() { return VersionCheck(); @@ -978,7 +1046,7 @@ public Task RunJob() public void UpdateUi() { + ApplySettings(); UiHasChanged?.Invoke(this, EventArgs.Empty); - InitCulture(); } } diff --git a/VpnHood.Client.Device.Android/ActivityEvents/ActivityEvent.cs b/VpnHood.Client.Device.Android/ActivityEvents/ActivityEvent.cs index 63341c68d..9150263b5 100644 --- a/VpnHood.Client.Device.Android/ActivityEvents/ActivityEvent.cs +++ b/VpnHood.Client.Device.Android/ActivityEvents/ActivityEvent.cs @@ -14,6 +14,8 @@ public class ActivityEvent : Activity, IActivityEvent public event EventHandler? RequestPermissionsResultEvent; public event EventHandler? KeyDownEvent; public event EventHandler? ConfigurationChangedEvent; + public event EventHandler? PauseEvent; + public event EventHandler? ResumeEvent; public event EventHandler? DestroyEvent; public Activity Activity => this; @@ -69,6 +71,17 @@ protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result base.OnActivityResult(requestCode, resultCode, data); } + protected override void OnResume() + { + base.OnResume(); + ResumeEvent?.Invoke(this, EventArgs.Empty); + } + + protected override void OnPause() + { + PauseEvent?.Invoke(this, EventArgs.Empty); + base.OnPause(); + } protected override void OnDestroy() { diff --git a/VpnHood.Client.Device.Android/ActivityEvents/IActivityEvent.cs b/VpnHood.Client.Device.Android/ActivityEvents/IActivityEvent.cs index e30b6d7e6..4cb411889 100644 --- a/VpnHood.Client.Device.Android/ActivityEvents/IActivityEvent.cs +++ b/VpnHood.Client.Device.Android/ActivityEvents/IActivityEvent.cs @@ -9,7 +9,9 @@ public interface IActivityEvent event EventHandler NewIntentEvent; event EventHandler RequestPermissionsResultEvent; event EventHandler KeyDownEvent; + event EventHandler? PauseEvent; + event EventHandler? ResumeEvent; event EventHandler DestroyEvent; - public event EventHandler? ConfigurationChangedEvent; + event EventHandler? ConfigurationChangedEvent; Activity Activity { get; } } \ No newline at end of file diff --git a/VpnHood.Client.Device.Android/AndroidDevice.cs b/VpnHood.Client.Device.Android/AndroidDevice.cs index 51a785be0..6d1976b67 100644 --- a/VpnHood.Client.Device.Android/AndroidDevice.cs +++ b/VpnHood.Client.Device.Android/AndroidDevice.cs @@ -13,15 +13,12 @@ namespace VpnHood.Client.Device.Droid; public class AndroidDevice : Singleton, IDevice { private TaskCompletionSource _grantPermissionTaskSource = new(); - private TaskCompletionSource _startServiceTaskSource = new(); - private IPacketCapture? _packetCapture; private const int RequestVpnPermissionId = 20100; private AndroidDeviceNotification? _deviceNotification; public event EventHandler? StartedAsService; public bool IsExcludeAppsSupported => true; public bool IsIncludeAppsSupported => true; - public bool IsLogToConsoleSupported => false; public bool IsAlwaysOnSupported => OperatingSystem.IsAndroidVersionAtLeast(24); public string OsInfo => $"{Build.Manufacturer}: {Build.Model}, Android: {Build.VERSION.Release}"; @@ -122,37 +119,37 @@ public DeviceAppInfo[] InstalledApps private async Task PrepareVpnService(IActivityEvent? activityEvent) { // Grant for permission if OnRequestVpnPermission is registered otherwise let service throw the error + VhLogger.Instance.LogTrace("Preparing VpnService..."); using var prepareIntent = VpnService.Prepare(activityEvent?.Activity ?? Application.Context); - if (prepareIntent != null) + if (prepareIntent == null) + return; // already prepared + + if (activityEvent == null) + throw new Exception("Please open the app and grant VPN permission to proceed."); + + _grantPermissionTaskSource = new TaskCompletionSource(); + activityEvent.ActivityResultEvent += Activity_OnActivityResult; + try { - if (activityEvent == null) - throw new Exception("Please open the app and grant VPN permission to proceed."); + VhLogger.Instance.LogTrace("Requesting user consent..."); + activityEvent.Activity.StartActivityForResult(prepareIntent, RequestVpnPermissionId); + await Task.WhenAny(_grantPermissionTaskSource.Task, Task.Delay(TimeSpan.FromMinutes(2))) + .VhConfigureAwait(); - _grantPermissionTaskSource = new TaskCompletionSource(); - activityEvent.ActivityResultEvent += Activity_OnActivityResult; - try - { - activityEvent.Activity.StartActivityForResult(prepareIntent, RequestVpnPermissionId); - await Task.WhenAny(_grantPermissionTaskSource.Task, Task.Delay(TimeSpan.FromMinutes(2))).VhConfigureAwait(); - if (!_grantPermissionTaskSource.Task.IsCompletedSuccessfully) - throw new Exception("Could not grant VPN permission in the given time."); + if (!_grantPermissionTaskSource.Task.IsCompletedSuccessfully) + throw new Exception("Could not grant VPN permission in the given time."); - if (!_grantPermissionTaskSource.Task.Result) - throw new Exception("VPN permission has been rejected."); - } - finally - { - activityEvent.ActivityResultEvent -= Activity_OnActivityResult; - } + if (!_grantPermissionTaskSource.Task.Result) + throw new Exception("VPN permission has been rejected."); + } + finally + { + activityEvent.ActivityResultEvent -= Activity_OnActivityResult; } } public async Task CreatePacketCapture(IUiContext? uiContext) { - // remove current if still exists - _packetCapture?.Dispose(); - _packetCapture = null; - // prepare vpn service var androidUiContext = (AndroidUiContext?)uiContext; await PrepareVpnService(androidUiContext?.ActivityEvent); @@ -160,6 +157,7 @@ public async Task CreatePacketCapture(IUiContext? uiContext) // start service var intent = new Intent(Application.Context, typeof(AndroidPacketCapture)); intent.PutExtra("manual", true); + AndroidPacketCapture.StartServiceTaskCompletionSource = new TaskCompletionSource(); if (OperatingSystem.IsAndroidVersionAtLeast(26)) { Application.Context.StartForegroundService(intent.SetAction("connect")); @@ -170,21 +168,21 @@ public async Task CreatePacketCapture(IUiContext? uiContext) } // check is service started - _startServiceTaskSource = new TaskCompletionSource(); - await Task.WhenAny(_startServiceTaskSource.Task, Task.Delay(TimeSpan.FromSeconds(10))).VhConfigureAwait(); - if (_packetCapture == null) - throw new Exception("Could not start VpnService in the given time."); - - return _packetCapture; + try + { + var packetCapture = await AndroidPacketCapture.StartServiceTaskCompletionSource.Task.WaitAsync(TimeSpan.FromSeconds(10)); + return packetCapture; + } + catch (Exception ex) + { + AndroidPacketCapture.StartServiceTaskCompletionSource.TrySetCanceled(); + throw new Exception("Could not create VpnService in given time.", ex); + } } internal void OnServiceStartCommand(AndroidPacketCapture packetCapture, Intent? intent) { - _packetCapture = packetCapture; - _packetCapture.Stopped += PacketCapture_Stopped; - _startServiceTaskSource.TrySetResult(true); - // set foreground _deviceNotification ??= CreateDefaultNotification(); packetCapture.StartForeground(_deviceNotification.NotificationId, _deviceNotification.Notification); @@ -204,11 +202,6 @@ internal void OnServiceStartCommand(AndroidPacketCapture packetCapture, Intent? } } - private void PacketCapture_Stopped(object? sender, EventArgs e) - { - _packetCapture = null; - } - private static string EncodeToBase64(Drawable drawable, int quality) { var bitmap = DrawableToBitmap(drawable); @@ -243,4 +236,4 @@ public void Dispose() _deviceNotification?.Notification.Dispose(); DisposeSingleton(); } -} \ No newline at end of file +} diff --git a/VpnHood.Client.Device.Android/AndroidPacketCapture.cs b/VpnHood.Client.Device.Android/AndroidPacketCapture.cs index 317f2aab1..839201b7f 100644 --- a/VpnHood.Client.Device.Android/AndroidPacketCapture.cs +++ b/VpnHood.Client.Device.Android/AndroidPacketCapture.cs @@ -7,18 +7,21 @@ using Android.OS; using Android.Runtime; using Java.IO; +using Java.Net; using Microsoft.Extensions.Logging; using PacketDotNet; using VpnHood.Common.Logging; using VpnHood.Common.Net; using VpnHood.Common.Utils; +using ProtocolType = PacketDotNet.ProtocolType; +using Socket = System.Net.Sockets.Socket; namespace VpnHood.Client.Device.Droid; [Service( - Permission = Manifest.Permission.BindVpnService, - Exported = true, + Permission = Manifest.Permission.BindVpnService, + Exported = true, ForegroundServiceType = ForegroundService.TypeSystemExempted)] [IntentFilter(["android.net.VpnService"])] public class AndroidPacketCapture : VpnService, IPacketCapture @@ -29,10 +32,13 @@ public class AndroidPacketCapture : VpnService, IPacketCapture private ParcelFileDescriptor? _mInterface; private int _mtu; private FileOutputStream? _outStream; // Packets received need to be written to this output stream. + private readonly ConnectivityManager? _connectivityManager = ConnectivityManager.FromContext(Application.Context); + internal static TaskCompletionSource? StartServiceTaskCompletionSource { get; set; } public event EventHandler? PacketReceivedFromInbound; public event EventHandler? Stopped; public bool Started => _mInterface != null; + private bool _isServiceStarted; public IpNetwork[]? IncludeNetworks { get; set; } public bool CanSendPacketToOutbound => false; public bool CanExcludeApps => true; @@ -88,7 +94,7 @@ public void StartCapture() builder.SetMtu(Mtu); // DNS Servers - AddVpnServers(builder); + AddDnsServers(builder); // Routes AddRoutes(builder); @@ -143,24 +149,34 @@ public void ProtectSocket(Socket socket) public void StopCapture() { - if (!Started) - return; - VhLogger.Instance.LogTrace("Stopping VPN Service..."); - CloseVpn(); + StopVpnService(); } - void IDisposable.Dispose() + [return: GeneratedEnum] + public override StartCommandResult OnStartCommand(Intent? intent, + [GeneratedEnum] StartCommandFlags flags, int startId) { - // The parent should not be disposed, never call parent dispose + // close Vpn if it is already started CloseVpn(); - } - [return: GeneratedEnum] - public override StartCommandResult OnStartCommand(Intent? intent, [GeneratedEnum] StartCommandFlags flags, - int startId) - { - AndroidDevice.Instance.OnServiceStartCommand(this, intent); + // post vpn service start command + try + { + AndroidDevice.Instance.OnServiceStartCommand(this, intent); + } + catch (Exception ex) + { + StartServiceTaskCompletionSource?.TrySetException(ex); + StopVpnService(); + return StartCommandResult.NotSticky; + } + + // signal start command + if (intent?.Action == "connect") + StartServiceTaskCompletionSource?.TrySetResult(this); + + _isServiceStarted = true; return StartCommandResult.Sticky; } @@ -171,7 +187,7 @@ private void AddRoutes(Builder builder) builder.AddRoute(network.Prefix.ToString(), network.PrefixLength); } - private void AddVpnServers(Builder builder) + private void AddDnsServers(Builder builder) { var dnsServers = VhUtil.IsNullOrEmpty(DnsServers) ? IPAddressUtil.GoogleDnsServers : DnsServers; if (!AddIpV6Address) @@ -184,7 +200,7 @@ private void AddVpnServers(Builder builder) private void AddAppFilter(Builder builder) { // Applications Filter - if (IncludeApps?.Length > 0) + if (IncludeApps != null) { // make sure to add current app if an allowed app exists var packageName = ApplicationContext?.PackageName ?? @@ -203,7 +219,7 @@ private void AddAppFilter(Builder builder) } } - if (ExcludeApps?.Length > 0) + if (ExcludeApps != null) { var packageName = ApplicationContext?.PackageName ?? throw new Exception("Could not get the app PackageName!"); @@ -228,7 +244,7 @@ private Task ReadingPacketTask() { var buf = new byte[short.MaxValue]; int read; - while ((read = _inStream.Read(buf)) > 0) + while (!_isClosing && (read = _inStream.Read(buf)) > 0) { var packetBuffer = buf[..read]; // copy buffer for packet var ipPacket = Packet.ParsePacket(LinkLayers.Raw, packetBuffer)?.Extract(); @@ -245,17 +261,29 @@ private Task ReadingPacketTask() VhLogger.Instance.LogError(ex, "Error occurred in Android ReadingPacketTask."); } - if (Started) - CloseVpn(); - + StopVpnService(); return Task.FromResult(0); } + public bool? IsInProcessPacket(ProtocolType protocol, IPEndPoint localEndPoint, IPEndPoint remoteEndPoint) + { + var localAddress = new InetSocketAddress(InetAddress.GetByAddress(localEndPoint.Address.GetAddressBytes()), localEndPoint.Port); + var remoteAddress = new InetSocketAddress(InetAddress.GetByAddress(remoteEndPoint.Address.GetAddressBytes()), remoteEndPoint.Port); + + // Android 9 and below + if (!OperatingSystem.IsAndroidVersionAtLeast(29)) + return false; //not supported + + // Android 10 and above + var uid = _connectivityManager?.GetConnectionOwnerUid((int)protocol, localAddress, remoteAddress); + return uid == Process.MyUid(); + } + protected virtual void ProcessPacket(IPPacket ipPacket) { try { - PacketReceivedFromInbound?.Invoke(this, + PacketReceivedFromInbound?.Invoke(this, new PacketReceivedEventArgs([ipPacket], this)); } catch (Exception ex) @@ -268,16 +296,16 @@ protected virtual void ProcessPacket(IPPacket ipPacket) public override void OnDestroy() { VhLogger.Instance.LogTrace("VpnService has been destroyed!"); - CloseVpn(); - - base.OnDestroy(); - - Stopped?.Invoke(this, EventArgs.Empty); + base.OnDestroy(); } + private bool _isClosing; private void CloseVpn() { + if (_mInterface == null || _isClosing) return; + _isClosing = true; + VhLogger.Instance.LogTrace("Closing VpnService..."); // close streams @@ -308,11 +336,28 @@ private void CloseVpn() _mInterface = null; } - StopVpnService(); + try + { + Stopped?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + + VhLogger.Instance.LogError(ex, "Error while invoking Stopped event."); + } + + _isClosing = false; } private void StopVpnService() { + // make sure to close vpn; it has self check + CloseVpn(); + + // close the service + if (!_isServiceStarted) return; + _isServiceStarted = false; + try { // it must be after _mInterface.Close @@ -320,12 +365,25 @@ private void StopVpnService() StopForeground(StopForegroundFlags.Remove); else StopForeground(true); + } + catch (Exception ex) + { + VhLogger.Instance.LogError(ex, "Error in StopForeground of VpnService."); + } + try + { StopSelf(); } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Error while stopping the VpnService."); + VhLogger.Instance.LogError(ex, "Error in StopSelf of VpnService."); } } + + void IDisposable.Dispose() + { + // The parent should not be disposed, never call parent dispose + StopVpnService(); + } } diff --git a/VpnHood.Client.Device.Android/AndroidUiContext.cs b/VpnHood.Client.Device.Android/AndroidUiContext.cs index 07136de04..29fc09b16 100644 --- a/VpnHood.Client.Device.Android/AndroidUiContext.cs +++ b/VpnHood.Client.Device.Android/AndroidUiContext.cs @@ -6,4 +6,4 @@ public class AndroidUiContext(IActivityEvent activityEvent) : IUiContext { public IActivityEvent ActivityEvent => activityEvent; public Activity Activity => activityEvent.Activity; -} +} \ No newline at end of file diff --git a/VpnHood.Client.Device.Android/VpnHood.Client.Device.Android.csproj b/VpnHood.Client.Device.Android/VpnHood.Client.Device.Android.csproj index 2acccce94..1437e30a0 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.5.535 + 4.6.544 $([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 fcd7e7029..e691a74fc 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) diff --git a/VpnHood.Client.Device.WinDivert/WinDivertDevice.cs b/VpnHood.Client.Device.WinDivert/WinDivertDevice.cs index 076ebbac0..fa2c3f20d 100644 --- a/VpnHood.Client.Device.WinDivert/WinDivertDevice.cs +++ b/VpnHood.Client.Device.WinDivert/WinDivertDevice.cs @@ -8,7 +8,6 @@ public class WinDivertDevice : IDevice public string OsInfo => Environment.OSVersion + ", " + (Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit"); public bool IsExcludeAppsSupported => IsDebugMode; - public bool IsLogToConsoleSupported => true; public bool IsAlwaysOnSupported => false; public bool IsIncludeAppsSupported => IsDebugMode; diff --git a/VpnHood.Client.Device.WinDivert/WinDivertPacketCapture.cs b/VpnHood.Client.Device.WinDivert/WinDivertPacketCapture.cs index 5c51131a4..f1f8632c2 100644 --- a/VpnHood.Client.Device.WinDivert/WinDivertPacketCapture.cs +++ b/VpnHood.Client.Device.WinDivert/WinDivertPacketCapture.cs @@ -8,6 +8,7 @@ using SharpPcap.WinDivert; using VpnHood.Common.Logging; using VpnHood.Common.Net; +using ProtocolType = PacketDotNet.ProtocolType; namespace VpnHood.Client.Device.WinDivert; @@ -20,21 +21,10 @@ public class WinDivertPacketCapture : IPacketCapture private bool _disposed; private IpNetwork[]? _includeNetworks; private WinDivertHeader? _lastCaptureHeader; - - public WinDivertPacketCapture() - { - // initialize devices - _device = new SharpPcap.WinDivert.WinDivertDevice { Flags = 0 }; - _device.OnPacketArrival += Device_OnPacketArrival; - - // manage WinDivert file - SetWinDivertDllFolder(); - - } - + + public const short ProtectedTtl = 111; public event EventHandler? PacketReceivedFromInbound; public event EventHandler? Stopped; - public bool Started => _device.Started; public virtual bool CanSendPacketToOutbound => true; @@ -46,12 +36,22 @@ public virtual IPAddress[]? DnsServers set => throw new NotSupportedException(); } - public virtual bool CanProtectSocket => false; + public WinDivertPacketCapture() + { + // initialize devices + _device = new SharpPcap.WinDivert.WinDivertDevice { Flags = 0 }; + _device.OnPacketArrival += Device_OnPacketArrival; + + // manage WinDivert file + SetWinDivertDllFolder(); + + } + + public virtual bool CanProtectSocket => true; public virtual void ProtectSocket(Socket socket) { - throw new NotSupportedException( - $"{nameof(ProtectSocket)} is not supported by {GetType().Name}"); + socket.Ttl = ProtectedTtl; } public void SendPacketToInbound(IList ipPackets) @@ -122,7 +122,7 @@ public void StartCapture() } // add outbound; filter loopback - var filter = $"(ip or ipv6) and outbound and !loopback and (udp.DstPort==53 or ({phraseX}))"; + var filter = $"(ip or ipv6) and outbound and !loopback and ip.TTL!={ProtectedTtl} and (udp.DstPort==53 or ({phraseX}))"; // filter = $"(ip or ipv6) and outbound and !loopback and (protocol!=6 or tcp.DstPort!=3389) and (protocol!=6 or tcp.SrcPort!=3389) and (udp.DstPort==53 or ({phraseX}))"; filter = filter.Replace("ipv6.DstAddr>=::", "ipv6"); // WinDivert bug try @@ -214,8 +214,6 @@ private static void SetWinDivertDllFolder() LoadLibrary(Path.Combine(destinationFolder, "WinDivert.dll")); } - #region Applications Filter - public bool CanExcludeApps => false; public bool CanIncludeApps => false; @@ -246,5 +244,8 @@ public bool AddIpV6Address set => throw new NotSupportedException(); } - #endregion + public bool? IsInProcessPacket(ProtocolType protocol, IPEndPoint localEndPoint, IPEndPoint remoteEndPoint) + { + return null; + } } \ No newline at end of file diff --git a/VpnHood.Client.Device/ActiveUiContext.cs b/VpnHood.Client.Device/ActiveUiContext.cs new file mode 100644 index 000000000..7d344604b --- /dev/null +++ b/VpnHood.Client.Device/ActiveUiContext.cs @@ -0,0 +1,26 @@ +using VpnHood.Client.Device.Exceptions; + +namespace VpnHood.Client.Device; + +public class ActiveUiContext : IUiContext +{ + private static IUiContext? _context; + public static event EventHandler? OnChanged; + + public static IUiContext? Context + { + get => _context; + set + { + if (_context == value) + return; + + _context = value; + OnChanged?.Invoke(null, EventArgs.Empty); + } + } + + public static IUiContext RequiredContext => Context ?? throw new UiContextNotAvailableException(); + + +} \ No newline at end of file diff --git a/VpnHood.Client.Device/IDevice.cs b/VpnHood.Client.Device/IDevice.cs index fa0caaf2a..a5e8d143b 100644 --- a/VpnHood.Client.Device/IDevice.cs +++ b/VpnHood.Client.Device/IDevice.cs @@ -5,7 +5,6 @@ public interface IDevice : IDisposable event EventHandler StartedAsService; bool IsExcludeAppsSupported { get; } bool IsIncludeAppsSupported { get; } - bool IsLogToConsoleSupported { get; } bool IsAlwaysOnSupported { get; } string OsInfo { get; } DeviceAppInfo[] InstalledApps { get; } diff --git a/VpnHood.Client.Device/IPacketCapture.cs b/VpnHood.Client.Device/IPacketCapture.cs index a49b35044..9c99255c9 100644 --- a/VpnHood.Client.Device/IPacketCapture.cs +++ b/VpnHood.Client.Device/IPacketCapture.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Net.Sockets; using PacketDotNet; using VpnHood.Common.Net; @@ -25,9 +24,10 @@ public interface IPacketCapture : IDisposable bool CanSendPacketToOutbound { get; } void StartCapture(); void StopCapture(); - void ProtectSocket(Socket socket); + void ProtectSocket(System.Net.Sockets.Socket socket); void SendPacketToInbound(IPPacket ipPacket); void SendPacketToInbound(IList packets); void SendPacketToOutbound(IPPacket ipPacket); void SendPacketToOutbound(IList ipPackets); + bool? IsInProcessPacket(ProtocolType protocol, IPEndPoint localEndPoint, IPEndPoint remoteEndPoint); } \ No newline at end of file diff --git a/VpnHood.Client.Device/NullPacketCapture.cs b/VpnHood.Client.Device/NullPacketCapture.cs index a9be61fab..dc24c6f4e 100644 --- a/VpnHood.Client.Device/NullPacketCapture.cs +++ b/VpnHood.Client.Device/NullPacketCapture.cs @@ -2,6 +2,7 @@ using System.Net.Sockets; using PacketDotNet; using VpnHood.Common.Net; +using ProtocolType = PacketDotNet.ProtocolType; namespace VpnHood.Client.Device; @@ -10,7 +11,7 @@ public class NullPacketCapture : IPacketCapture public event EventHandler? PacketReceivedFromInbound; public event EventHandler? Stopped; public virtual bool Started { get; set; } - public virtual bool IsDnsServersSupported { get; set; } + public virtual bool IsDnsServersSupported { get; set; } = true; public virtual IPAddress[]? DnsServers { get; set; } public virtual bool CanExcludeApps { get; set; } = true; public virtual bool CanIncludeApps { get; set; } = true; @@ -61,6 +62,11 @@ public virtual void SendPacketToOutbound(IList ipPackets) // nothing } + public bool? IsInProcessPacket(ProtocolType protocol, IPEndPoint localEndPoint, IPEndPoint remoteEndPoint) + { + return null; + } + public virtual void Dispose() { StopCapture(); diff --git a/VpnHood.Client.Device/VpnHood.Client.Device.csproj b/VpnHood.Client.Device/VpnHood.Client.Device.csproj index a9fb543bf..5517be43b 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -32,7 +32,7 @@ - + diff --git a/VpnHood.Client/Abstractions/IIpRangeProvider.cs b/VpnHood.Client/Abstractions/IIpRangeProvider.cs index 100a7c00e..65e690c73 100644 --- a/VpnHood.Client/Abstractions/IIpRangeProvider.cs +++ b/VpnHood.Client/Abstractions/IIpRangeProvider.cs @@ -5,5 +5,5 @@ namespace VpnHood.Client.Abstractions; public interface IIpRangeProvider { - Task GetIncludeIpRanges(IPAddress clientIp); + Task GetIncludeIpRanges(IPAddress clientIp, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/VpnHood.Client/ClientHost.cs b/VpnHood.Client/ClientHost.cs index 0ecd03d1d..0b90f60a3 100644 --- a/VpnHood.Client/ClientHost.cs +++ b/VpnHood.Client/ClientHost.cs @@ -9,7 +9,9 @@ using VpnHood.Tunneling; using VpnHood.Tunneling.Channels; using VpnHood.Tunneling.ClientStreams; +using VpnHood.Tunneling.DomainFiltering; using VpnHood.Tunneling.Messaging; +using VpnHood.Tunneling.Utils; using ProtocolType = PacketDotNet.ProtocolType; namespace VpnHood.Client; @@ -32,6 +34,7 @@ internal class ClientHost( public IPAddress CatcherAddressIpV4 { get; } = catcherAddressIpV4; public IPAddress CatcherAddressIpV6 { get; } = catcherAddressIpV6; + private bool IsIpV6Supported => vpnHoodClient is { IsIpV6SupportedByClient: true, IsIpV6SupportedByServer: true }; public void Start() { @@ -43,7 +46,8 @@ public void Start() _tcpListenerIpV4 = new TcpListener(IPAddress.Any, 0); _tcpListenerIpV4.Start(); _localEndpointIpV4 = (IPEndPoint)_tcpListenerIpV4.LocalEndpoint; //it is slow; make sure to cache it - VhLogger.Instance.LogInformation($"{VhLogger.FormatType(this)} is listening on {VhLogger.Format(_localEndpointIpV4)}"); + VhLogger.Instance.LogInformation( + $"{VhLogger.FormatType(this)} is listening on {VhLogger.Format(_localEndpointIpV4)}"); _ = AcceptTcpClientLoop(_tcpListenerIpV4); // IpV6 @@ -58,7 +62,8 @@ public void Start() } catch (Exception ex) { - VhLogger.Instance.LogError(ex, $"Could not create listener on {VhLogger.Format(new IPEndPoint(IPAddress.IPv6Any, 0))}!"); + VhLogger.Instance.LogError(ex, + $"Could not create listener on {VhLogger.Format(new IPEndPoint(IPAddress.IPv6Any, 0))}!"); } } @@ -90,13 +95,14 @@ private async Task AcceptTcpClientLoop(TcpListener tcpListener) public IPPacket[] ProcessOutgoingPacket(IList ipPackets) { if (_localEndpointIpV4 == null) - throw new InvalidOperationException($"{nameof(_localEndpointIpV4)} has not been initialized! Did you call {nameof(Start)}!"); + throw new InvalidOperationException( + $"{nameof(_localEndpointIpV4)} has not been initialized! Did you call {nameof(Start)}!"); _ipPackets.Clear(); // prevent reallocation in this intensive method var ret = _ipPackets; // ReSharper disable once ForCanBeConvertedToForeach - for (var i=0; i< ipPackets.Count; i++) + for (var i = 0; i < ipPackets.Count; i++) { var ipPacket = ipPackets[i]; var loopbackAddress = ipPacket.Version == IPVersion.IPv4 ? CatcherAddressIpV4 : CatcherAddressIpV6; @@ -105,6 +111,9 @@ public IPPacket[] ProcessOutgoingPacket(IList ipPackets) try { + if (ipPacket.Version == IPVersion.IPv6 && !IsIpV6Supported) + throw new Exception("IPv6 is not supported."); + tcpPacket = PacketUtil.ExtractTcp(ipPacket); // check local endpoint @@ -118,7 +127,8 @@ public IPPacket[] ProcessOutgoingPacket(IList ipPackets) // redirect to inbound if (Equals(ipPacket.DestinationAddress, loopbackAddress)) { - var natItem = (NatItemEx?)vpnHoodClient.Nat.Resolve(ipPacket.Version, ipPacket.Protocol, tcpPacket.DestinationPort) + var natItem = (NatItemEx?)vpnHoodClient.Nat.Resolve(ipPacket.Version, ipPacket.Protocol, + tcpPacket.DestinationPort) ?? throw new Exception("Could not find incoming tcp destination in NAT."); ipPacket.SourceAddress = natItem.DestinationAddress; @@ -133,12 +143,22 @@ public IPPacket[] ProcessOutgoingPacket(IList ipPackets) var sync = tcpPacket is { Synchronize: true, Acknowledgment: false }; var natItem = sync ? vpnHoodClient.Nat.Add(ipPacket, true) - : vpnHoodClient.Nat.Get(ipPacket) ?? throw new Exception("Could not find outgoing tcp destination in NAT."); + : vpnHoodClient.Nat.Get(ipPacket) ?? + throw new Exception("Could not find outgoing tcp destination in NAT."); + + // set isInProcess + if (sync) + { + natItem.IsInProcess = vpnHoodClient.SocketFactory.IsInProcessPacket(ProtocolType.Tcp, + new IPEndPoint(ipPacket.SourceAddress, tcpPacket.SourcePort), + new IPEndPoint(ipPacket.DestinationAddress, tcpPacket.DestinationPort)); + } tcpPacket.SourcePort = natItem.NatId; // 1 ipPacket.DestinationAddress = ipPacket.SourceAddress; // 2 ipPacket.SourceAddress = loopbackAddress; //3 tcpPacket.DestinationPort = (ushort)localEndPoint.Port; //4 + } PacketUtil.UpdateIpPacket(ipPacket); @@ -149,11 +169,14 @@ public IPPacket[] ProcessOutgoingPacket(IList ipPackets) if (tcpPacket != null) { ret.Add(PacketUtil.CreateTcpResetReply(ipPacket, true)); - PacketUtil.LogPacket(ipPacket, "ClientHost: Error in processing packet. Dropping packet and sending TCP rest.", LogLevel.Error, ex); + PacketUtil.LogPacket(ipPacket, + "ClientHost: Error in processing packet. Dropping packet and sending TCP rest.", LogLevel.Error, + ex); } else { - PacketUtil.LogPacket(ipPacket, "ClientHost: Error in processing packet. Dropping packet.", LogLevel.Error, ex); + PacketUtil.LogPacket(ipPacket, "ClientHost: Error in processing packet. Dropping packet.", + LogLevel.Error, ex); } } } @@ -166,6 +189,7 @@ private async Task ProcessClient(TcpClient orgTcpClient, CancellationToken cance if (orgTcpClient is null) throw new ArgumentNullException(nameof(orgTcpClient)); ConnectorRequestResult? requestResult = null; StreamProxyChannel? channel = null; + var ipVersion = IPVersion.IPv4; try { @@ -179,12 +203,14 @@ private async Task ProcessClient(TcpClient orgTcpClient, CancellationToken cance // get original remote from NAT var orgRemoteEndPoint = (IPEndPoint)orgTcpClient.Client.RemoteEndPoint; - var ipVersion = orgRemoteEndPoint.AddressFamily == AddressFamily.InterNetwork + ipVersion = orgRemoteEndPoint.AddressFamily == AddressFamily.InterNetwork ? IPVersion.IPv4 : IPVersion.IPv6; - var natItem = (NatItemEx?)vpnHoodClient.Nat.Resolve(ipVersion, ProtocolType.Tcp, (ushort)orgRemoteEndPoint.Port) - ?? throw new Exception($"Could not resolve original remote from NAT! RemoteEndPoint: {VhLogger.Format(orgTcpClient.Client.RemoteEndPoint)}"); + var natItem = + (NatItemEx?)vpnHoodClient.Nat.Resolve(ipVersion, ProtocolType.Tcp, (ushort)orgRemoteEndPoint.Port) + ?? throw new Exception( + $"Could not resolve original remote from NAT! RemoteEndPoint: {VhLogger.Format(orgTcpClient.Client.RemoteEndPoint)}"); // create a scope for the logger using var scope = VhLogger.Instance.BeginScope("LocalPort: {LocalPort}, RemoteEp: {RemoteEp}", @@ -196,14 +222,29 @@ private async Task ProcessClient(TcpClient orgTcpClient, CancellationToken cance if (!Equals(orgRemoteEndPoint.Address, loopbackAddress)) throw new Exception("TcpProxy rejected an outbound connection!"); - // Check IpFilter - if (!vpnHoodClient.IsInIpRange(natItem.DestinationAddress)) + // Filter by SNI + var filterResult = await vpnHoodClient.DomainFilterService + .Process(orgTcpClient.GetStream(), natItem.DestinationAddress, cancellationToken) + .VhConfigureAwait(); + + if (filterResult.Action == DomainFilterAction.Block) + { + VhLogger.Instance.LogInformation(GeneralEventId.Sni, + "Domain has been blocked. Domain: {Domain}", + VhLogger.FormatHostName(filterResult.DomainName)); + + throw new Exception($"Domain has been blocked. Domain: {filterResult.DomainName}"); + } + + // Filter by IP + if (natItem.IsInProcess == true || filterResult.Action == DomainFilterAction.Exclude || + (!vpnHoodClient.IsInIpRange(natItem.DestinationAddress) && filterResult.Action != DomainFilterAction.Include)) { var channelId = Guid.NewGuid() + ":client"; await vpnHoodClient.AddPassthruTcpStream( - new TcpClientStream(orgTcpClient, orgTcpClient.GetStream(), channelId), - new IPEndPoint(natItem.DestinationAddress, natItem.DestinationPort), - channelId, cancellationToken) + new TcpClientStream(orgTcpClient, orgTcpClient.GetStream(), channelId), + new IPEndPoint(natItem.DestinationAddress, natItem.DestinationPort), + channelId, filterResult.ReadData, cancellationToken) .VhConfigureAwait(); return; } @@ -225,14 +266,22 @@ await vpnHoodClient.AddPassthruTcpStream( // create a StreamProxyChannel VhLogger.Instance.LogTrace(GeneralEventId.StreamProxyChannel, - $"Adding a channel to session {VhLogger.FormatId(request.SessionId)}..."); + "Adding a channel to session. SessionId: {SessionId}...", VhLogger.FormatId(request.SessionId)); var orgTcpClientStream = new TcpClientStream(orgTcpClient, orgTcpClient.GetStream(), request.RequestId + ":host"); + // flush initBuffer + await proxyClientStream.Stream.WriteAsync(filterResult.ReadData, cancellationToken); + + // add stream proxy channel = new StreamProxyChannel(request.RequestId, orgTcpClientStream, proxyClientStream); vpnHoodClient.Tunnel.AddChannel(channel); } catch (Exception ex) { + // disable IPv6 if detect the new network does not have IpV6 + if (ipVersion == IPVersion.IPv6 && ex is SocketException { SocketErrorCode: SocketError.NetworkUnreachable }) + vpnHoodClient.IsIpV6SupportedByClient = false; + if (channel != null) await channel.DisposeAsync().VhConfigureAwait(); if (requestResult != null) await requestResult.DisposeAsync().VhConfigureAwait(); orgTcpClient.Dispose(); @@ -244,6 +293,7 @@ await vpnHoodClient.AddPassthruTcpStream( } } + public ValueTask DisposeAsync() { if (_disposed) return default; @@ -254,4 +304,4 @@ public ValueTask DisposeAsync() return default; } -} +} \ No newline at end of file diff --git a/VpnHood.Client/ClientOptions.cs b/VpnHood.Client/ClientOptions.cs index 09dad840a..c2afcaad5 100644 --- a/VpnHood.Client/ClientOptions.cs +++ b/VpnHood.Client/ClientOptions.cs @@ -1,6 +1,8 @@ using System.Net; +using Ga4.Trackers; using VpnHood.Client.Abstractions; using VpnHood.Common.Net; +using VpnHood.Tunneling.DomainFiltering; using VpnHood.Tunneling.Factory; namespace VpnHood.Client; @@ -40,10 +42,14 @@ public class ClientOptions public TimeSpan MinTcpDatagramTimespan { get; set; } = TimeSpan.FromMinutes(5); public TimeSpan MaxTcpDatagramTimespan { get; set; } = TimeSpan.FromMinutes(10); public bool AllowAnonymousTracker { get; set; } = true; + public bool AllowEndPointTracker { get; set; } public bool DropUdpPackets { get; set; } - public string? AppGa4MeasurementId { get; set; } public string? ServerLocation { get; set; } - + public DomainFilter DomainFilter { get; set; } = new (); + public bool ForceLogSni { get; set; } + public TimeSpan ServerQueryTimeout { get; set; } = TimeSpan.FromSeconds(10); + public ITracker? Tracker { get; set; } + // ReSharper disable StringLiteralTypo public const string SampleAccessKey = "vh://eyJ2Ijo0LCJuYW1lIjoiVnBuSG9vZCBTYW1wbGUiLCJzaWQiOiIxMzAwIiwidGlkIjoiYTM0Mjk4ZDktY2YwYi00MGEwLWI5NmMtZGJhYjYzMWQ2MGVjIiwiaWF0IjoiMjAyNC0wNi0xNFQyMjozMjo1NS44OTQ5ODAyWiIsInNlYyI6Im9wcTJ6M0M0ak9rdHNodXl3c0VKNXc9PSIsInNlciI6eyJjdCI6IjIwMjQtMDYtMDVUMDQ6MTU6MzZaIiwiaG5hbWUiOiJtby5naXdvd3l2eS5uZXQiLCJocG9ydCI6MCwiaXN2IjpmYWxzZSwic2VjIjoidmFCcVU5UkMzUUhhVzR4RjVpYllGdz09IiwiY2giOiIzZ1hPSGU1ZWN1aUM5cStzYk83aGxMb2tRYkE9IiwidXJsIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3Zwbmhvb2QvVnBuSG9vZC5GYXJtS2V5cy9tYWluL0ZyZWVfZW5jcnlwdGVkX3Rva2VuLnR4dCIsImVwIjpbIjUxLjgxLjIxMC4xNjQ6NDQzIiwiWzI2MDQ6MmRjMDoyMDI6MzAwOjo1Y2VdOjQ0MyJdLCJsb2MiOlsiVVMvT3JlZ29uIiwiVVMvVmlyZ2luaWEiXX19"; // ReSharper restore StringLiteralTypo diff --git a/VpnHood.Client/ClientProxyManager.cs b/VpnHood.Client/ClientProxyManager.cs index a614ae98e..e9f16d60f 100644 --- a/VpnHood.Client/ClientProxyManager.cs +++ b/VpnHood.Client/ClientProxyManager.cs @@ -3,6 +3,7 @@ using VpnHood.Common.Logging; using VpnHood.Tunneling; using VpnHood.Tunneling.Factory; +using VpnHood.Tunneling.Utils; namespace VpnHood.Client; diff --git a/VpnHood.Client/ClientSocketFactory.cs b/VpnHood.Client/ClientSocketFactory.cs index 6e113ed23..bf9732e1f 100644 --- a/VpnHood.Client/ClientSocketFactory.cs +++ b/VpnHood.Client/ClientSocketFactory.cs @@ -1,4 +1,5 @@ -using System.Net.Sockets; +using System.Net; +using System.Net.Sockets; using VpnHood.Client.Device; using VpnHood.Common.Utils; using VpnHood.Tunneling.Factory; @@ -37,4 +38,9 @@ public void SetKeepAlive(Socket socket, bool enable) { socketFactory.SetKeepAlive(socket, enable); } + + public bool? IsInProcessPacket(PacketDotNet.ProtocolType protocol, IPEndPoint localEndPoint, IPEndPoint remoteEndPoint) + { + return packetCapture.IsInProcessPacket(protocol, localEndPoint, remoteEndPoint); + } } \ No newline at end of file diff --git a/VpnHood.Client/ClientTrackerBuilder.cs b/VpnHood.Client/ClientTrackerBuilder.cs new file mode 100644 index 000000000..99633eb15 --- /dev/null +++ b/VpnHood.Client/ClientTrackerBuilder.cs @@ -0,0 +1,55 @@ +using Ga4.Trackers; +using System.Net; +using VpnHood.Common.Messaging; +using VpnHood.Common.Net; + +namespace VpnHood.Client; + +public static class ClientTrackerBuilder +{ + public static TrackEvent BuildConnectionAttempt(bool connected, string? serverLocation, bool isIpV6Supported) + { + return new TrackEvent + { + EventName = "vh_connect_attempt", + Parameters = new Dictionary + { + { "server_location", serverLocation ?? "" }, + { "connected", connected.ToString() }, + { "ipv6_supported", isIpV6Supported.ToString() }, + } + }; + } + + public static TrackEvent BuildEndPointStatus(IPEndPoint endPoint, bool available) + { + return new TrackEvent + { + EventName = "vh_endpoint_status", + Parameters = new Dictionary + { + {"ep", endPoint}, + {"ip_v6", endPoint.Address.IsV6()}, + {"available", available} + } + }; + } + + public static TrackEvent BuildUsage(Traffic usage, int requestCount, int connectionCount) + { + var trackEvent = new TrackEvent + { + EventName = "vh_usage", + Parameters = new Dictionary + { + {"traffic_total", Math.Round(usage.Total / 1_000_000d)}, + {"traffic_sent", Math.Round(usage.Sent / 1_000_000d)}, + {"traffic_received", Math.Round(usage.Received / 1_000_000d)}, + {"requests", requestCount}, + {"connections", connectionCount} + } + }; + + return trackEvent; + } +} \ No newline at end of file diff --git a/VpnHood.Client/ClientUsageTracker.cs b/VpnHood.Client/ClientUsageTracker.cs index 99209066d..02de7c75a 100644 --- a/VpnHood.Client/ClientUsageTracker.cs +++ b/VpnHood.Client/ClientUsageTracker.cs @@ -1,4 +1,4 @@ -using Ga4.Ga4Tracking; +using Ga4.Trackers; using VpnHood.Common.Jobs; using VpnHood.Common.Messaging; using VpnHood.Common.Utils; @@ -9,21 +9,18 @@ internal class ClientUsageTracker : IJob, IAsyncDisposable { private readonly AsyncLock _reportLock = new(); private readonly VpnHoodClient.ClientStat _clientStat; - private readonly Ga4Tracker _ga4Tracker; + private readonly ITracker _tracker; private Traffic _lastTraffic = new(); private int _lastRequestCount; private int _lastConnectionCount; private bool _disposed; public JobSection JobSection { get; } = new(TimeSpan.FromMinutes(25)); - public ClientUsageTracker(VpnHoodClient.ClientStat clientStat, Version version, Ga4Tracker ga4Tracker) + public ClientUsageTracker(VpnHoodClient.ClientStat clientStat, ITracker tracker) { _clientStat = clientStat; - _ga4Tracker = ga4Tracker; + _tracker = tracker; JobRunner.Default.Add(this); - - var useProperties = new Dictionary { { "client_version", version.ToString(3) } }; - _ = ga4Tracker.Track(new Ga4TagEvent { EventName = Ga4TagEvents.SessionStart }, useProperties); } public Task RunJob() @@ -43,20 +40,9 @@ public async Task Report() var requestCount = _clientStat.ConnectorStat.RequestCount; var connectionCount = _clientStat.ConnectorStat.CreatedConnectionCount; - var tagEvent = new Ga4TagEvent - { - EventName = "usage", - Properties = new Dictionary - { - {"traffic_total", Math.Round(usage.Total / 1_000_000d)}, - {"traffic_sent", Math.Round(usage.Sent / 1_000_000d)}, - {"traffic_received", Math.Round(usage.Received / 1_000_000d)}, - {"requests", requestCount - _lastRequestCount}, - {"connections", connectionCount - _lastConnectionCount} - } - }; + var trackEvent = ClientTrackerBuilder.BuildUsage(usage, requestCount - _lastRequestCount, connectionCount - _lastConnectionCount); - await _ga4Tracker.Track(tagEvent).VhConfigureAwait(); + await _tracker.Track([trackEvent]).VhConfigureAwait(); _lastTraffic = traffic; _lastRequestCount = requestCount; _lastConnectionCount = connectionCount; diff --git a/VpnHood.Client/Exceptions/RedirectHostException.cs b/VpnHood.Client/Exceptions/RedirectHostException.cs index 4dc7834cb..f8e4005f5 100644 --- a/VpnHood.Client/Exceptions/RedirectHostException.cs +++ b/VpnHood.Client/Exceptions/RedirectHostException.cs @@ -1,17 +1,22 @@ using System.Net; using VpnHood.Common.Exceptions; using VpnHood.Common.Messaging; +using VpnHood.Common.Utils; namespace VpnHood.Client.Exceptions; -public class RedirectHostException : SessionException +public class RedirectHostException(SessionResponse sessionResponse) : SessionException(sessionResponse) { - public RedirectHostException(SessionResponse sessionResponse) - : base(sessionResponse) + public IPEndPoint[] RedirectHostEndPoints { - if (sessionResponse.RedirectHostEndPoint == null) - throw new ArgumentNullException(nameof(sessionResponse.RedirectHostEndPoint)); - } + get + { + if (!VhUtil.IsNullOrEmpty(SessionResponse.RedirectHostEndPoints)) + return SessionResponse.RedirectHostEndPoints; - public IPEndPoint RedirectHostEndPoint => SessionResponse.RedirectHostEndPoint!; + return SessionResponse.RedirectHostEndPoint != null + ? [SessionResponse.RedirectHostEndPoint] + : []; + } + } } \ No newline at end of file diff --git a/VpnHood.Client/Exceptions/UnreachableServer.cs b/VpnHood.Client/Exceptions/UnreachableServer.cs new file mode 100644 index 000000000..c97ca61ce --- /dev/null +++ b/VpnHood.Client/Exceptions/UnreachableServer.cs @@ -0,0 +1,9 @@ +using VpnHood.Common; + +namespace VpnHood.Client.Exceptions; + +public class UnreachableServer(string? serverLocation = null) + : Exception( + ServerLocationInfo.IsAuto(serverLocation) + ? "There is no reachable server at this moment. Please try again later." + : $"There is no reachable server at this moment. Please try again later. Location: {serverLocation}"); diff --git a/VpnHood.Client/ServerFinder.cs b/VpnHood.Client/ServerFinder.cs index a65812216..111b82287 100644 --- a/VpnHood.Client/ServerFinder.cs +++ b/VpnHood.Client/ServerFinder.cs @@ -1,61 +1,161 @@ -using System.Collections.Concurrent; -using System.Net; +using System.Net; +using Ga4.Trackers; using Microsoft.Extensions.Logging; using VpnHood.Client.ConnectorServices; +using VpnHood.Client.Exceptions; using VpnHood.Common; using VpnHood.Common.Exceptions; using VpnHood.Common.Logging; using VpnHood.Common.Messaging; +using VpnHood.Common.Net; using VpnHood.Common.Utils; +using VpnHood.Tunneling; using VpnHood.Tunneling.Factory; using VpnHood.Tunneling.Messaging; namespace VpnHood.Client; -public class ServerFinder(int maxDegreeOfParallelism = 10) +public class ServerFinder( + ISocketFactory socketFactory, + ServerToken serverToken, + string? serverLocation, + TimeSpan serverQueryTimeout, + ITracker? tracker, + int maxDegreeOfParallelism = 10) { - public IReadOnlyDictionary? HostEndPointStatus { get; private set; } + private class HostStatus + { + public required IPEndPoint TcpEndPoint { get; init; } + public bool? Available { get; set; } + } + + private HostStatus[] _hostEndPointStatuses = []; + public bool IncludeIpV6 { get; set; } = true; + public string? ServerLocation => serverLocation; // There are much work to be done here - public async Task FindBestServerAsync(ServerToken serverToken, ISocketFactory socketFactory, CancellationToken cancellationToken) + public async Task FindBestServerAsync(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).VhConfigureAwait(); - return HostEndPointStatus.FirstOrDefault(x=>x.Value).Key; //todo check if it is null + // get all endpoints from serverToken + var hostEndPoints = await ServerTokenHelper.ResolveHostEndPoints(serverToken); + if (!hostEndPoints.Any()) + throw new Exception("Could not find any server endpoint. Please check your access key."); + + // exclude ip v6 if not supported + if (!IncludeIpV6) + hostEndPoints = hostEndPoints.Where(x => !x.Address.IsV6() || x.Address.Equals(IPAddress.IPv6Loopback)).ToArray(); + + // for compatibility don't query server for single endpoint + // todo: does not need on 535 or upper due to ServerStatusRequest + if (hostEndPoints.Count(x => x.Address.IsV4()) == 1) + return hostEndPoints.First(x=>x.Address.IsV4()); + + // randomize endpoint + VhUtil.Shuffle(hostEndPoints); + + // find the best server + _hostEndPointStatuses = await VerifyServersStatus(hostEndPoints, byOrder: false, cancellationToken: cancellationToken); + var res = _hostEndPointStatuses.FirstOrDefault(x => x.Available == true)?.TcpEndPoint; + + _ = TrackEndPointsAvailability([], _hostEndPointStatuses).VhConfigureAwait(); + if (res != null) + return res; + + _ = tracker?.Track(ClientTrackerBuilder.BuildConnectionAttempt(connected: false, serverLocation: ServerLocation, isIpV6Supported: IncludeIpV6)); + throw new UnreachableServer(serverLocation: ServerLocation); + } + + public async Task FindBestServerAsync(IPEndPoint[] hostEndPoints, CancellationToken cancellationToken) + { + if (!hostEndPoints.Any()) + throw new Exception("There is no server endpoint. Please check server configuration."); + + var hostStatuses = hostEndPoints + .Select(x => new HostStatus { TcpEndPoint = x }) + .ToArray(); + + // exclude ip v6 if not supported (IPv6Loopback is for tests) + if (!IncludeIpV6) + hostEndPoints = hostEndPoints.Where(x =>!x.Address.IsV6() || x.Address.Equals(IPAddress.IPv6Loopback)).ToArray(); + + // merge old values + foreach (var hostStatus in hostStatuses) + hostStatus.Available = _hostEndPointStatuses + .FirstOrDefault(x => x.TcpEndPoint.Equals(hostStatus.TcpEndPoint))?.Available; + + var results = await VerifyServersStatus(hostEndPoints, byOrder: true, cancellationToken: cancellationToken); + var res = results.FirstOrDefault(x => x.Available == true)?.TcpEndPoint; + + // track new endpoints availability + _ = TrackEndPointsAvailability(_hostEndPointStatuses, results).VhConfigureAwait(); + if (res != null) + return res; + + _ = tracker?.Track(ClientTrackerBuilder.BuildConnectionAttempt(connected: false, serverLocation: ServerLocation, isIpV6Supported: IncludeIpV6)); + throw new UnreachableServer(serverLocation: ServerLocation); } - private async Task> VerifyServersStatus(IEnumerable connectors, - CancellationToken cancellationToken) + private Task TrackEndPointsAvailability(HostStatus[] oldStatuses, HostStatus[] newStatuses) { - var hostEndPointStatus = new ConcurrentDictionary(); + // find new discovered statuses + var changesStatus = newStatuses + .Where(x => + x.Available != null && + !oldStatuses.Any(y => y.Available == x.Available && y.TcpEndPoint.Equals(x.TcpEndPoint))) + .ToArray(); + + var trackEvents = changesStatus + .Where(x => x.Available != null) + .Select(x => ClientTrackerBuilder.BuildEndPointStatus(x.TcpEndPoint, x.Available!.Value)) + .ToArray(); + + // report endpoints + var endPointReport = string.Join(", ", changesStatus.Select(x => $"{VhLogger.Format(x.TcpEndPoint)} => {x.Available}")); + VhLogger.Instance.LogInformation(GeneralEventId.Session, "HostEndPoints: {EndPoints}", endPointReport); + + return tracker?.Track(trackEvents) ?? Task.CompletedTask; + } + + private async Task VerifyServersStatus(IPEndPoint[] hostEndPoints, bool byOrder, CancellationToken cancellationToken) + { + var hostStatuses = hostEndPoints + .Select(x => new HostStatus { TcpEndPoint = x }) + .ToArray(); try { // check all servers using var cancellationTokenSource = new CancellationTokenSource(); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, cancellationToken); - await VhUtil.ParallelForEachAsync(connectors, async connector => + await VhUtil.ParallelForEachAsync(hostStatuses, async hostStatus => { - var serverStatus = await VerifyServerStatus(connector, linkedCancellationTokenSource.Token).VhConfigureAwait(); - hostEndPointStatus[connector.EndPointInfo.TcpEndPoint] = serverStatus; - if (serverStatus) + var connector = CreateConnector(hostStatus.TcpEndPoint); + + // ReSharper disable once AccessToDisposedClosure + hostStatus.Available = await VerifyServerStatus(connector, linkedCancellationTokenSource.Token).VhConfigureAwait(); + + // ReSharper disable once AccessToDisposedClosure + if (hostStatus.Available == true && !byOrder) linkedCancellationTokenSource.Cancel(); // no need to continue, we find a server + // search by order + if (byOrder) + { + // ReSharper disable once LoopCanBeConvertedToQuery (It can not! [false, false, null, true] is not accepted ) + foreach (var item in hostStatuses) + { + if (item.Available == null) + break; // wait to get the result in order + + if (item.Available.Value) + { + // ReSharper disable once AccessToDisposedClosure + linkedCancellationTokenSource.Cancel(); + break; + } + } + } + }, maxDegreeOfParallelism, linkedCancellationTokenSource.Token).VhConfigureAwait(); } @@ -64,21 +164,20 @@ await VhUtil.ParallelForEachAsync(connectors, async connector => // it means a server has been found } - return hostEndPointStatus; + return hostStatuses; } - 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) + new ServerStatusRequest + { + RequestId = Guid.NewGuid().ToString(), + Message = "Hi, How are you?" + }, + cancellationToken) .VhConfigureAwait(); // this should be already handled by the connector and never happen @@ -87,20 +186,29 @@ private static async Task VerifyServerStatus(ConnectorService connector, C return true; } - catch (UnauthorizedAccessException) - { - throw; - } - catch (SessionException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { - throw; + throw; // query cancelled due to discovery cancellationToken } catch (Exception ex) { - VhLogger.Instance.LogError(ex, "Could not get server status. EndPoint: {EndPoint}", + VhLogger.Instance.LogWarning(ex, "Could not get server status. EndPoint: {EndPoint}", VhLogger.Format(connector.EndPointInfo.TcpEndPoint)); return false; } } + + private ConnectorService CreateConnector(IPEndPoint tcpEndPoint) + { + var endPointInfo = new ConnectorEndPointInfo + { + CertificateHash = serverToken.CertificateHash, + HostName = serverToken.HostName, + TcpEndPoint = tcpEndPoint + }; + var connector = new ConnectorService(endPointInfo, socketFactory, serverQueryTimeout, false); + connector.Init(serverProtocolVersion: 0, tcpRequestTimeout: serverQueryTimeout, serverSecret: serverToken.Secret, tcpReuseTimeout: TimeSpan.Zero); + return connector; + } } \ No newline at end of file diff --git a/VpnHood.Client/VpnHood.Client.csproj b/VpnHood.Client/VpnHood.Client.csproj index c2868bd5a..b030e3737 100644 --- a/VpnHood.Client/VpnHood.Client.csproj +++ b/VpnHood.Client/VpnHood.Client.csproj @@ -19,13 +19,13 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) - + diff --git a/VpnHood.Client/VpnHoodClient.cs b/VpnHood.Client/VpnHoodClient.cs index c54450369..279131ffa 100644 --- a/VpnHood.Client/VpnHoodClient.cs +++ b/VpnHood.Client/VpnHoodClient.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Sockets; -using Ga4.Ga4Tracking; +using Ga4.Trackers; +using Ga4.Trackers.Ga4Tags; using Microsoft.Extensions.Logging; using PacketDotNet; using VpnHood.Client.Abstractions; @@ -13,11 +14,14 @@ using VpnHood.Common.Logging; using VpnHood.Common.Messaging; using VpnHood.Common.Net; +using VpnHood.Common.Trackers; using VpnHood.Common.Utils; using VpnHood.Tunneling; using VpnHood.Tunneling.Channels; using VpnHood.Tunneling.ClientStreams; +using VpnHood.Tunneling.DomainFiltering; using VpnHood.Tunneling.Messaging; +using VpnHood.Tunneling.Utils; using PacketReceivedEventArgs = VpnHood.Client.Device.PacketReceivedEventArgs; using ProtocolType = PacketDotNet.ProtocolType; @@ -40,7 +44,7 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable private readonly TimeSpan _minTcpDatagramLifespan; private readonly TimeSpan _maxTcpDatagramLifespan; private readonly bool _allowAnonymousTracker; - private readonly string? _appGa4MeasurementId; + private readonly ITracker? _usageTracker; private IPAddress[] _dnsServersIpV4 = []; private IPAddress[] _dnsServersIpV6 = []; private IPAddress[] _dnsServers = []; @@ -55,10 +59,9 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable private ConnectorService? _connectorService; private readonly TimeSpan _tcpConnectTimeout; private DateTime? _autoWaitTime; - private readonly string? _serverLocation; + private readonly ServerFinder _serverFinder; private ConnectorService ConnectorService => VhUtil.GetRequiredInstance(_connectorService); private int ProtocolVersion { get; } - internal Nat Nat { get; } internal Tunnel Tunnel { get; } internal ClientSocketFactory SocketFactory { get; } @@ -66,7 +69,8 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable public event EventHandler? StateChanged; public Version? ServerVersion { get; private set; } public IPAddress? PublicAddress { get; private set; } - public bool IsIpV6Supported { get; private set; } + public bool IsIpV6SupportedByServer { get; private set; } + public bool IsIpV6SupportedByClient { get; internal set; } public TimeSpan SessionTimeout { get; set; } public TimeSpan AutoWaitTimeout { get; set; } public TimeSpan ReconnectTimeout { get; set; } @@ -86,7 +90,7 @@ public class VpnHoodClient : IDisposable, IAsyncDisposable 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 DomainFilterService DomainFilterService { get; } public VpnHoodClient(IPacketCapture packetCapture, Guid clientId, Token token, ClientOptions options) { @@ -109,10 +113,14 @@ public VpnHoodClient(IPacketCapture packetCapture, Guid clientId, Token token, C _maxDatagramChannelCount = options.MaxDatagramChannelCount; _proxyManager = new ClientProxyManager(packetCapture, SocketFactory, new ProxyManagerOptions()); _ipRangeProvider = options.IpRangeProvider; - _appGa4MeasurementId = options.AppGa4MeasurementId; + _usageTracker = options.Tracker; _tcpConnectTimeout = options.ConnectTimeout; _useUdpChannel = options.UseUdpChannel; _adProvider = options.AdProvider; + _serverFinder = new ServerFinder(options.SocketFactory, token.ServerToken, + serverLocation: options.ServerLocation, + serverQueryTimeout: options.ServerQueryTimeout, + tracker: options.AllowEndPointTracker ? options.Tracker : null); ReconnectTimeout = options.ReconnectTimeout; AutoWaitTimeout = options.AutoWaitTimeout; @@ -125,7 +133,7 @@ public VpnHoodClient(IPacketCapture packetCapture, Guid clientId, Token token, C IncludeLocalNetwork = options.IncludeLocalNetwork; PacketCaptureIncludeIpRanges = options.PacketCaptureIncludeIpRanges; DropUdpPackets = options.DropUdpPackets; - _serverLocation = options.ServerLocation; + DomainFilterService = new DomainFilterService(options.DomainFilter, options.ForceLogSni); // NAT Nat = new Nat(true); @@ -190,8 +198,8 @@ private void PacketCapture_OnStopped(object sender, EventArgs e) _ = DisposeAsync(false); } - internal async Task AddPassthruTcpStream(IClientStream orgTcpClientStream, IPEndPoint hostEndPoint, string channelId, - CancellationToken cancellationToken) + internal async Task AddPassthruTcpStream(IClientStream orgTcpClientStream, IPEndPoint hostEndPoint, + string channelId, byte[] initBuffer, CancellationToken cancellationToken) { // set timeout using var cancellationTokenSource = new CancellationTokenSource(ConnectorService.RequestTimeout); @@ -206,6 +214,9 @@ internal async Task AddPassthruTcpStream(IClientStream orgTcpClientStream, IPEnd var bypassChannel = new StreamProxyChannel(channelId, orgTcpClientStream, new TcpClientStream(tcpClient, tcpClient.GetStream(), channelId + ":host")); + // flush initBuffer + await tcpClient.GetStream().WriteAsync(initBuffer, linkedCancellationTokenSource.Token); + try { _proxyManager.AddChannel(bypassChannel); } catch { await bypassChannel.DisposeAsync().VhConfigureAwait(); throw; } } @@ -219,12 +230,15 @@ public async Task Connect(CancellationToken cancellationToken = default) if (State != ClientState.None) throw new Exception("Connection is already in progress."); + // report config + IsIpV6SupportedByClient = await IPAddressUtil.IsIpv6Supported(); + _serverFinder.IncludeIpV6 = IsIpV6SupportedByClient; ThreadPool.GetMinThreads(out var workerThreads, out var completionPortThreads); VhLogger.Instance.LogInformation( "UseUdpChannel: {UseUdpChannel}, DropUdpPackets: {DropUdpPackets}, IncludeLocalNetwork: {IncludeLocalNetwork}, " + - "MinWorkerThreads: {WorkerThreads}, CompletionPortThreads: {CompletionPortThreads}", - UseUdpChannel, DropUdpPackets, IncludeLocalNetwork, workerThreads, completionPortThreads); + "MinWorkerThreads: {WorkerThreads}, CompletionPortThreads: {CompletionPortThreads}, ClientIpV6: {ClientIpV6}", + UseUdpChannel, DropUdpPackets, IncludeLocalNetwork, workerThreads, completionPortThreads, IsIpV6SupportedByClient); // report version VhLogger.Instance.LogInformation("ClientVersion: {ClientVersion}, ClientProtocolVersion: {ClientProtocolVersion}, ClientId: {ClientId}", @@ -241,7 +255,7 @@ public async Task Connect(CancellationToken cancellationToken = default) var endPointInfo = new ConnectorEndPointInfo { HostName = Token.ServerToken.HostName, - TcpEndPoint = await ServerTokenHelper.ResolveHostEndPoint(Token.ServerToken).VhConfigureAwait(), + TcpEndPoint = await _serverFinder.FindBestServerAsync(cancellationToken).VhConfigureAwait(), CertificateHash = Token.ServerToken.CertificateHash }; _connectorService = new ConnectorService(endPointInfo, SocketFactory, _tcpConnectTimeout); @@ -341,7 +355,7 @@ private void PacketCapture_OnPacketReceivedFromInbound(object sender, PacketRece var passthruPackets = _sendingPackets.PassthruPackets; var proxyPackets = _sendingPackets.ProxyPackets; var droppedPackets = _sendingPackets.DroppedPackets; - + // ReSharper disable once ForCanBeConvertedToForeach for (var i = 0; i < e.IpPackets.Count; i++) { @@ -355,7 +369,7 @@ private void PacketCapture_OnPacketReceivedFromInbound(object sender, PacketRece if (isDnsPacket) { // Drop IPv6 if not support - if (isIpV6 && !IsIpV6Supported) + if (isIpV6 && !IsIpV6SupportedByServer) { droppedPackets.Add(ipPacket); } @@ -374,7 +388,7 @@ private void PacketCapture_OnPacketReceivedFromInbound(object sender, PacketRece passthruPackets.Add(ipPacket); // Drop IPv6 if not support - else if (isIpV6 && !IsIpV6Supported) + else if (isIpV6 && !IsIpV6SupportedByServer) droppedPackets.Add(ipPacket); // Check IPv6 control message such as Solicitations @@ -396,7 +410,7 @@ private void PacketCapture_OnPacketReceivedFromInbound(object sender, PacketRece else { // Drop IPv6 if not support - if (isIpV6 && !IsIpV6Supported) + if (isIpV6 && !IsIpV6SupportedByServer) droppedPackets.Add(ipPacket); // Check IPv6 control message such as Solicitations @@ -413,7 +427,7 @@ private void PacketCapture_OnPacketReceivedFromInbound(object sender, PacketRece // Udp else if (ipPacket.Protocol == ProtocolType.Udp && !DropUdpPackets) { - if (IsInIpRange(ipPacket.DestinationAddress)) + if (IsInIpRange(ipPacket.DestinationAddress)) //todo add InInProcess tunnelPackets.Add(ipPacket); else proxyPackets.Add(ipPacket); @@ -502,7 +516,7 @@ public bool IsInIpRange(IPAddress ipAddress) isInRange = IncludeIpRanges.IsInRange(ipAddress); // cache the result - // we really don't need to keep that much ips in the cache + // we don't need to keep that much ips in the cache if (_includeIps.Count > 0xFFFF) { VhLogger.Instance.LogInformation("Clearing IP filter cache!"); @@ -641,8 +655,9 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all EncryptedClientId = VhUtil.EncryptClientId(clientInfo.ClientId, Token.Secret), ClientInfo = clientInfo, TokenId = Token.TokenId, - ServerLocation = _serverLocation, - AllowRedirect = allowRedirect + ServerLocation = _serverFinder.ServerLocation, + AllowRedirect = allowRedirect, + IsIpV6Supported = IsIpV6SupportedByClient }; await using var requestResult = await SendRequest(request, cancellationToken).VhConfigureAwait(); @@ -671,31 +686,25 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all // Anonymous server usage tracker if (!string.IsNullOrEmpty(sessionResponse.GaMeasurementId)) { - var ga4Tracking = new Ga4Tracker + var ga4Tracking = new Ga4TagTracker() { SessionCount = 1, MeasurementId = sessionResponse.GaMeasurementId, - ApiSecret = string.Empty, ClientId = ClientId.ToString(), SessionId = SessionId.ToString(), - UserAgent = UserAgent + UserAgent = UserAgent, + UserProperties = new Dictionary { { "client_version", Version.ToString(3) } } }; - var useProperties = new Dictionary { { "client_version", Version.ToString(3) } }; - _ = ga4Tracking.Track(new Ga4TagEvent { EventName = Ga4TagEvents.SessionStart }, useProperties); + _ = ga4Tracking.Track(new Ga4TagEvent { EventName = TrackEventNames.SessionStart }); } // Anonymous app usage tracker - if (!string.IsNullOrEmpty(_appGa4MeasurementId)) - _clientUsageTracker = new ClientUsageTracker(Stat, Version, new Ga4Tracker - { - MeasurementId = _appGa4MeasurementId, - SessionCount = 1, - ApiSecret = string.Empty, - ClientId = ClientId.ToString(), - SessionId = SessionId.ToString(), - UserAgent = UserAgent - }); + if (_usageTracker != null) + { + _ = _usageTracker.Track(ClientTrackerBuilder.BuildConnectionAttempt(connected: true, _serverFinder.ServerLocation, isIpV6Supported: IsIpV6SupportedByClient)); + _clientUsageTracker = new ClientUsageTracker(Stat, _usageTracker); + } } // get session id @@ -707,7 +716,7 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all SessionStatus.SuppressedTo = sessionResponse.SuppressedTo; PublicAddress = sessionResponse.ClientPublicAddress; ServerVersion = Version.Parse(sessionResponse.ServerVersion); - IsIpV6Supported = sessionResponse.IsIpV6Supported; + IsIpV6SupportedByServer = sessionResponse.IsIpV6Supported; Stat.ServerLocationInfo = sessionResponse.ServerLocation != null ? ServerLocationInfo.Parse(sessionResponse.ServerLocation) : null; if (sessionResponse.UdpPort > 0) HostUdpEndPoint = new IPEndPoint(ConnectorService.EndPointInfo.TcpEndPoint.Address, sessionResponse.UdpPort.Value); @@ -721,11 +730,14 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all IncludeIpRanges = IncludeIpRanges.Intersect(sessionResponse.IncludeIpRanges); // Get IncludeIpRange for clientIp - var filterIpRanges = _ipRangeProvider != null ? await _ipRangeProvider.GetIncludeIpRanges(sessionResponse.ClientPublicAddress).VhConfigureAwait() : null; - if (!VhUtil.IsNullOrEmpty(filterIpRanges)) + if (_ipRangeProvider != null) { - filterIpRanges = filterIpRanges.Union(DnsServers.Select((x => new IpRange(x)))); - IncludeIpRanges = IncludeIpRanges.Intersect(filterIpRanges); + var filterIpRanges = await _ipRangeProvider.GetIncludeIpRanges(sessionResponse.ClientPublicAddress, cancellationToken).VhConfigureAwait(); + if (!VhUtil.IsNullOrEmpty(filterIpRanges)) + { + filterIpRanges = filterIpRanges.Union(DnsServers.Select((x => new IpRange(x)))); + IncludeIpRanges = IncludeIpRanges.Intersect(filterIpRanges); + } } // set DNS after setting IpFilters @@ -767,8 +779,8 @@ private async Task ConnectInternal(CancellationToken cancellationToken, bool all } catch (RedirectHostException ex) when (allowRedirect) { - // todo; init new connector - ConnectorService.EndPointInfo.TcpEndPoint = ex.RedirectHostEndPoint; + // todo: init new connector + ConnectorService.EndPointInfo.TcpEndPoint = await _serverFinder.FindBestServerAsync(ex.RedirectHostEndPoints, cancellationToken); await ConnectInternal(cancellationToken, false).VhConfigureAwait(); } } @@ -1002,13 +1014,13 @@ public void Dispose() _ = DisposeAsync(); } - public ValueTask DisposeAsync() { return DisposeAsync(false); } private readonly AsyncLock _disposeLock = new(); + public async ValueTask DisposeAsync(bool waitForBye) { using var lockResult = await _disposeLock.LockAsync().VhConfigureAwait(); diff --git a/VpnHood.Common/Ga4Tracking/Ga4TagEvents.cs b/VpnHood.Common/Ga4Tracking/Ga4TagEvents.cs deleted file mode 100644 index cc0fe88f1..000000000 --- a/VpnHood.Common/Ga4Tracking/Ga4TagEvents.cs +++ /dev/null @@ -1,8 +0,0 @@ -// ReSharper disable once CheckNamespace -namespace Ga4.Ga4Tracking; - -public class Ga4TagEvents -{ - public static string SessionStart => "session_start"; - public static string PageView => "page_view"; -} \ No newline at end of file diff --git a/VpnHood.Common/Ga4Tracking/Ga4TagProperties.cs b/VpnHood.Common/Ga4Tracking/Ga4TagProperties.cs deleted file mode 100644 index ea2a126fc..000000000 --- a/VpnHood.Common/Ga4Tracking/Ga4TagProperties.cs +++ /dev/null @@ -1,8 +0,0 @@ -// ReSharper disable once CheckNamespace -namespace Ga4.Ga4Tracking; - -public class Ga4TagProperties -{ - public static string PageLocation => "page_location"; - public static string PageTitle => "page_title"; -} \ No newline at end of file diff --git a/VpnHood.Common/Logging/SyncLogger.cs b/VpnHood.Common/Logging/SyncLogger.cs index 9dbc6995c..f29d2d808 100644 --- a/VpnHood.Common/Logging/SyncLogger.cs +++ b/VpnHood.Common/Logging/SyncLogger.cs @@ -22,6 +22,15 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except Func formatter) { lock (_lock) - logger.Log(logLevel, eventId, state, exception, formatter); + { + try + { + logger.Log(logLevel, eventId, state, exception, formatter); + } + catch (Exception ex) + { + Console.WriteLine($"Logger error! Could not write into logger. Error: {ex.Message}"); + } + } } } \ No newline at end of file diff --git a/VpnHood.Common/Logging/TextLogger.cs b/VpnHood.Common/Logging/TextLogger.cs index fd80a1511..1542bc6dd 100644 --- a/VpnHood.Common/Logging/TextLogger.cs +++ b/VpnHood.Common/Logging/TextLogger.cs @@ -36,7 +36,7 @@ protected void GetScopeInformation(StringBuilder stringBuilder) { var (builder, length) = state; var first = length == builder.Length; - builder.Append(first ? "=> " : " => ").Append(scope); + builder.Append(first ? "" : " => ").Append(scope); }, (stringBuilder, initialLength)); } @@ -44,16 +44,28 @@ protected string FormatLog(LogLevel logLevel, EventId eventId, TState st Func formatter) { var logBuilder = new StringBuilder(); + var time = DateTime.Now.ToString("HH:mm:ss.ffff"); if (includeScopes) { logBuilder.AppendLine(); - logBuilder.Append($"{logLevel.ToString()[..4]} "); + logBuilder.Append($"{time} | "); + logBuilder.Append(logLevel.ToString()[..4] + " | "); GetScopeInformation(logBuilder); logBuilder.AppendLine(); } + else + logBuilder.Append($"{time} | "); - var message = $"| {DateTime.Now:HH:mm:ss.ffff} | {eventId.Name} | {formatter(state, exception)}"; + // event + if (!string.IsNullOrEmpty(eventId.Name)) + { + logBuilder.Append(eventId.Name); + logBuilder.Append(" | "); + } + + // message + var message = formatter(state, exception); if (exception != null) message += "\r\nException: " + exception; diff --git a/VpnHood.Common/Logging/VhConsoleLogger.cs b/VpnHood.Common/Logging/VhConsoleLogger.cs new file mode 100644 index 000000000..b3b0a349b --- /dev/null +++ b/VpnHood.Common/Logging/VhConsoleLogger.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; + +namespace VpnHood.Common.Logging; + +public class VhConsoleLogger(bool includeScopes = true, bool singleLine = true) : TextLogger(includeScopes) +{ + private static bool? _isColorSupported; + + private static bool IsColorSupported + { + get + { + if (_isColorSupported == null) + { + try + { + _ = Console.ForegroundColor; + _isColorSupported = true; + } + catch + { + _isColorSupported = false; + } + } + + return _isColorSupported.Value; + } + } + + public override void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) + { + var text = FormatLog(logLevel, eventId, state, exception, formatter); + if (singleLine) + text = text.Replace("\n", " ").Replace("\r", "").Trim(); + + if (IsColorSupported) + { + var prevColor = Console.ForegroundColor; + Console.ForegroundColor = GetColor(logLevel); + Console.WriteLine(text); + Console.ForegroundColor = prevColor; + } + else + { + Console.WriteLine(text); + } + + } + + public ConsoleColor GetColor(LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Trace => ConsoleColor.Gray, + LogLevel.Debug => ConsoleColor.Gray, + LogLevel.Information => ConsoleColor.White, + LogLevel.Warning => ConsoleColor.Yellow, + LogLevel.Error => ConsoleColor.Red, + LogLevel.Critical => ConsoleColor.DarkRed, + _ => ConsoleColor.White + }; + } +} \ No newline at end of file diff --git a/VpnHood.Common/Logging/VhLogger.cs b/VpnHood.Common/Logging/VhLogger.cs index 077145c23..09ce8f215 100644 --- a/VpnHood.Common/Logging/VhLogger.cs +++ b/VpnHood.Common/Logging/VhLogger.cs @@ -134,6 +134,7 @@ private static string RedactIpAddress(string text, string keyText) } } + // todo: check this public static bool IsSocketCloseException(Exception ex) { return (ex.InnerException != null && IsSocketCloseException(ex.InnerException)) || @@ -143,7 +144,7 @@ OperationCanceledException or TaskCanceledException or SocketException { - SocketErrorCode: SocketError.ConnectionAborted or + SocketErrorCode: SocketError.ConnectionAborted or SocketError.OperationAborted or SocketError.ConnectionReset or SocketError.ConnectionRefused or diff --git a/VpnHood.Common/Messaging/Traffic.cs b/VpnHood.Common/Messaging/Traffic.cs index 27cc2cb0d..ed86aaf92 100644 --- a/VpnHood.Common/Messaging/Traffic.cs +++ b/VpnHood.Common/Messaging/Traffic.cs @@ -4,9 +4,6 @@ namespace VpnHood.Common.Messaging; public class Traffic : IEquatable, ICloneable { - [Obsolete("Version 2.7.360 or upper")] - public long SentTraffic { set => Sent = value; } - [Obsolete("Version 2.7.360 or upper")] public long ReceivedTraffic { set => Received = value; } public long Sent { get; set; } diff --git a/VpnHood.Common/Net/IPAddressExtensions.cs b/VpnHood.Common/Net/IPAddressExtensions.cs new file mode 100644 index 000000000..4de809533 --- /dev/null +++ b/VpnHood.Common/Net/IPAddressExtensions.cs @@ -0,0 +1,18 @@ +using System.Net; +using System.Net.Sockets; + +namespace VpnHood.Common.Net; + +// ReSharper disable once InconsistentNaming +public static class IPAddressExtensions +{ + public static bool IsV6(this IPAddress ipAddress) + { + return ipAddress.AddressFamily == AddressFamily.InterNetworkV6; + } + + public static bool IsV4(this IPAddress ipAddress) + { + return ipAddress.AddressFamily == AddressFamily.InterNetwork; + } +} \ No newline at end of file diff --git a/VpnHood.Common/Net/IPAddressUtil.cs b/VpnHood.Common/Net/IPAddressUtil.cs index 97c2d9656..e0c76c78a 100644 --- a/VpnHood.Common/Net/IPAddressUtil.cs +++ b/VpnHood.Common/Net/IPAddressUtil.cs @@ -18,6 +18,23 @@ public static class IPAddressUtil IPAddress.Parse("2001:4860:4860::8844") ]; + public static IPAddress[] KidsSafeCloudflareDnsServers { get; } = + [ + IPAddress.Parse("1.1.1.3"), + IPAddress.Parse("1.0.0.3"), + IPAddress.Parse("2606:4700:4700::1113"), + IPAddress.Parse("2606:4700:4700::1003") + ]; + + public static IPAddress[] ReliableDnsServers { get; } = + [ + GoogleDnsServers.First(x=>x.IsV4()), + GoogleDnsServers.First(x=>x.IsV6()), + KidsSafeCloudflareDnsServers.First(x=>x.IsV4()), + KidsSafeCloudflareDnsServers.First(x=>x.IsV6()) + ]; + + public static async Task GetPrivateIpAddresses() { var ret = new List(); @@ -38,27 +55,23 @@ public static async Task IsIpv6Supported() { // it may throw error if IPv6 is not supported before creating task var ping = new Ping(); - var pingTask = ping.SendPingAsync("2001:4860:4860::8888"); - var ping2 = ping.SendPingAsync("2001:4860:4860::8844"); - try - { - if ((await pingTask.VhConfigureAwait()).Status == IPStatus.Success) - return true; - } - catch - { - //ignore - } + var pingTasks = ReliableDnsServers + .Where(x => x.IsV6()) + .Select(x => ping.SendPingAsync(x)); - try + foreach (var pingTask in pingTasks) { - if ((await ping2.VhConfigureAwait()).Status == IPStatus.Success) - return true; - } - catch - { - // ignore + try + { + if ((await pingTask.VhConfigureAwait()).Status == IPStatus.Success) + return true; + } + catch + { + //ignore + } } + } catch { @@ -83,14 +96,17 @@ public static async Task GetPublicIpAddresses() } public static Task GetPrivateIpAddress(AddressFamily addressFamily) + { + using var udpClient = new UdpClient(addressFamily); + return GetPrivateIpAddress(udpClient); + } + + public static Task GetPrivateIpAddress(UdpClient udpClient) { try { - var remoteIp = addressFamily == AddressFamily.InterNetwork - ? IPAddress.Parse("8.8.8.8") - : IPAddress.Parse("2001:4860:4860::8888"); - - using var udpClient = new UdpClient(addressFamily); + var addressFamily = udpClient.Client.AddressFamily; + var remoteIp = KidsSafeCloudflareDnsServers.First(x => x.AddressFamily == addressFamily); udpClient.Connect(remoteIp, 53); var endPoint = (IPEndPoint)udpClient.Client.LocalEndPoint; var ipAddress = endPoint.Address; @@ -104,30 +120,65 @@ public static async Task GetPublicIpAddresses() public static async Task GetPublicIpAddress(AddressFamily addressFamily, TimeSpan? timeout = null) { - try - { - //var url = addressFamily == AddressFamily.InterNetwork - // ? "https://api.ipify.org?format=json" - // : "https://api6.ipify.org?format=json"; - - var url = addressFamily == AddressFamily.InterNetwork - ? "https://api4.my-ip.io/v2/ip.json" - : "https://api6.my-ip.io/v2/ip.json"; - - var handler = new HttpClientHandler { AllowAutoRedirect = true }; - using var httpClient = new HttpClient(handler); - - httpClient.Timeout = timeout ?? TimeSpan.FromSeconds(5); - var json = await httpClient.GetStringAsync(url).VhConfigureAwait(); - var document = JsonDocument.Parse(json); - var ipString = document.RootElement.GetProperty("ip").GetString(); - var ipAddress = IPAddress.Parse(ipString ?? throw new InvalidOperationException()); - return ipAddress.AddressFamily == addressFamily ? ipAddress : null; - } - catch - { - return null; - } + try { return await GetPublicIpAddressByCloudflare(addressFamily, timeout); } + catch { /* continue next service */ } + + try { return await GetPublicIpAddressByIpify(addressFamily, timeout); } + catch { /* ignore */ } + + return null; + } + + private static async Task GetPublicIpAddressByCloudflare(AddressFamily addressFamily, TimeSpan? timeout = null) + { + var url = addressFamily == AddressFamily.InterNetwork + ? "https://1.1.1.1/cdn-cgi/trace" + : "https://[2606:4700:4700::1111]/cdn-cgi/trace"; + + using var httpClient = new HttpClient(); + httpClient.Timeout = timeout ?? TimeSpan.FromSeconds(5); + var content = await httpClient.GetStringAsync(url).VhConfigureAwait(); + + // Split the response into lines + var lines = content.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + var ipLine = lines.SingleOrDefault(x => x.StartsWith("ip=", StringComparison.OrdinalIgnoreCase)); + return ipLine != null ? IPAddress.Parse(ipLine.Split('=')[1]) : null; + } + + public static async Task GetCountryCodeByCloudflare(TimeSpan? timeout = default, CancellationToken cancellationToken = default) + { + const string url = "https://cloudflare.com/cdn-cgi/trace"; + + using var httpClient = new HttpClient(); + httpClient.Timeout = timeout ?? TimeSpan.FromSeconds(5); + var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, url), cancellationToken); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + + // Split the response into lines + var lines = content.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + var ipLine = lines.SingleOrDefault(x => x.StartsWith("loc=", StringComparison.OrdinalIgnoreCase)); + return ipLine?.Split('=')[1]; + } + + private static async Task GetPublicIpAddressByIpify(AddressFamily addressFamily, TimeSpan? timeout = null) + { + //var url = addressFamily == AddressFamily.InterNetwork + // ? "https://api.ipify.org?format=json" + // : "https://api6.ipify.org?format=json"; + + var url = addressFamily == AddressFamily.InterNetwork + ? "https://api4.my-ip.io/v2/ip.json" + : "https://api6.my-ip.io/v2/ip.json"; + + var handler = new HttpClientHandler { AllowAutoRedirect = true }; + using var httpClient = new HttpClient(handler); + httpClient.Timeout = timeout ?? TimeSpan.FromSeconds(5); + var json = await httpClient.GetStringAsync(url).VhConfigureAwait(); + var document = JsonDocument.Parse(json); + var ipString = document.RootElement.GetProperty("ip").GetString(); + var ipAddress = IPAddress.Parse(ipString ?? throw new InvalidOperationException()); + return ipAddress.AddressFamily == addressFamily ? ipAddress : null; } public static IPAddress GetAnyIpAddress(AddressFamily addressFamily) diff --git a/VpnHood.Common/ServerLocationInfo.cs b/VpnHood.Common/ServerLocationInfo.cs index 7dcdddb93..d52fa1d53 100644 --- a/VpnHood.Common/ServerLocationInfo.cs +++ b/VpnHood.Common/ServerLocationInfo.cs @@ -45,6 +45,18 @@ public static ServerLocationInfo Parse(string value) return ret; } + public static ServerLocationInfo? TryParse(string value) + { + try + { + return Parse(value); + } + catch + { + return null; + } + } + public bool IsMatch(ServerLocationInfo serverLocationInfo) { return @@ -76,4 +88,10 @@ private static string GetCountryName(string countryCode) } } + public static bool IsAuto(string? serverLocation) + { + return + string.IsNullOrEmpty(serverLocation) || + TryParse(serverLocation)?.Equals(Auto) == true; + } } \ No newline at end of file diff --git a/VpnHood.Common/Ga4Tracking/Ga4MeasurementEvent.cs b/VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementEvent.cs similarity index 79% rename from VpnHood.Common/Ga4Tracking/Ga4MeasurementEvent.cs rename to VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementEvent.cs index 4e0727cdc..1d443fb09 100644 --- a/VpnHood.Common/Ga4Tracking/Ga4MeasurementEvent.cs +++ b/VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementEvent.cs @@ -1,12 +1,12 @@ using System.Text.Json.Serialization; // ReSharper disable once CheckNamespace -namespace Ga4.Ga4Tracking; +namespace Ga4.Trackers.Ga4Measurements; public class Ga4MeasurementEvent : ICloneable { [JsonPropertyName("name")] - public required string Name { get; init; } + public required string EventName { get; init; } [JsonPropertyName("params")] public Dictionary Parameters { get; set; } = new(); @@ -15,7 +15,7 @@ public object Clone() { var ret = new Ga4MeasurementEvent { - Name = Name, + EventName = EventName, Parameters = new Dictionary(Parameters, StringComparer.OrdinalIgnoreCase) }; diff --git a/VpnHood.Common/Ga4Tracking/Ga4MeasurementPayload.cs b/VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementPayload.cs similarity index 84% rename from VpnHood.Common/Ga4Tracking/Ga4MeasurementPayload.cs rename to VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementPayload.cs index 5d9314e4f..569288dc2 100644 --- a/VpnHood.Common/Ga4Tracking/Ga4MeasurementPayload.cs +++ b/VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementPayload.cs @@ -1,10 +1,8 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; // ReSharper disable once CheckNamespace -namespace Ga4.Ga4Tracking; +namespace Ga4.Trackers.Ga4Measurements; -[SuppressMessage("ReSharper", "UnusedMember.Global")] internal class Ga4MeasurementPayload { public class UserProperty diff --git a/VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementTracker.cs b/VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementTracker.cs new file mode 100644 index 000000000..ecb1e2dce --- /dev/null +++ b/VpnHood.Common/Trackers/Ga4Measurements/Ga4MeasurementTracker.cs @@ -0,0 +1,60 @@ +// ReSharper disable once CheckNamespace +namespace Ga4.Trackers.Ga4Measurements; + +public class Ga4MeasurementTracker : TrackerBase, IGa4MeasurementTracker +{ + public required string ApiSecret { get; init; } + public bool IsDebugEndPoint { get; set; } + + public Task Track(Ga4MeasurementEvent ga4Event) + { + var tracks = new[] { ga4Event }; + return Track(tracks); + } + + public Task Track(IEnumerable ga4Events) + { + if (!IsEnabled) + return Task.CompletedTask; + + var gaEventArray = ga4Events.Select(x => (Ga4MeasurementEvent)x.Clone()).ToArray(); + if (!gaEventArray.Any()) + throw new ArgumentException("Events can not be empty! ", nameof(ga4Events)); + + // updating events by default values + foreach (var ga4Event in gaEventArray) + { + if (IsAdminDebugView && !ga4Event.Parameters.TryGetValue("debug_mode", out _)) + ga4Event.Parameters.Add("debug_mode", 1); + + if (!string.IsNullOrEmpty(SessionId) && !ga4Event.Parameters.TryGetValue("session_id", out _)) + ga4Event.Parameters.Add("session_id", SessionId); + } + + + var ga4Payload = new Ga4MeasurementPayload + { + ClientId = ClientId, + UserId = UserId, + Events = gaEventArray, + UserProperties = UserProperties.Any() ? UserProperties.ToDictionary(p => p.Key, p => new Ga4MeasurementPayload.UserProperty { Value = p.Value }) : null + }; + + var baseUri = IsDebugEndPoint ? new Uri("https://www.google-analytics.com/debug/mp/collect") : new Uri("https://www.google-analytics.com/mp/collect"); + var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(baseUri, $"?api_secret={ApiSecret}&measurement_id={MeasurementId}")); + PrepareHttpHeaders(requestMessage.Headers); + return SendHttpRequest(requestMessage, "Measurement", ga4Payload); + } + + public override Task Track(IEnumerable trackEvents) + { + var ga4MeasurementEvents = trackEvents.Select(x => + new Ga4MeasurementEvent + { + EventName = x.EventName, + Parameters = x.Parameters + }); + + return Track(ga4MeasurementEvents); + } +} \ No newline at end of file diff --git a/VpnHood.Common/Trackers/Ga4Measurements/IGa4MeasurementTracker.cs b/VpnHood.Common/Trackers/Ga4Measurements/IGa4MeasurementTracker.cs new file mode 100644 index 000000000..8e068ca1f --- /dev/null +++ b/VpnHood.Common/Trackers/Ga4Measurements/IGa4MeasurementTracker.cs @@ -0,0 +1,7 @@ +// ReSharper disable once CheckNamespace +namespace Ga4.Trackers.Ga4Measurements; + +public interface IGa4MeasurementTracker : ITracker +{ + public Task Track(IEnumerable ga4Events); +} \ No newline at end of file diff --git a/VpnHood.Common/Ga4Tracking/Ga4TagEvent.cs b/VpnHood.Common/Trackers/Ga4Tags/Ga4TagEvent.cs similarity index 83% rename from VpnHood.Common/Ga4Tracking/Ga4TagEvent.cs rename to VpnHood.Common/Trackers/Ga4Tags/Ga4TagEvent.cs index 052dbf5c2..27db902e8 100644 --- a/VpnHood.Common/Ga4Tracking/Ga4TagEvent.cs +++ b/VpnHood.Common/Trackers/Ga4Tags/Ga4TagEvent.cs @@ -1,7 +1,5 @@ - - -// ReSharper disable once CheckNamespace -namespace Ga4.Ga4Tracking; +// ReSharper disable once CheckNamespace +namespace Ga4.Trackers.Ga4Tags; public class Ga4TagEvent { diff --git a/VpnHood.Common/Ga4Tracking/Ga4Tracker.cs b/VpnHood.Common/Trackers/Ga4Tags/Ga4TagTracker.cs similarity index 55% rename from VpnHood.Common/Ga4Tracking/Ga4Tracker.cs rename to VpnHood.Common/Trackers/Ga4Tags/Ga4TagTracker.cs index 05cc3fb8b..28d485e03 100644 --- a/VpnHood.Common/Ga4Tracking/Ga4Tracker.cs +++ b/VpnHood.Common/Trackers/Ga4Tags/Ga4TagTracker.cs @@ -1,73 +1,21 @@ using System.Globalization; -using System.Net.Http.Headers; using System.Runtime.InteropServices; -using System.Text.Json; using System.Text.RegularExpressions; -using VpnHood.Common.Utils; // ReSharper disable once CheckNamespace -namespace Ga4.Ga4Tracking; +namespace Ga4.Trackers.Ga4Tags; -public class Ga4Tracker +public class Ga4TagTracker : TrackerBase, IGa4TagTracker { - private static readonly Lazy HttpClientLazy = new(() => new HttpClient()); - private static HttpClient HttpClient => HttpClientLazy.Value; - - public required string MeasurementId { get; init; } - public required string ApiSecret { get; init; } - public required string SessionId { get; set; } public required int SessionCount { get; set; } = 1; - public string UserAgent { get; set; } = Environment.OSVersion.ToString().Replace(" ", ""); - public required string ClientId { get; init; } - public string? UserId { get; init; } - public bool IsEnabled { get; set; } = true; - public bool IsDebugEndPoint { get; set; } - public bool IsAdminDebugView { get; set; } - public bool IsLogEnabled => IsDebugEndPoint || IsAdminDebugView; - public bool? IsMobile { get; init; } // GTag only - - - // private static string GetPlatform() - // { - // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - // return "Windows"; - // - // if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - // return "Linux"; - // - // if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - // return "macOS"; - // - // return "Unknown"; - // } - - private void PrepareHttpHeaders(HttpHeaders httpHeaders) - { - httpHeaders.Add("User-Agent", UserAgent); - //requestMessage.Headers.Add("Sec-Ch-Ua", "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Microsoft Edge\";v=\"114\""); - //httpHeaders.Add("Sec-Ch-Ua-Mobile", "1"); - //httpHeaders.Add("Sec-Ch-Ua-Platform", "\"Android\""); - } + public bool? IsMobile { get; init; } - private static bool CheckIsMobileByUserAgent(string? userAgent) + public Task Track(Ga4TagEvent ga4Event) { - if (string.IsNullOrEmpty(userAgent)) - return false; - - // check isMobile from agent - var mobileRegex = new Regex( - @"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", - RegexOptions.IgnoreCase); + if (!IsEnabled) + return Task.CompletedTask; - var isMobile = !string.IsNullOrEmpty(userAgent) && mobileRegex.IsMatch(userAgent); - return isMobile; - } - - public Task Track(Ga4TagEvent ga4Event, Dictionary? userProperties = null) - { - if (!IsEnabled) return Task.CompletedTask; var isMobile = IsMobile ?? CheckIsMobileByUserAgent(UserAgent); - userProperties ??= new Dictionary(); // ReSharper disable StringLiteralTypo var parameters = new List<(string, object)> @@ -115,7 +63,7 @@ public Task Track(Ga4TagEvent ga4Event, Dictionary? userProperti if (UserId != null) parameters.Add(("uid", UserId)); if (ga4Event.DocumentLocation != null) parameters.Add(("dl", ga4Event.DocumentLocation)); // Document Location, Actual page's Pathname. It does not include the hostname, query String or Fragment. document.location.pathname. eg: "https://localhost" if (ga4Event.DocumentTitle != null) parameters.Add(("dt", ga4Event.DocumentTitle)); // Document Title, Actual page's Title, document.title. eg: "My page title" - if (ga4Event.DocumentReferrer!= null) parameters.Add(("dr", ga4Event.DocumentReferrer)); // Document Referrer, Actual page's Referrer, document.referrer. eg: "https://www.google.com" + if (ga4Event.DocumentReferrer != null) parameters.Add(("dr", ga4Event.DocumentReferrer)); // Document Referrer, Actual page's Referrer, document.referrer. eg: "https://www.google.com" if (ga4Event.IsFirstVisit) parameters.Add(("_fv", 1)); // first visit, if the "_ga_THYNGSTER" cookie is not set, the first event will have this value present. This will internally create a new "first_visit" event on GA4. If this event is also a conversion the value will be "2" if not, will be "1" // It's the total engagement time in milliseconds since the last event. @@ -125,7 +73,7 @@ public Task Track(Ga4TagEvent ga4Event, Dictionary? userProperti if (IsAdminDebugView) parameters.Add(("_dbg", 1)); // Analytics debug view // add user Properties - foreach (var userProperty in userProperties) + foreach (var userProperty in UserProperties) { var propName = userProperty.Value is int or long ? "upn" : "up"; propName += "." + userProperty.Key; @@ -143,96 +91,56 @@ public Task Track(Ga4TagEvent ga4Event, Dictionary? userProperti var url = new Uri( "https://www.google-analytics.com/g/collect?" + string.Join('&', parameters.Select(x => $"{x.Item1}={x.Item2}")) - ); + ); var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); PrepareHttpHeaders(requestMessage.Headers); return SendHttpRequest(requestMessage, "GTag"); } - public Task Track(Ga4MeasurementEvent ga4Event) + public async Task Track(IEnumerable ga4Events) { - var tracks = new[] { ga4Event }; - return Track(tracks); + foreach (var ga4TagEvent in ga4Events) + await Track(ga4TagEvent).ConfigureAwait(false); } - public Task Track(IEnumerable ga4Events, Dictionary? userProperties = null) + public override Task Track(IEnumerable trackEvents) { - if (!IsEnabled) return Task.CompletedTask; - var gaEventArray = ga4Events.Select(x => (Ga4MeasurementEvent)x.Clone()).ToArray(); - if (!gaEventArray.Any()) throw new ArgumentException("Events can not be empty! ", nameof(ga4Events)); - - // updating events by default values - foreach (var ga4Event in gaEventArray) - { - if (IsAdminDebugView && !ga4Event.Parameters.TryGetValue("debug_mode", out _)) - ga4Event.Parameters.Add("debug_mode", 1); - - if (!string.IsNullOrEmpty(SessionId) && !ga4Event.Parameters.TryGetValue("session_id", out _)) - ga4Event.Parameters.Add("session_id", SessionId); - } - - - var ga4Payload = new Ga4MeasurementPayload - { - ClientId = ClientId, - UserId = UserId, - Events = gaEventArray, - UserProperties = userProperties != null && userProperties.Any() ? userProperties.ToDictionary(p => p.Key, p => new Ga4MeasurementPayload.UserProperty { Value = p.Value }) : null - }; - - var baseUri = IsDebugEndPoint ? new Uri("https://www.google-analytics.com/debug/mp/collect") : new Uri("https://www.google-analytics.com/mp/collect"); - var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(baseUri, $"?api_secret={ApiSecret}&measurement_id={MeasurementId}")); - PrepareHttpHeaders(requestMessage.Headers); - return SendHttpRequest(requestMessage, "Measurement", ga4Payload); + var ga4TagEvents = trackEvents.Select(x => + new Ga4TagEvent + { + EventName = x.EventName, + Properties = x.Parameters + }); + return Track(ga4TagEvents); } - public Task TrackErrorByTag(string action, string msg) + private static bool CheckIsMobileByUserAgent(string? userAgent) { - return Track(new Ga4TagEvent - { - EventName = "exception", - Properties = new Dictionary - { - {"page_location", "ex/" + action}, - {"page_title", msg} - } - }); - } + if (string.IsNullOrEmpty(userAgent)) + return false; - private async Task SendHttpRequest(HttpRequestMessage requestMessage, string name, object? jsonData = null) - { - try - { - if (IsLogEnabled) - { - await Console.Out.WriteLineAsync($"* Sending {name}...").VhConfigureAwait(); - await Console.Out.WriteLineAsync($"Url: {requestMessage.RequestUri}").VhConfigureAwait(); - await Console.Out.WriteLineAsync( - $"Headers: {JsonSerializer.Serialize(requestMessage.Headers, new JsonSerializerOptions { WriteIndented = true })}").VhConfigureAwait(); - } + // check isMobile from agent + var mobileRegex = new Regex( + @"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", + RegexOptions.IgnoreCase); - if (jsonData != null) - { - requestMessage.Content = new StringContent(JsonSerializer.Serialize(jsonData)); - requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - await Console.Out.WriteLineAsync( - $"Data: {JsonSerializer.Serialize(jsonData, new JsonSerializerOptions { WriteIndented = true })}").VhConfigureAwait(); - } - - var res = await HttpClient.SendAsync(requestMessage).VhConfigureAwait(); - if (IsLogEnabled) - { - await Console.Out.WriteLineAsync("Result: ").VhConfigureAwait(); - await Console.Out.WriteLineAsync(await res.Content.ReadAsStringAsync()).VhConfigureAwait(); - } - } - catch - { - if (IsDebugEndPoint) - throw; - } + var isMobile = !string.IsNullOrEmpty(userAgent) && mobileRegex.IsMatch(userAgent); + return isMobile; } + // private static string GetPlatform() + // { + // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + // return "Windows"; + // + // if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + // return "Linux"; + // + // if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // return "macOS"; + // + // return "Unknown"; + // } } \ No newline at end of file diff --git a/VpnHood.Common/Trackers/Ga4Tags/IGa4TagTracker.cs b/VpnHood.Common/Trackers/Ga4Tags/IGa4TagTracker.cs new file mode 100644 index 000000000..a601cf9dd --- /dev/null +++ b/VpnHood.Common/Trackers/Ga4Tags/IGa4TagTracker.cs @@ -0,0 +1,7 @@ +// ReSharper disable once CheckNamespace +namespace Ga4.Trackers.Ga4Tags; + +public interface IGa4TagTracker : ITracker +{ + public Task Track(Ga4TagEvent ga4Event); +} \ No newline at end of file diff --git a/VpnHood.Common/Trackers/ITracker.cs b/VpnHood.Common/Trackers/ITracker.cs new file mode 100644 index 000000000..f465cd1ad --- /dev/null +++ b/VpnHood.Common/Trackers/ITracker.cs @@ -0,0 +1,9 @@ +// ReSharper disable once CheckNamespace +namespace Ga4.Trackers; + +public interface ITracker +{ + bool IsEnabled { get; set; } + Task Track(IEnumerable trackEvents); + Task Track(TrackEvent trackEvent); +} \ No newline at end of file diff --git a/VpnHood.Common/Trackers/TrackEvent.cs b/VpnHood.Common/Trackers/TrackEvent.cs new file mode 100644 index 000000000..84be7e4e8 --- /dev/null +++ b/VpnHood.Common/Trackers/TrackEvent.cs @@ -0,0 +1,8 @@ +// ReSharper disable once CheckNamespace +namespace Ga4.Trackers; + +public class TrackEvent +{ + public required string EventName { get; init; } + public Dictionary Parameters { get; init; } = new(); +} \ No newline at end of file diff --git a/VpnHood.Common/Trackers/TrackEventNames.cs b/VpnHood.Common/Trackers/TrackEventNames.cs new file mode 100644 index 000000000..e6fc1fdc3 --- /dev/null +++ b/VpnHood.Common/Trackers/TrackEventNames.cs @@ -0,0 +1,8 @@ +// ReSharper disable once CheckNamespace +namespace VpnHood.Common.Trackers; + +public class TrackEventNames +{ + public const string SessionStart = "session_start"; + public const string PageView = "page_view"; +} \ No newline at end of file diff --git a/VpnHood.Common/Trackers/TrackParameterNames.cs b/VpnHood.Common/Trackers/TrackParameterNames.cs new file mode 100644 index 000000000..0ad29b7b0 --- /dev/null +++ b/VpnHood.Common/Trackers/TrackParameterNames.cs @@ -0,0 +1,8 @@ +// ReSharper disable once CheckNamespace +namespace VpnHood.Common.Trackers; + +public class TrackParameterNames +{ + public const string PageLocation = "page_location"; + public const string PageTitle = "page_title"; +} \ No newline at end of file diff --git a/VpnHood.Common/Trackers/TrackerBase.cs b/VpnHood.Common/Trackers/TrackerBase.cs new file mode 100644 index 000000000..8f49a8775 --- /dev/null +++ b/VpnHood.Common/Trackers/TrackerBase.cs @@ -0,0 +1,101 @@ +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; +using System.Text.Json; + +// ReSharper disable once CheckNamespace +namespace Ga4.Trackers; + +public abstract class TrackerBase : ITracker +{ + private static readonly Lazy HttpClientLazy = new(() => new HttpClient()); + private static HttpClient HttpClient => HttpClientLazy.Value; + public required string MeasurementId { get; init; } + public required string SessionId { get; set; } + public required string ClientId { get; init; } + public string? UserId { get; init; } + public string UserAgent { get; set; } = Environment.OSVersion.ToString().Replace(" ", ""); + public bool IsEnabled { get; set; } = true; + public bool IsAdminDebugView { get; set; } + public ILogger? Logger { get; set; } + public EventId LoggerEventId { get; set; } = new(0, "Ga4Tracker"); + public bool ThrowExceptionOnError { get; set; } + public Dictionary UserProperties { get; set; } = new(); + + public abstract Task Track(IEnumerable trackEvents); + public Task Track(TrackEvent trackEvent) => Track([trackEvent]); + + public Task TrackError(string action, Exception ex) + { + var trackEvent = new TrackEvent + { + EventName = "exception", + Parameters = new Dictionary + { + { "page_location", "ex/" + action }, + { "page_title", ex.Message } + } + }; + + return Track([trackEvent]); + } + + protected void PrepareHttpHeaders(HttpHeaders httpHeaders) + { + httpHeaders.Add("User-Agent", UserAgent); + //requestMessage.Headers.Add("Sec-Ch-Ua", "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Microsoft Edge\";v=\"114\""); + //httpHeaders.Add("Sec-Ch-Ua-Mobile", "1"); + //httpHeaders.Add("Sec-Ch-Ua-Platform", "\"Android\""); + } + + protected async Task SendHttpRequest(HttpRequestMessage requestMessage, string name, object? jsonData = null) + { + if (!IsEnabled) + return; + + try + { + // log + Logger?.LogInformation(LoggerEventId, + "Sending Ga4Track: {name}, Url: {Url}, Headers: {Headers}", + name, requestMessage.RequestUri, JsonSerializer.Serialize(requestMessage.Headers, new JsonSerializerOptions { WriteIndented = true })); + + if (jsonData != null) + { + requestMessage.Content = new StringContent(JsonSerializer.Serialize(jsonData)); + requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + // log + var data = JsonSerializer.Serialize(jsonData, new JsonSerializerOptions { WriteIndented = true }); + Logger?.LogInformation(LoggerEventId, "Ga4Track Data: {Data}", data); + } + + var res = await HttpClient.SendAsync(requestMessage).ConfigureAwait(false); + + // log + var result = await res.Content.ReadAsStringAsync().ConfigureAwait(false); + Logger?.LogInformation(LoggerEventId, "Ga4Track Result: {Result}", result); + } + catch (Exception ex) + { + Logger?.LogError(LoggerEventId, ex, "Ga4Track could not send its track"); + if (ThrowExceptionOnError) + throw; + } + } + + public void UseSimpleLogger(bool singleLine = false) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddSimpleConsole(configure => + { + // ReSharper disable once StringLiteralTypo + configure.TimestampFormat = "[HH:mm:ss.ffff] "; + configure.IncludeScopes = false; + configure.SingleLine = singleLine; + }); + }); + + Logger = loggerFactory.CreateLogger(""); + } +} diff --git a/VpnHood.Common/Utils/VhTaskExtensions.cs b/VpnHood.Common/Utils/VhTaskExtensions.cs index 11b2d23cd..c847911d4 100644 --- a/VpnHood.Common/Utils/VhTaskExtensions.cs +++ b/VpnHood.Common/Utils/VhTaskExtensions.cs @@ -1,25 +1,27 @@ -namespace VpnHood.Common.Utils; +using System.Runtime.CompilerServices; + +namespace VpnHood.Common.Utils; public static class VhTaskExtensions { - public static async Task VhConfigureAwait(this Task task) + public static ConfiguredTaskAwaitable VhConfigureAwait(this Task task) { - await task.ConfigureAwait(false); + return task.ConfigureAwait(false); } - public static async Task VhConfigureAwait(this Task task) + public static ConfiguredTaskAwaitable VhConfigureAwait(this Task task) { - return await task.ConfigureAwait(false); + return task.ConfigureAwait(false); } - public static async ValueTask VhConfigureAwait(this ValueTask task) + public static ConfiguredValueTaskAwaitable VhConfigureAwait(this ValueTask task) { - await task.ConfigureAwait(false); + return task.ConfigureAwait(false); } - public static async ValueTask VhConfigureAwait(this ValueTask task) + public static ConfiguredValueTaskAwaitable VhConfigureAwait(this ValueTask task) { - return await task.ConfigureAwait(false); + return task.ConfigureAwait(false); } } \ No newline at end of file diff --git a/VpnHood.Common/Utils/VhTrackerExtensions.cs b/VpnHood.Common/Utils/VhTrackerExtensions.cs new file mode 100644 index 000000000..c05ed685b --- /dev/null +++ b/VpnHood.Common/Utils/VhTrackerExtensions.cs @@ -0,0 +1,43 @@ +using Ga4.Trackers; +using Microsoft.Extensions.Logging; +using VpnHood.Common.Logging; + +namespace VpnHood.Common.Utils; + +public static class VhTrackerExtensions +{ + public static Task VhTrackErrorAsync(this ITracker tracker, Exception exception, string message, string action) + { + return VhTrackErrorAsync(tracker, exception, message, action, false); + } + + public static Task VhTrackWarningAsync(this ITracker tracker, Exception exception, string message, string action) + { + return VhTrackErrorAsync(tracker, exception, message, action, true); + } + + + private static async Task VhTrackErrorAsync(this ITracker tracker, Exception exception, string message, string action, bool isWarning) + { + try + { + var trackEvent = new TrackEvent + { + EventName = "vh_exception", + Parameters = new Dictionary + { + { "method", action }, + { "message", message + ", " + exception.Message }, + { "error_type", exception.GetType().Name }, + { "error_level", isWarning ? "warning" : "error" } + } + }; + + await tracker.Track([trackEvent]); + } + catch (Exception ex) + { + VhLogger.Instance.LogError(ex, "Could not error to anonymous tracker."); + } + } +} \ No newline at end of file diff --git a/VpnHood.Common/Utils/VhUtil.cs b/VpnHood.Common/Utils/VhUtil.cs index 79e8a9512..e056122e3 100644 --- a/VpnHood.Common/Utils/VhUtil.cs +++ b/VpnHood.Common/Utils/VhUtil.cs @@ -439,4 +439,15 @@ public static bool TryDeleteFile(string filePath) return false; } } + + public static void Shuffle(T[] array) + { + var rng = new Random(); + var n = array.Length; + for (var i = n - 1; i > 0; i--) + { + var j = rng.Next(i + 1); + (array[i], array[j]) = (array[j], array[i]); + } + } } \ No newline at end of file diff --git a/VpnHood.Common/VpnHood.Common.csproj b/VpnHood.Common/VpnHood.Common.csproj index 55188c76d..16b66aabe 100644 --- a/VpnHood.Common/VpnHood.Common.csproj +++ b/VpnHood.Common/VpnHood.Common.csproj @@ -19,13 +19,13 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) - + diff --git a/VpnHood.Server.Access/Messaging/SessionRequestEx.cs b/VpnHood.Server.Access/Messaging/SessionRequestEx.cs index a4b5ce22a..dd0cc31a4 100644 --- a/VpnHood.Server.Access/Messaging/SessionRequestEx.cs +++ b/VpnHood.Server.Access/Messaging/SessionRequestEx.cs @@ -20,4 +20,5 @@ public class SessionRequestEx public required string? ExtraData { get; set; } public string? ServerLocation { get; set; } public bool AllowRedirect { get; set; } = true; + public bool? IsIpV6Supported { 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 0352393e6..42bfea051 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -31,7 +31,7 @@ - + diff --git a/VpnHood.Server.App.Net/ServerApp.cs b/VpnHood.Server.App.Net/ServerApp.cs index d4db42fb7..128f8cbdc 100644 --- a/VpnHood.Server.App.Net/ServerApp.cs +++ b/VpnHood.Server.App.Net/ServerApp.cs @@ -1,6 +1,7 @@ using System.Net; using System.Runtime.InteropServices; -using Ga4.Ga4Tracking; +using Ga4.Trackers; +using Ga4.Trackers.Ga4Tags; using McMaster.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; using NLog; @@ -26,7 +27,7 @@ public class ServerApp : IDisposable private const string FileNameAppCommand = "appcommand"; private const string FolderNameStorage = "storage"; private const string FolderNameInternal = "internal"; - private readonly Ga4Tracker _gaTracker; + private readonly ITracker _tracker; private readonly CommandListener _commandListener; private VpnHoodServer? _vpnHoodServer; private FileStream? _lockStream; @@ -73,23 +74,26 @@ public ServerApp() _commandListener = new CommandListener(Path.Combine(storagePath, FileNameAppCommand)); _commandListener.CommandReceived += CommandListener_CommandReceived; + // create access server + AccessManager = AppSettings.HttpAccessManager != null + ? CreateHttpAccessManager(AppSettings.HttpAccessManager) + : CreateFileAccessManager(StoragePath, AppSettings.FileAccessManager); + // tracker var anonyClientId = GetServerId(Path.Combine(InternalStoragePath, "server-id")).ToString(); - _gaTracker = new Ga4Tracker + _tracker = new Ga4TagTracker { // ReSharper disable once StringLiteralTypo MeasurementId = "G-9SWLGEX6BT", SessionCount = 1, - ApiSecret = string.Empty, ClientId = anonyClientId, SessionId = Guid.NewGuid().ToString(), - IsEnabled = AppSettings.AllowAnonymousTracker + IsEnabled = AppSettings.AllowAnonymousTracker, + UserProperties = new Dictionary + { + { "server_version", VpnHoodServer.ServerVersion }, + { "access_manager", AccessManager.GetType().Name } } }; - - // create access server - AccessManager = AppSettings.HttpAccessManager != null - ? CreateHttpAccessManager(AppSettings.HttpAccessManager) - : CreateFileAccessManager(StoragePath, AppSettings.FileAccessManager); } private void InitFileLogger(string storagePath) @@ -122,7 +126,6 @@ private void CurrentDomain_ProcessExit(object? sender, EventArgs e) _vpnHoodServer.Dispose(); } } - public static Guid GetServerId(string serverIdFile) { if (File.Exists(serverIdFile) && Guid.TryParse(File.ReadAllText(serverIdFile), out var serverId)) @@ -236,7 +239,7 @@ private void StartServer(CommandLineApplication cmdApp) // run server _vpnHoodServer = new VpnHoodServer(AccessManager, new ServerOptions { - GaTracker = _gaTracker, + Tracker = _tracker, SystemInfoProvider = systemInfoProvider, StoragePath = InternalStoragePath, Config = AppSettings.ServerConfig diff --git a/VpnHood.Server.App.Net/VpnHood.Server.App.Net.csproj b/VpnHood.Server.App.Net/VpnHood.Server.App.Net.csproj index b75d1b30e..6b8f93823 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.5.522 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) VpnHoodServer diff --git a/VpnHood.Server/ServerHost.cs b/VpnHood.Server/ServerHost.cs index 4b0fa190b..678e63905 100644 --- a/VpnHood.Server/ServerHost.cs +++ b/VpnHood.Server/ServerHost.cs @@ -20,7 +20,7 @@ namespace VpnHood.Server; -internal class ServerHost : IAsyncDisposable, IJob +public class ServerHost : IAsyncDisposable, IJob { private readonly HashSet _clientStreams = []; private const int ServerProtocolVersion = 5; @@ -37,6 +37,7 @@ internal class ServerHost : IAsyncDisposable, IJob public IpRange[]? NetFilterIncludeIpRanges { get; set; } public IPAddress[]? DnsServers { get; set; } public CertificateHostName[] Certificates { get; private set; } = []; + public IPEndPoint[] TcpEndPoints => _tcpListeners.Select(x => (IPEndPoint)x.LocalEndpoint).ToArray(); public ServerHost(SessionManager sessionManager) { @@ -561,6 +562,7 @@ private async Task ProcessHello(IClientStream clientStream, CancellationToken ca AccessUsage = sessionResponse.AccessUsage, SuppressedBy = sessionResponse.SuppressedBy, RedirectHostEndPoint = sessionResponse.RedirectHostEndPoint, + RedirectHostEndPoints = sessionResponse.RedirectHostEndPoints, SessionId = sessionResponse.SessionId, SessionKey = sessionResponse.SessionKey, ServerSecret = _sessionManager.ServerSecret, diff --git a/VpnHood.Server/ServerOptions.cs b/VpnHood.Server/ServerOptions.cs index 6af00fe02..7883a5dd2 100644 --- a/VpnHood.Server/ServerOptions.cs +++ b/VpnHood.Server/ServerOptions.cs @@ -1,4 +1,4 @@ -using Ga4.Ga4Tracking; +using Ga4.Trackers; using VpnHood.Server.Access.Configurations; using VpnHood.Server.SystemInformation; using VpnHood.Tunneling.Factory; @@ -8,7 +8,7 @@ namespace VpnHood.Server; public class ServerOptions { public SocketFactory SocketFactory { get; init; } = new(); - public Ga4Tracker? GaTracker { get; init; } + public ITracker? Tracker { get; init; } public ISystemInfoProvider? SystemInfoProvider { get; init; } public INetFilter NetFilter { get; init; } = new NetFilter(); public bool AutoDisposeAccessManager { get; init; } = true; diff --git a/VpnHood.Server/Session.cs b/VpnHood.Server/Session.cs index 112cdef16..5db5174da 100644 --- a/VpnHood.Server/Session.cs +++ b/VpnHood.Server/Session.cs @@ -16,6 +16,7 @@ using VpnHood.Tunneling.ClientStreams; using VpnHood.Tunneling.Factory; using VpnHood.Tunneling.Messaging; +using VpnHood.Tunneling.Utils; using ProtocolType = PacketDotNet.ProtocolType; namespace VpnHood.Server; diff --git a/VpnHood.Server/SessionManager.cs b/VpnHood.Server/SessionManager.cs index 7f18c0f9c..3071f0a41 100644 --- a/VpnHood.Server/SessionManager.cs +++ b/VpnHood.Server/SessionManager.cs @@ -1,11 +1,12 @@ using System.Collections.Concurrent; using System.Text.Json; -using Ga4.Ga4Tracking; +using Ga4.Trackers; using Microsoft.Extensions.Logging; using VpnHood.Common.Jobs; using VpnHood.Common.Logging; using VpnHood.Common.Messaging; using VpnHood.Common.Net; +using VpnHood.Common.Trackers; using VpnHood.Common.Utils; using VpnHood.Server.Access.Configurations; using VpnHood.Server.Access.Managers; @@ -32,7 +33,7 @@ public class SessionManager : IAsyncDisposable, IJob public ConcurrentDictionary Sessions { get; } = new(); public TrackingOptions TrackingOptions { get; set; } = new(); public SessionOptions SessionOptions { get; set; } = new(); - public Ga4Tracker? GaTracker { get; } + public ITracker? Tracker { get; } public byte[] ServerSecret { @@ -47,14 +48,14 @@ public byte[] ServerSecret internal SessionManager(IAccessManager accessManager, INetFilter netFilter, SocketFactory socketFactory, - Ga4Tracker? gaTracker, + ITracker? tracker, Version serverVersion, SessionManagerOptions options) { _accessManager = accessManager ?? throw new ArgumentNullException(nameof(accessManager)); _socketFactory = socketFactory ?? throw new ArgumentNullException(nameof(socketFactory)); _serverSecret = VhUtil.GenerateKey(128); - GaTracker = gaTracker; + Tracker = tracker; ApiKey = HttpUtil.GetApiKey(_serverSecret, TunnelDefaults.HttpPassCheck); NetFilter = netFilter; ServerVersion = serverVersion; @@ -122,7 +123,8 @@ public async Task CreateSession(HelloRequest helloRequest, IP EncryptedClientId = helloRequest.EncryptedClientId, TokenId = helloRequest.TokenId, ServerLocation = helloRequest.ServerLocation, - AllowRedirect = helloRequest.AllowRedirect + AllowRedirect = helloRequest.AllowRedirect, + IsIpV6Supported = helloRequest.IsIpV6Supported }).VhConfigureAwait(); // Access Error should not pass to the client in create session @@ -146,22 +148,22 @@ public async Task CreateSession(HelloRequest helloRequest, IP private Task GaTrackNewSession(ClientInfo clientInfo) { - if (GaTracker == null) + if (Tracker == null) return Task.CompletedTask; // track new session var serverVersion = ServerVersion.ToString(3); - return GaTracker.Track(new Ga4TagEvent + return Tracker.Track([new TrackEvent { - EventName = Ga4TagEvents.PageView, - Properties = new Dictionary + EventName = TrackEventNames.PageView, + Parameters = new Dictionary { { "client_version", clientInfo.ClientVersion }, { "server_version", serverVersion }, - { Ga4TagProperties.PageTitle, $"server_version/{serverVersion}" }, - { Ga4TagProperties.PageLocation, $"server_version/{serverVersion}" } + { TrackParameterNames.PageTitle, $"server_version/{serverVersion}" }, + { TrackParameterNames.PageLocation, $"server_version/{serverVersion}" } } - }); + }]); } private async Task RecoverSession(RequestBase sessionRequest, IPEndPointPair ipEndPointPair) @@ -265,13 +267,13 @@ public async Task RunJob() private Task SendHeartbeat() { - if (GaTracker == null) + if (Tracker == null) return Task.CompletedTask; - return GaTracker.Track(new Ga4TagEvent + return Tracker.Track(new TrackEvent { EventName = "heartbeat", - Properties = new Dictionary + Parameters = new Dictionary { { "session_count", Sessions.Count(x => !x.Value.IsDisposed) } } diff --git a/VpnHood.Server/VpnHood.Server.csproj b/VpnHood.Server/VpnHood.Server.csproj index 40826701d..bef05cc85 100644 --- a/VpnHood.Server/VpnHood.Server.csproj +++ b/VpnHood.Server/VpnHood.Server.csproj @@ -19,14 +19,14 @@ VpnHood.png https://github.com/vpnhood/vpnhood https://github.com/vpnhood/vpnhood - 4.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) - + diff --git a/VpnHood.Server/VpnHoodServer.cs b/VpnHood.Server/VpnHoodServer.cs index a6d1b8f8a..22941a60d 100644 --- a/VpnHood.Server/VpnHoodServer.cs +++ b/VpnHood.Server/VpnHoodServer.cs @@ -3,7 +3,7 @@ using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Text.Json; -using Ga4.Ga4Tracking; +using Ga4.Trackers; using Microsoft.Extensions.Logging; using VpnHood.Common.ApiClients; using VpnHood.Common.Exceptions; @@ -11,6 +11,7 @@ using VpnHood.Common.Logging; using VpnHood.Common.Messaging; using VpnHood.Common.Net; +using VpnHood.Common.Trackers; using VpnHood.Common.Utils; using VpnHood.Server.Access; using VpnHood.Server.Access.Configurations; @@ -24,7 +25,6 @@ namespace VpnHood.Server; public class VpnHoodServer : IAsyncDisposable, IJob { private readonly bool _autoDisposeAccessManager; - private readonly ServerHost _serverHost; private readonly string _lastConfigFilePath; private bool _disposed; private ApiError? _lastConfigError; @@ -35,6 +35,7 @@ public class VpnHoodServer : IAsyncDisposable, IJob private Task _sendStatusTask = Task.CompletedTask; private Http01ChallengeService? _http01ChallengeService; + public ServerHost ServerHost { get; } public JobSection JobSection { get; } public static Version ServerVersion => typeof(VpnHoodServer).Assembly.GetName().Version; public SessionManager SessionManager { get; } @@ -53,15 +54,15 @@ public VpnHoodServer(IAccessManager accessManager, ServerOptions options) SessionManager = new SessionManager(accessManager, options.NetFilter, options.SocketFactory, - options.GaTracker, + options.Tracker, ServerVersion, - new SessionManagerOptions{ CleanupInterval = options.CleanupInterval }); + new SessionManagerOptions { CleanupInterval = options.CleanupInterval }); _autoDisposeAccessManager = options.AutoDisposeAccessManager; _lastConfigFilePath = Path.Combine(options.StoragePath, "last-config.json"); _publicIpDiscovery = options.PublicIpDiscovery; _config = options.Config; - _serverHost = new ServerHost(SessionManager); + ServerHost = new ServerHost(SessionManager); VhLogger.TcpCloseEventId = GeneralEventId.TcpLife; JobRunner.Default.Add(this); @@ -99,6 +100,7 @@ public async Task Start() // Report current OS Version VhLogger.Instance.LogInformation("Module: {Module}", GetType().Assembly.GetName().FullName); VhLogger.Instance.LogInformation("OS: {OS}", SystemInfoProvider.GetSystemInfo()); + VhLogger.Instance.LogInformation("IsDiagnoseMode: {IsDiagnoseMode}", VhLogger.IsDiagnoseMode); // Report TcpBuffers var tcpClient = new TcpClient(); @@ -141,7 +143,7 @@ private async Task Configure() FreeUdpPortV6 = freeUdpPortV6 }; - var publicIpV4 = serverInfo.PublicIpAddresses.SingleOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork); + var publicIpV4 = serverInfo.PublicIpAddresses.SingleOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork); var publicIpV6 = serverInfo.PublicIpAddresses.SingleOrDefault(x => x.AddressFamily == AddressFamily.InterNetworkV6); var isIpV6Supported = publicIpV6 != null || await IPAddressUtil.IsIpv6Supported().VhConfigureAwait(); VhLogger.Instance.LogInformation("Public IPv4: {IPv4}, Public IPv6: {IpV6}, IsV6Supported: {IsV6Supported}", @@ -161,11 +163,11 @@ private async Task Configure() .Concat(serverInfo.PrivateIpAddresses) .Concat(serverConfig.TcpEndPoints?.Select(x => x.Address) ?? Array.Empty()); - ConfigNetFilter(SessionManager.NetFilter, _serverHost, serverConfig.NetFilterOptions, + ConfigNetFilter(SessionManager.NetFilter, ServerHost, serverConfig.NetFilterOptions, privateAddresses: allServerIps, isIpV6Supported, dnsServers: serverConfig.DnsServersValue); // Reconfigure server host - await _serverHost.Configure( + await ServerHost.Configure( serverConfig.TcpEndPointsValue, serverConfig.UdpEndPointsValue, serverConfig.DnsServersValue, serverConfig.Certificates.Select(x => new X509Certificate2(x.RawData)) .ToArray()).VhConfigureAwait(); @@ -190,7 +192,7 @@ await _serverHost.Configure( if (ex is SocketException socketException) _lastConfigError.Data.Add("SocketErrorCode", socketException.SocketErrorCode.ToString()); - _ = SessionManager.GaTracker?.TrackErrorByTag("configure", ex.Message); + SessionManager.Tracker?.VhTrackErrorAsync(ex, "Could not configure server!", "Configure"); VhLogger.Instance.LogError(ex, "Could not configure server! Retrying after {TotalSeconds} seconds.", JobSection.Interval.TotalSeconds); await SendStatusToAccessManager(false).VhConfigureAwait(); } @@ -353,24 +355,18 @@ private async Task SendStatusToAccessManager(bool allowConfigure) private Task GaTrackStart() { - if (SessionManager.GaTracker == null) + if (SessionManager.Tracker == null) return Task.CompletedTask; // track - var useProperties = new Dictionary + return SessionManager.Tracker.Track(new TrackEvent { - { "server_version", ServerVersion }, - { "access_manager", AccessManager.GetType().Name } - }; - - return SessionManager.GaTracker.Track(new Ga4TagEvent - { - EventName = Ga4TagEvents.SessionStart, - Properties = new Dictionary + EventName = TrackEventNames.SessionStart, + Parameters = new Dictionary { { "access_manager", AccessManager.GetType().Name } } - }, useProperties); + }); } public void Dispose() @@ -394,7 +390,7 @@ public async ValueTask DisposeAsync() // wait for configuration try { await _configureTask.VhConfigureAwait(); } catch {/* no error */ } try { await _sendStatusTask.VhConfigureAwait(); } catch {/* no error*/ } - await _serverHost.DisposeAsync().VhConfigureAwait(); // before disposing session manager to prevent recovering sessions + await ServerHost.DisposeAsync().VhConfigureAwait(); // before disposing session manager to prevent recovering sessions await SessionManager.DisposeAsync().VhConfigureAwait(); _http01ChallengeService?.Dispose(); diff --git a/VpnHood.Tunneling/Channels/UdpChannel.cs b/VpnHood.Tunneling/Channels/UdpChannel.cs index a3eed4cd4..41554e574 100644 --- a/VpnHood.Tunneling/Channels/UdpChannel.cs +++ b/VpnHood.Tunneling/Channels/UdpChannel.cs @@ -5,6 +5,7 @@ using VpnHood.Common.Logging; using VpnHood.Common.Messaging; using VpnHood.Common.Utils; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling.Channels; diff --git a/VpnHood.Tunneling/DatagramMessaging/DatagramMessageHandler.cs b/VpnHood.Tunneling/DatagramMessaging/DatagramMessageHandler.cs index f063fca1b..16311a40a 100644 --- a/VpnHood.Tunneling/DatagramMessaging/DatagramMessageHandler.cs +++ b/VpnHood.Tunneling/DatagramMessaging/DatagramMessageHandler.cs @@ -1,5 +1,6 @@ using System.Net; using PacketDotNet; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling.DatagramMessaging; diff --git a/VpnHood.Tunneling/DomainFiltering/DomainFilter.cs b/VpnHood.Tunneling/DomainFiltering/DomainFilter.cs new file mode 100644 index 000000000..64a5b74d3 --- /dev/null +++ b/VpnHood.Tunneling/DomainFiltering/DomainFilter.cs @@ -0,0 +1,8 @@ +namespace VpnHood.Tunneling.DomainFiltering; + +public class DomainFilter +{ + public string[] Blocks { get; set; } = []; + public string[] Excludes { get; set; } = []; + public string[] Includes { get; set; } = []; +} \ No newline at end of file diff --git a/VpnHood.Tunneling/DomainFiltering/DomainFilterAction.cs b/VpnHood.Tunneling/DomainFiltering/DomainFilterAction.cs new file mode 100644 index 000000000..3d1020dda --- /dev/null +++ b/VpnHood.Tunneling/DomainFiltering/DomainFilterAction.cs @@ -0,0 +1,9 @@ +namespace VpnHood.Tunneling.DomainFiltering; + +public enum DomainFilterAction +{ + None, + Block, + Exclude, + Include +} \ No newline at end of file diff --git a/VpnHood.Tunneling/DomainFiltering/DomainFilterResult.cs b/VpnHood.Tunneling/DomainFiltering/DomainFilterResult.cs new file mode 100644 index 000000000..7cf745e2d --- /dev/null +++ b/VpnHood.Tunneling/DomainFiltering/DomainFilterResult.cs @@ -0,0 +1,8 @@ +namespace VpnHood.Tunneling.DomainFiltering; + +public class DomainFilterResult +{ + public DomainFilterAction Action { get; init; } + public string? DomainName { get; init; } + public byte[] ReadData { get; init; } = []; +} \ No newline at end of file diff --git a/VpnHood.Tunneling/DomainFiltering/DomainFilterService.cs b/VpnHood.Tunneling/DomainFiltering/DomainFilterService.cs new file mode 100644 index 000000000..7de6fdae0 --- /dev/null +++ b/VpnHood.Tunneling/DomainFiltering/DomainFilterService.cs @@ -0,0 +1,90 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using VpnHood.Common.Logging; +using VpnHood.Common.Utils; +using VpnHood.Tunneling.Utils; + +namespace VpnHood.Tunneling.DomainFiltering; + +public class DomainFilterService(DomainFilter domainFilter, bool forceLogSni) +{ + public async Task Process(Stream tlsStream, IPAddress remoteAddress, CancellationToken cancellationToken) + { + // none if domain filter is empty + if (!forceLogSni && domainFilter.Includes.Length == 0 && domainFilter.Excludes.Length == 0 && domainFilter.Blocks.Length == 0) + return new DomainFilterResult + { + Action = DomainFilterAction.None + }; + + // extract SNI + var sniData = await SniExtractor.ExtractSni(tlsStream, cancellationToken).VhConfigureAwait(); + VhLogger.Instance.LogInformation(GeneralEventId.Sni, + "Domain: {Domain}, DestEp: {IP}", + VhLogger.FormatHostName(sniData.Sni), VhLogger.Format(remoteAddress)); + + // no SNI + var res = new DomainFilterResult + { + DomainName = sniData.Sni, + ReadData = sniData.ReadData, + Action = Process(sniData.Sni) + }; + + return res; + } + + private DomainFilterAction Process(string? domain) + { + var topDomains = ExtractTopDomains(domain); + foreach (var topDomain in topDomains) + { + var res = ProcessInternal(topDomain, domainFilter); + if (res != DomainFilterAction.None) + return res; + } + + return domainFilter.Includes.Length == 0 + ? DomainFilterAction.None + : DomainFilterAction.Exclude; + } + + public static DomainFilterAction ProcessInternal(string domain, DomainFilter domainFilter) + { + var topDomains = ExtractTopDomains(domain); + foreach (var topDomain in topDomains) + { + if (IsMatch(topDomain, domainFilter.Blocks)) + return DomainFilterAction.Block; + + if (IsMatch(topDomain, domainFilter.Excludes)) + return DomainFilterAction.Exclude; + + if (IsMatch(topDomain, domainFilter.Includes)) + return DomainFilterAction.Include; + } + + return DomainFilterAction.None; + } + + private static bool IsMatch(string domain, string[] domains) + { + return domains.Contains(domain); + } + + private static string[] ExtractTopDomains(string? domain) + { + if (string.IsNullOrEmpty(domain)) + return []; + + var topDomains = new List(); + var parts = domain.Split('.'); + for (var i = 0; i < parts.Length; i++) + { + var topDomain = string.Join('.', parts.Skip(i)); + topDomains.Add(topDomain); + } + + return topDomains.ToArray(); + } +} \ No newline at end of file diff --git a/VpnHood.Tunneling/GeneralEventId.cs b/VpnHood.Tunneling/GeneralEventId.cs index ff38583f8..3c1a65b31 100644 --- a/VpnHood.Tunneling/GeneralEventId.cs +++ b/VpnHood.Tunneling/GeneralEventId.cs @@ -6,6 +6,7 @@ public static class GeneralEventId { public static EventId Essential = new((int)EventCode.Essential, nameof(Essential)); public static EventId Session = new((int)EventCode.Session, nameof(Session)); + public static EventId Sni = new((int)EventCode.Sni, nameof(Sni)); public static EventId SessionTrack = new((int)EventCode.SessionTrack, nameof(SessionTrack)); public static EventId Nat = new((int)EventCode.Nat, nameof(Nat)); public static EventId Ping = new((int)EventCode.Ping, nameof(Ping)); @@ -30,6 +31,7 @@ private enum EventCode { Essential = 10, Session, + Sni, Nat, Ping, Dns, diff --git a/VpnHood.Tunneling/Messaging/HelloRequest.cs b/VpnHood.Tunneling/Messaging/HelloRequest.cs index 6bd012dbf..12aa41a83 100644 --- a/VpnHood.Tunneling/Messaging/HelloRequest.cs +++ b/VpnHood.Tunneling/Messaging/HelloRequest.cs @@ -10,4 +10,5 @@ public class HelloRequest() public required byte[] EncryptedClientId { get; init; } public string? ServerLocation { get; init; } // format: countryCode/region/city public bool AllowRedirect { get; init; } = true; + public bool? IsIpV6Supported { get; init; } } \ No newline at end of file diff --git a/VpnHood.Tunneling/NatItem.cs b/VpnHood.Tunneling/NatItem.cs index 9610dc519..343188214 100644 --- a/VpnHood.Tunneling/NatItem.cs +++ b/VpnHood.Tunneling/NatItem.cs @@ -3,6 +3,7 @@ using PacketDotNet; using VpnHood.Common.Logging; using VpnHood.Common.Utils; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; @@ -16,7 +17,7 @@ public class NatItem public ushort SourcePort { get; } public ushort IcmpId { get; } public DateTime AccessTime { get; internal set; } - + public bool? IsInProcess { get; set; } public NatItem(IPPacket ipPacket) { diff --git a/VpnHood.Tunneling/NatItemEx.cs b/VpnHood.Tunneling/NatItemEx.cs index dabac76b7..58d4fd1db 100644 --- a/VpnHood.Tunneling/NatItemEx.cs +++ b/VpnHood.Tunneling/NatItemEx.cs @@ -1,6 +1,7 @@ using System.Net; using PacketDotNet; using VpnHood.Common.Logging; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; @@ -53,6 +54,8 @@ public override int GetHashCode() public override string ToString() { return - $"{Protocol}:{NatId}, LocalEp: {VhLogger.Format(SourceAddress)}:{SourcePort}, RemoteEp: {VhLogger.Format(DestinationAddress)}:{DestinationPort}"; + $"{Protocol}:{NatId}, " + + $"LocalEp: {VhLogger.Format(SourceAddress)}:{SourcePort}, " + + $"RemoteEp: {VhLogger.Format(DestinationAddress)}:{DestinationPort}"; } } \ No newline at end of file diff --git a/VpnHood.Tunneling/PingProxy.cs b/VpnHood.Tunneling/PingProxy.cs index 36253a71b..6cbd9ab7c 100644 --- a/VpnHood.Tunneling/PingProxy.cs +++ b/VpnHood.Tunneling/PingProxy.cs @@ -3,6 +3,7 @@ using PacketDotNet.Utils; using VpnHood.Common.Collections; using VpnHood.Common.Utils; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/PingProxyPool.cs b/VpnHood.Tunneling/PingProxyPool.cs index af70de689..d332a2cbd 100644 --- a/VpnHood.Tunneling/PingProxyPool.cs +++ b/VpnHood.Tunneling/PingProxyPool.cs @@ -4,6 +4,7 @@ using VpnHood.Common.Jobs; using VpnHood.Common.Logging; using VpnHood.Common.Utils; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/ProxyManager.cs b/VpnHood.Tunneling/ProxyManager.cs index 16a6a105e..d808d304e 100644 --- a/VpnHood.Tunneling/ProxyManager.cs +++ b/VpnHood.Tunneling/ProxyManager.cs @@ -6,6 +6,7 @@ using VpnHood.Common.Utils; using VpnHood.Tunneling.Channels; using VpnHood.Tunneling.Factory; +using VpnHood.Tunneling.Utils; using ProtocolType = PacketDotNet.ProtocolType; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/StreamPacketReader.cs b/VpnHood.Tunneling/StreamPacketReader.cs index e6fb818c2..1deca9df8 100644 --- a/VpnHood.Tunneling/StreamPacketReader.cs +++ b/VpnHood.Tunneling/StreamPacketReader.cs @@ -2,6 +2,7 @@ using PacketDotNet; using VpnHood.Common.Logging; using VpnHood.Common.Utils; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/Tunnel.cs b/VpnHood.Tunneling/Tunnel.cs index 373361a9c..e794366d1 100644 --- a/VpnHood.Tunneling/Tunnel.cs +++ b/VpnHood.Tunneling/Tunnel.cs @@ -6,6 +6,7 @@ using VpnHood.Common.Utils; using VpnHood.Tunneling.Channels; using VpnHood.Tunneling.DatagramMessaging; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/UdpProxy.cs b/VpnHood.Tunneling/UdpProxy.cs index 607285bd5..e45bc6967 100644 --- a/VpnHood.Tunneling/UdpProxy.cs +++ b/VpnHood.Tunneling/UdpProxy.cs @@ -5,6 +5,7 @@ using VpnHood.Common.Collections; using VpnHood.Common.Logging; using VpnHood.Common.Utils; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/UdpProxyEx.cs b/VpnHood.Tunneling/UdpProxyEx.cs index 55fce935e..ca9d276fa 100644 --- a/VpnHood.Tunneling/UdpProxyEx.cs +++ b/VpnHood.Tunneling/UdpProxyEx.cs @@ -6,6 +6,7 @@ using VpnHood.Common.Collections; using VpnHood.Common.Logging; using VpnHood.Common.Utils; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/UdpProxyPool.cs b/VpnHood.Tunneling/UdpProxyPool.cs index 89f6b540c..bc28d9d81 100644 --- a/VpnHood.Tunneling/UdpProxyPool.cs +++ b/VpnHood.Tunneling/UdpProxyPool.cs @@ -5,6 +5,7 @@ using VpnHood.Common.Logging; using VpnHood.Tunneling.Exceptions; using VpnHood.Tunneling.Factory; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/UdpProxyPoolEx.cs b/VpnHood.Tunneling/UdpProxyPoolEx.cs index 3157f3688..36f96818b 100644 --- a/VpnHood.Tunneling/UdpProxyPoolEx.cs +++ b/VpnHood.Tunneling/UdpProxyPoolEx.cs @@ -5,6 +5,7 @@ using VpnHood.Common.Logging; using VpnHood.Tunneling.Exceptions; using VpnHood.Tunneling.Factory; +using VpnHood.Tunneling.Utils; namespace VpnHood.Tunneling; diff --git a/VpnHood.Tunneling/PacketUtil.cs b/VpnHood.Tunneling/Utils/PacketUtil.cs similarity index 97% rename from VpnHood.Tunneling/PacketUtil.cs rename to VpnHood.Tunneling/Utils/PacketUtil.cs index 099fc1ab4..71d8038e0 100644 --- a/VpnHood.Tunneling/PacketUtil.cs +++ b/VpnHood.Tunneling/Utils/PacketUtil.cs @@ -8,7 +8,7 @@ using ProtocolType = PacketDotNet.ProtocolType; // ReSharper disable UnusedMember.Global -namespace VpnHood.Tunneling; +namespace VpnHood.Tunneling.Utils; public static class PacketUtil { @@ -322,12 +322,12 @@ public static void LogPacket(IPPacket ipPacket, string message, LogLevel logLeve } case ProtocolType.Tcp: - { - eventId = GeneralEventId.Tcp; - var tcpPacket = ExtractTcp(ipPacket); - packetPayload = tcpPacket.PayloadData ?? []; - break; - } + { + eventId = GeneralEventId.Tcp; + var tcpPacket = ExtractTcp(ipPacket); + packetPayload = tcpPacket.PayloadData ?? []; + break; + } } VhLogger.Instance.Log(logLevel, eventId, exception, @@ -338,7 +338,7 @@ public static void LogPacket(IPPacket ipPacket, string message, LogLevel logLeve catch (Exception ex) { VhLogger.Instance.LogError(GeneralEventId.Packet, - ex, "Could not extract packet for log. Packet: {Packet}, Message: {Message}, Exception: {Exception}", + ex, "Could not extract packet for log. Packet: {Packet}, Message: {Message}, Exception: {Exception}", Format(ipPacket), message, exception); } } diff --git a/VpnHood.Tunneling/Utils/TlsUtil.cs b/VpnHood.Tunneling/Utils/TlsUtil.cs new file mode 100644 index 000000000..bd9c4c1fa --- /dev/null +++ b/VpnHood.Tunneling/Utils/TlsUtil.cs @@ -0,0 +1,121 @@ +using System.Text; +using Microsoft.Extensions.Logging; +using VpnHood.Common.Logging; +using VpnHood.Common.Utils; + +namespace VpnHood.Tunneling.Utils; + +public static class SniExtractor +{ + public class SniData + { + public required string? Sni { get; init; } + public required byte[] ReadData { get; init; } + } + + public static async Task ExtractSni(Stream tcpStream, CancellationToken cancellationToken) + { + // extract SNI + var initBuffer = new byte[1000]; + var bufCount = await tcpStream + .ReadAsync(initBuffer, 0, initBuffer.Length, cancellationToken) + .VhConfigureAwait(); + + return new SniData + { + Sni = ExtractSni(initBuffer[..bufCount]), + ReadData = initBuffer[..bufCount] + }; + } + + public static string? ExtractSni(byte[] payloadData) + { + try + { + return GetSniFromStreamInternal(payloadData); + } + catch (Exception ex) + { + VhLogger.Instance.LogError(GeneralEventId.Tcp, ex, "Could not extract sni"); + return null; + } + } + + public static string? GetSniFromStreamInternal(byte[] payloadData) + { + if (payloadData.Length == 0) + return null; + + // Check if it's a TLS ClientHello (0x16 is Handshake, 0x01 is ClientHello) + if (payloadData[0] != 0x16 || payloadData[5] != 0x01) + return null; + + var currentPos = 43; // Position of SessionID length + if (currentPos >= payloadData.Length) + return null; + + var sessionIdLength = payloadData[currentPos]; + currentPos += 1 + sessionIdLength; // Move past the SessionID + + if (currentPos + 2 > payloadData.Length) + return null; // Ensure there's enough data for cipher suites length + + var cipherSuitesLength = (payloadData[currentPos] << 8) | payloadData[currentPos + 1]; + currentPos += 2 + cipherSuitesLength; // Move past the Cipher Suites + + if (currentPos + 1 > payloadData.Length) + return null; // Ensure there's enough data for compression methods length + + var compressionMethodsLength = payloadData[currentPos]; + currentPos += 1 + compressionMethodsLength; // Move past the Compression Methods + + if (currentPos + 2 > payloadData.Length) + return null; // Extensions start position is out of bounds + + var extensionsLength = (payloadData[currentPos] << 8) | payloadData[currentPos + 1]; + currentPos += 2; // Move past the extensions length + + // Ensure the extensions length does not exceed the remaining payload length + if (currentPos + extensionsLength > payloadData.Length) + extensionsLength = payloadData.Length - currentPos; //Extensions length exceeds payload length. Adjusting to payload boundary + + while (currentPos < payloadData.Length && currentPos < extensionsLength + currentPos) + { + if (currentPos + 4 > payloadData.Length) + return null; // Extension header is out of bounds. + + var extensionType = (payloadData[currentPos] << 8) | payloadData[currentPos + 1]; + var extensionLength = (payloadData[currentPos + 2] << 8) | payloadData[currentPos + 3]; + currentPos += 4; // Move past the extension header + + if (currentPos + extensionLength > payloadData.Length) + return null; // Extension length is out of bounds. + + if (extensionType == 0x0000) // SNI extension type + { + if (currentPos + 2 > payloadData.Length) + return null; // Server name list length is out of bounds + + var serverNameListLength = (payloadData[currentPos] << 8) | payloadData[currentPos + 1]; + currentPos += 2; // Move past the server name list length + + if (currentPos + serverNameListLength > payloadData.Length) + return null; // Server name list length is out of bounds + + // var serverNameType = payloadData[currentPos]; + var serverNameLength = (payloadData[currentPos + 1] << 8) | payloadData[currentPos + 2]; + currentPos += 3; // Move past the server name type and length + + if (currentPos + serverNameLength > payloadData.Length) + return null; // Server name length is out of bounds + + var sni = Encoding.ASCII.GetString(payloadData, currentPos, serverNameLength); + return sni; + } + + currentPos += extensionLength; // Move to the next extension + } + + return null; + } +} \ No newline at end of file diff --git a/VpnHood.Tunneling/VpnHood.Tunneling.csproj b/VpnHood.Tunneling/VpnHood.Tunneling.csproj index 6668da302..2cdd96b47 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.5.535 + 4.6.544 $([System.DateTime]::Now.ToString("yyyy.M.d.HHmm")) @@ -28,7 +28,7 @@ - + diff --git a/VpnHood.sln.DotSettings b/VpnHood.sln.DotSettings index d570611f7..a03981291 100644 --- a/VpnHood.sln.DotSettings +++ b/VpnHood.sln.DotSettings @@ -1,16 +1,7 @@  - True ExplicitlyExcluded ExplicitlyExcluded - - - - - - - - True True True @@ -53,7 +44,6 @@ True True True - True True True @@ -69,9 +59,9 @@ True True True - True True True True - True \ No newline at end of file + True + \ No newline at end of file