From a679629a19ca1a3f635691734544004b37ac29bb Mon Sep 17 00:00:00 2001 From: sa-l10n-translation Date: Mon, 11 Nov 2024 06:03:42 +0000 Subject: [PATCH 01/12] i18n: Upgrade translations from crowdin (e46b9e3b). --- .../Properties/.locale-state.metadata | 2 +- .../Properties/Resources.fi-FI.resx | 26 +++++++++---------- .../Properties/Resources.sl-SI.resx | 14 +++++----- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/ProtonVPN.Translations/Properties/.locale-state.metadata b/src/ProtonVPN.Translations/Properties/.locale-state.metadata index 34370ebf..46ed9c1f 100644 --- a/src/ProtonVPN.Translations/Properties/.locale-state.metadata +++ b/src/ProtonVPN.Translations/Properties/.locale-state.metadata @@ -1,4 +1,4 @@ { "project": "windows-vpn", - "locale": "aa0428265e4075189145b086704b9a007598e582" + "locale": "43ee2d9909c2ac670adffdbb113416f4dd9145eb" } \ No newline at end of file diff --git a/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx b/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx index 408305ae..041cf03a 100644 --- a/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx +++ b/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx @@ -2064,7 +2064,7 @@ Ota se käyttöön seuraamalla <Hyperlink Command="{Binding OpenArticleComman The part of text displayed in a balloon above Secure Core Pin in Map. The full text is "(Secure Core) country name". - Hanki rajattomasti vaihtoja VPN Plus -tilauksella + Hanki rajattomasti vaihtoja VPN Plussalla Olet saavuttanut tällä erää käytettävissä olevan palvelinvaihdosten enimmäismäärän. @@ -2598,7 +2598,7 @@ Ota se käyttöön seuraamalla <Hyperlink Command="{Binding OpenArticleComman The heading of Secure Core server info displayed in Server Load popup. The {0} is a placeholder for entry country, {1} - exit country. Popup opens by pressing mouse button on server load image in Countries tab of Sidebar section of main app window. - Hanki maailmanlaajuiden kattavuus VPN Plus -tilauksella + Hanki maailmanlaajuiden kattavuus VPN Plussalla Ei maa, jonka halusit? Päivitä tilaus valitaksesi minkä tahansa palvelimen. @@ -2773,8 +2773,8 @@ Ota se käyttöön seuraamalla <Hyperlink Command="{Binding OpenArticleComman The WireGuard protocol name displayed in the Protocol combo box in Connection tab of Settings window. Do not translate. - Omien DNS-palvelimien käyttö poistaa NetShieldin käytöstä. DNS-palvelimet vastaavat siitä, että ohjaudut oikealle verkkosivustolle. -<LineBreak/><LineBreak/>Käytä vain luotettavia palvelimia. Haluatko käyttää omia DNS-palvelimia? + Vaihtoehtoisten DNS-palvelimien käyttö poistaa NetShieldin käytöstä. DNS-palvelimet vastaavat siitä, että ohjaudut oikealle verkkosivustolle. +<LineBreak/><LineBreak/>Käytä vain luotettavia palvelimia. Haluatko käyttää vaihtoehtoisia DNS-palvelimia? The text displayed in Custom DNS Servers warning modal window when use is trying to turn the feature ON when NetShield feature is turned on as well @@ -2782,11 +2782,11 @@ Ota se käyttöön seuraamalla <Hyperlink Command="{Binding OpenArticleComman The text on Upgrade button in Connection tab of Settings window - Omat DNS-palvelimet + Vaihtoehtoiset DNS-palvelimet The label for Custom DNS Servers combo box in Connection tab of Settings window - Syötä yksityinen DNS-palvelin (esim. AdGuard DNS) käsittelemään DNS-pyyntösi. Oman DNS-palvelimen käyttö poistaa NetShieldin käytöstä. DNS-palvelimet vastaavat siitä, että ohjaudut oikealle verkkosivustolle, joten käytä vain luotettavia palvelimia. + Syötä yksityinen DNS-palvelin (esim. AdGuard DNS) käsittelemään DNS-pyyntösi. Vaihtoehtoisen DNS-palvelimen käyttö poistaa NetShieldin käytöstä. DNS-palvelimet vastaavat siitä, että ohjaudut oikealle verkkosivustolle, joten käytä vain luotettavia palvelimia. The description of Custom DNS Servers setting displayed in the tooltip of the ( i ) image next to the label for Custom DNS Server toggle switch in Connection tab of Settings window @@ -3186,7 +3186,7 @@ This setting instructs the Proton VPN to start automatically when the user logs 30 päivän rahat takaisin -takuu - Avaa <Bold>maavalinta</Bold> VPN Plus -tilauksella + Avaa <Bold>maavalinta</Bold> VPN Plussalla Haluatko valita tietyn maan? @@ -3220,7 +3220,7 @@ This setting instructs the Proton VPN to start automatically when the user logs Tyypin 2 NAT (keskitasoinen) tarjoaa optimoidun nopeuden ja vakauden mahdollistamalla suorat yhteydet laitteiden välillä. - Avaa <Bold>tyypin 2 NAT</Bold> VPN Plus -tilauksella + Avaa <Bold>tyypin 2 NAT</Bold> VPN Plussalla Paranna pelikokemustasi @@ -3249,7 +3249,7 @@ This setting instructs the Proton VPN to start automatically when the user logs The button label in "allow non-standard ports" modal - Onko sinulla vaativia tai ammatillisia tietoteknisiä tarpeita, jotka tarvitsevat epätyypillisiä portteja? Päivitä VPN Plus -tilaukseen käyttääksesi tätä ja muita premium-ominaisuuksia. + Onko sinulla vaativia tai ammatillisia tietoteknisiä tarpeita, jotka tarvitsevat epätyypillisiä portteja? Päivitä VPN Plussaan käyttääksesi tätä ja muita premium-ominaisuuksia. The text in "allow non-standard ports" modal @@ -3257,7 +3257,7 @@ This setting instructs the Proton VPN to start automatically when the user logs The title in "allow non-standard ports" modal - Sisältyy VPN Business -tilaukseen. + Sisältyy VPN Businessiin. The tooltip text of the business plan badge @@ -3270,7 +3270,7 @@ This setting instructs the Proton VPN to start automatically when the user logs Määritä etäyhteys lähiverkkosi laitteille - Avaa <Bold>porttiohjaus</Bold> VPN Plus -tilauksella + Avaa <Bold>porttiohjaus</Bold> VPN Plussalla. Tehosta P2P-yhteyksiäsi @@ -3285,7 +3285,7 @@ This setting instructs the Proton VPN to start automatically when the user logs Määritä automaattinen yhdistäminen nopeampaa pääsyä varten. - Avaa <Bold>profiilit</Bold> VPN Plus -tilauksella + Avaa <Bold>profiilit</Bold> VPN Plussalla. Pääset nopeasti käsiksi usein käyttämiisi yhteyksiin @@ -3315,7 +3315,7 @@ This setting instructs the Proton VPN to start automatically when the user logs Käytä ulkomailla kotimaasi sisältöä ja paikallista sisältöä - Avaa <Bold>jaettu tunnelointi</Bold> VPN Plus -tilauksella + Avaa <Bold>jaettu tunnelointi</Bold> VPN Plussalla. Hanki molempien parhaat puolet diff --git a/src/ProtonVPN.Translations/Properties/Resources.sl-SI.resx b/src/ProtonVPN.Translations/Properties/Resources.sl-SI.resx index 92401798..7bace91a 100644 --- a/src/ProtonVPN.Translations/Properties/Resources.sl-SI.resx +++ b/src/ProtonVPN.Translations/Properties/Resources.sl-SI.resx @@ -2045,15 +2045,15 @@ Sledite <Hyperlink Command="{Binding OpenArticleCommand}"><Run Text="te The title of Login window. Should not be converted. - Hitrost odjemanja + Hitrost prejemanja: The label for download speed in Speed Graph on Map in main app window - Volumen prejemanja + Volumen prejemanja: The label for downloaded data amount in Speed Graph on Map in main app window - Seja + Seja: The label for VPN connection duration in Speed Graph on Map in main app window @@ -2069,11 +2069,11 @@ Sledite <Hyperlink Command="{Binding OpenArticleCommand}"><Run Text="te The label for traffic data in Speed Graph on Map in main app window - Hitrost pošiljanja + Hitrost oddajanja: The label for upload speed in Speed Graph on Map in main app window - Volumen oddajanja + Volumen oddajanja: The label for uploaded data amount in Speed Graph on Map in main app window @@ -2154,11 +2154,11 @@ Sledite <Hyperlink Command="{Binding OpenArticleCommand}"><Run Text="te The title of the Delinquency notification - Proton VPN je zaznal, da vaše trenutno omrežje preprečuje povezavo z VPN. Če omogočite pametni protokol, lahko zaobidete te omejitve. + Proton VPN je zaznal, da vaše trenutno omrežje preprečuje povezavo z VPN. Če omogočite "Smart protocol", lahko zaobidete te omejitve. The message displayed in the Enable Smart Protocol notification. - Pametni protokol je trenutno onemogočen + Smart protocol je trenutno onemogočen The title of the Enable Smart Protocol notification From 9e824667a2c585280ffbdb89a7bc54283ebe968a Mon Sep 17 00:00:00 2001 From: sa-l10n-translation Date: Mon, 18 Nov 2024 06:03:28 +0000 Subject: [PATCH 02/12] i18n: Upgrade translations from crowdin (a679629a). --- src/ProtonVPN.Translations/Properties/.locale-state.metadata | 2 +- src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ProtonVPN.Translations/Properties/.locale-state.metadata b/src/ProtonVPN.Translations/Properties/.locale-state.metadata index 46ed9c1f..62c73181 100644 --- a/src/ProtonVPN.Translations/Properties/.locale-state.metadata +++ b/src/ProtonVPN.Translations/Properties/.locale-state.metadata @@ -1,4 +1,4 @@ { "project": "windows-vpn", - "locale": "43ee2d9909c2ac670adffdbb113416f4dd9145eb" + "locale": "b0e0e013e20de5c7795a58f4eb856690278229a5" } \ No newline at end of file diff --git a/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx b/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx index 041cf03a..7dbb9285 100644 --- a/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx +++ b/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx @@ -1628,7 +1628,7 @@ Ota se käyttöön seuraamalla <Hyperlink Command="{Binding OpenArticleComman The text displayed in Disconnect modal window if TAP error is detected - VPN-palvelimen varmenteen vahvistuksessa on ongelma, joka saattaa viitata siihen, että verkkoyhteyttä on peukaloitu tai palvelimella on asetusongelma. Pyydä asiakaspalvelultamme apua lähettämällä <Hyperlink Command="{Binding ReportBugCommand}"><Run Text="ongelmaraportti"/></Hyperlink>. + VPN-palvelimen varmenteen vahvistuksessa on ongelma, joka saattaa viitata siihen, että verkkoyhteyttä on peukaloitu tai palvelimella on asetusongelma. Pyydä tueltamme apua lähettämällä <Hyperlink Command="{Binding ReportBugCommand}"><Run Text="ongelmaraportti"/></Hyperlink>. The error description displayed in Disconnect modal window if VPN server certificate validation error has occurred From bb8061d7634689716f9432704ca190a03f1108b1 Mon Sep 17 00:00:00 2001 From: Mindaugas Veblauskas Date: Tue, 19 Nov 2024 15:21:30 +0000 Subject: [PATCH 03/12] Fix callout driver --- .../Native/arm64/ProtonVPN.CalloutDriver.inf | 5 +- .../Native/arm64/ProtonVPN.CalloutDriver.sys | Bin 43936 -> 48056 bytes .../Native/arm64/protonvpn.calloutdriver.cat | Bin 12015 -> 12053 bytes Setup/Native/x64/ProtonVPN.CalloutDriver.inf | 5 +- Setup/Native/x64/ProtonVPN.CalloutDriver.sys | Bin 37768 -> 40360 bytes Setup/Native/x64/protonvpn.calloutdriver.cat | Bin 20122 -> 11895 bytes Setup/SetupBase.iss | 2 +- .../ProtonDrive.Downloader.csproj | 2 +- src/ProtonVPN.App/ProtonVPN.App.csproj | 2 +- src/ProtonVPN.CalloutDriver/.gitignore | 5 +- src/ProtonVPN.CalloutDriver/Callout.cpp | 2 +- .../ProtonVPN.CalloutDriver.arm64.ddf | 17 +++++ .../ProtonVPN.CalloutDriver.inf | 6 +- .../ProtonVPN.CalloutDriver.vcxproj | 70 +++++++++++++++--- .../ProtonVPN.CalloutDriver.x64.ddf | 17 +++++ src/ProtonVPN.CalloutDriver/README.md | 15 ++++ src/ProtonVPN.CalloutDriver/ReadMe.txt | 33 --------- .../Resources/VersionInfo.rc | Bin 2410 -> 2372 bytes .../ProtonVPN.App.Tests.csproj | 2 +- .../ProtonVPN.IntegrationTests.csproj | 2 +- .../ProtonVPN.UI.Tests.csproj | 2 +- 21 files changed, 132 insertions(+), 55 deletions(-) create mode 100644 src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.arm64.ddf create mode 100644 src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.x64.ddf create mode 100644 src/ProtonVPN.CalloutDriver/README.md delete mode 100644 src/ProtonVPN.CalloutDriver/ReadMe.txt diff --git a/Setup/Native/arm64/ProtonVPN.CalloutDriver.inf b/Setup/Native/arm64/ProtonVPN.CalloutDriver.inf index fcee7510..0be9e7ca 100644 --- a/Setup/Native/arm64/ProtonVPN.CalloutDriver.inf +++ b/Setup/Native/arm64/ProtonVPN.CalloutDriver.inf @@ -8,7 +8,8 @@ Class = WFPCALLOUTS ClassGuid = {57465043-616C-6C6F-7574-5F636C617373} Provider = %ManufacturerName% CatalogFile = ProtonVPN.CalloutDriver.cat -DriverVer = 08/19/2024,15.41.59.712 +DriverVer = 11/19/2024,7.43.45.10 +PnpLockdown = 1 [SourceDisksNames] 1 = %DiskName%,,,"" @@ -26,7 +27,7 @@ CopyFiles = ProtonVPN.CalloutDriver.Files [DefaultUninstall.NTARM64] LegacyUninstall = 1 -DelFiles = ProtonVPN.CalloutDriver.Files +;DelFiles = ProtonVPN.CalloutDriver.Files [DefaultInstall.NTARM64.Services] AddService = %ServiceName%,,ProtonVPN.CalloutDriver.Service diff --git a/Setup/Native/arm64/ProtonVPN.CalloutDriver.sys b/Setup/Native/arm64/ProtonVPN.CalloutDriver.sys index 57b351d298e858f56373860ba4bfd02cf96aa83c..1c179dda5bda8b3f722af1b5b456c124b35dc6cf 100644 GIT binary patch delta 19644 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 Dh0~qE delta 16539 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 diff --git a/Setup/Native/arm64/protonvpn.calloutdriver.cat b/Setup/Native/arm64/protonvpn.calloutdriver.cat index 73c2a544d9bfa1151eb27ebe62b50640f9a5cee8..5203800e6b9a2fd2ab27730981ae7a3b71a69819 100644 GIT binary patch delta 4212 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 delta 4302 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 D$*L6p delta 19013 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 Ib6K4HFF?fq82|tP literal 20122 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 ovTUs=%#`>XR-3r)z%(n3 - net8.0-windows10.0.17763.0 + net8.0-windows10.0.19041.0 WinExe ProtonDrive.Downloader ProtonDrive.Downloader diff --git a/src/ProtonVPN.App/ProtonVPN.App.csproj b/src/ProtonVPN.App/ProtonVPN.App.csproj index 3b48aaed..3e89cb7c 100644 --- a/src/ProtonVPN.App/ProtonVPN.App.csproj +++ b/src/ProtonVPN.App/ProtonVPN.App.csproj @@ -1,6 +1,6 @@  - net8.0-windows10.0.17763.0 + net8.0-windows10.0.19041.0 WinExe ProtonVPN ProtonVPN diff --git a/src/ProtonVPN.CalloutDriver/.gitignore b/src/ProtonVPN.CalloutDriver/.gitignore index 4a9c068e..8672de68 100644 --- a/src/ProtonVPN.CalloutDriver/.gitignore +++ b/src/ProtonVPN.CalloutDriver/.gitignore @@ -1,4 +1,7 @@ x64 Debug Release -Resources/RC* \ No newline at end of file +Resources/RC* +disk1 +setup.inf +setup.rpt \ No newline at end of file diff --git a/src/ProtonVPN.CalloutDriver/Callout.cpp b/src/ProtonVPN.CalloutDriver/Callout.cpp index 2216c633..998a7fa5 100644 --- a/src/ProtonVPN.CalloutDriver/Callout.cpp +++ b/src/ProtonVPN.CalloutDriver/Callout.cpp @@ -225,7 +225,7 @@ void FreeMemory(PVOID ptr) PVOID AllocateMemory(size_t size) { - return ExAllocatePoolWithTag(NonPagedPoolNx, size, ProtonTAG); + return ExAllocatePool2(POOL_FLAG_NON_PAGED, size, ProtonTAG); } void NTAPI CompleteBasicPacketInjection(VOID *data, diff --git a/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.arm64.ddf b/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.arm64.ddf new file mode 100644 index 00000000..e91eec4a --- /dev/null +++ b/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.arm64.ddf @@ -0,0 +1,17 @@ +.OPTION EXPLICIT +.Set CabinetFileCountThreshold=0 +.Set FolderFileCountThreshold=0 +.Set FolderSizeThreshold=0 +.Set MaxCabinetSize=0 +.Set MaxDiskFileCount=0 +.Set MaxDiskSize=0 +.Set CompressionType=MSZIP +.Set Cabinet=on +.Set Compress=on +.Set CabinetNameTemplate=ProtonVPN.CalloutDriver.arm64.cab +.Set DestinationDir=ProtonVPN.CalloutDriver + +bin\Release\ARM64\ProtonVPN.CalloutDriver\protonvpn.calloutdriver.cat +bin\Release\ARM64\ProtonVPN.CalloutDriver\ProtonVPN.CalloutDriver.inf +bin\Release\ARM64\ProtonVPN.CalloutDriver\ProtonVPN.CalloutDriver.pdb +bin\Release\ARM64\ProtonVPN.CalloutDriver\ProtonVPN.CalloutDriver.sys \ No newline at end of file diff --git a/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.inf b/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.inf index aeac9f12..07544095 100644 --- a/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.inf +++ b/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.inf @@ -9,6 +9,7 @@ ClassGuid = {57465043-616C-6C6F-7574-5F636C617373} Provider = %ManufacturerName% CatalogFile = ProtonVPN.CalloutDriver.cat DriverVer = 10/15/2020,1.0.4.0 +PnpLockdown = 1 [SourceDisksNames] 1 = %DiskName%,,,"" @@ -26,7 +27,10 @@ CopyFiles = ProtonVPN.CalloutDriver.Files [DefaultUninstall.NT$ARCH$] LegacyUninstall = 1 -DelFiles = ProtonVPN.CalloutDriver.Files +; With DelFiles the driver doesn't build. But it's fine since we use the +; callout driver as a service, so only sys file is used, but inf file is +; required signing and certification process, so we keep it. +;DelFiles = ProtonVPN.CalloutDriver.Files [DefaultInstall.NT$ARCH$.Services] AddService = %ServiceName%,,ProtonVPN.CalloutDriver.Service diff --git a/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.vcxproj b/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.vcxproj index 68366ead..4991d16d 100644 --- a/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.vcxproj +++ b/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.vcxproj @@ -1,10 +1,18 @@  + + Debug + ARM64 + Debug Win32 + + Release + ARM64 + Release Win32 @@ -19,7 +27,7 @@ - + @@ -49,7 +57,6 @@ - Windows7 true WindowsKernelModeDriver10.0 Driver @@ -57,7 +64,6 @@ Desktop - Windows7 false WindowsKernelModeDriver10.0 Driver @@ -65,7 +71,13 @@ Desktop - Windows7 + true + WindowsKernelModeDriver10.0 + Driver + KMDF + Desktop + + true WindowsKernelModeDriver10.0 Driver @@ -73,7 +85,13 @@ Desktop - Windows7 + false + WindowsKernelModeDriver10.0 + Driver + KMDF + Desktop + + false WindowsKernelModeDriver10.0 Driver @@ -103,18 +121,25 @@ bin\$(ConfigurationName)\$(Platform)\ $(ConfigurationName)\$(Platform)\ + + DbgengKernelDebugger + DbgengKernelDebugger bin\$(ConfigurationName)\$(Platform)\ $(ConfigurationName)\$(Platform)\ + + DbgengKernelDebugger + true + true true trace.h true - NT;NDIS60;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) + NT;NDIS630;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) $(DDK_LIB_PATH)ndis.lib;$(DDK_LIB_PATH)wdmsec.lib;$(DDK_LIB_PATH)fwpkclnt.lib;$(SDK_LIB_PATH)uuid.lib;%(AdditionalDependencies) @@ -126,7 +151,7 @@ true trace.h true - NT;NDIS60;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) + NT;NDIS630;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) $(DDK_LIB_PATH)ndis.lib;$(DDK_LIB_PATH)wdmsec.lib;$(DDK_LIB_PATH)fwpkclnt.lib;$(SDK_LIB_PATH)uuid.lib;%(AdditionalDependencies) @@ -138,19 +163,46 @@ true trace.h true - NT;NDIS60;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) + NT;NDIS630;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) $(DDK_LIB_PATH)ndis.lib;$(DDK_LIB_PATH)wdmsec.lib;$(DDK_LIB_PATH)fwpkclnt.lib;$(SDK_LIB_PATH)uuid.lib;%(AdditionalDependencies) + + + true + true + trace.h + true + NT;NDIS630;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) + StdCall + + + $(DDK_LIB_PATH)ndis.lib;$(DDK_LIB_PATH)wdmsec.lib;$(DDK_LIB_PATH)fwpkclnt.lib;$(SDK_LIB_PATH)uuid.lib;%(AdditionalDependencies) + + + + true true trace.h true - NT;NDIS60;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) + NT;NDIS630;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) + + + $(DDK_LIB_PATH)ndis.lib;$(DDK_LIB_PATH)wdmsec.lib;$(DDK_LIB_PATH)fwpkclnt.lib;$(SDK_LIB_PATH)uuid.lib;%(AdditionalDependencies) + + + + + true + true + trace.h + true + NT;NDIS630;NDIS_SUPPORT_NDIS6;C_DEFINES=$(C_DEFINES) -DPOOL_NX_OPTIN=1;%(PreprocessorDefinitions) $(DDK_LIB_PATH)ndis.lib;$(DDK_LIB_PATH)wdmsec.lib;$(DDK_LIB_PATH)fwpkclnt.lib;$(SDK_LIB_PATH)uuid.lib;%(AdditionalDependencies) diff --git a/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.x64.ddf b/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.x64.ddf new file mode 100644 index 00000000..8dc34716 --- /dev/null +++ b/src/ProtonVPN.CalloutDriver/ProtonVPN.CalloutDriver.x64.ddf @@ -0,0 +1,17 @@ +.OPTION EXPLICIT +.Set CabinetFileCountThreshold=0 +.Set FolderFileCountThreshold=0 +.Set FolderSizeThreshold=0 +.Set MaxCabinetSize=0 +.Set MaxDiskFileCount=0 +.Set MaxDiskSize=0 +.Set CompressionType=MSZIP +.Set Cabinet=on +.Set Compress=on +.Set CabinetNameTemplate=ProtonVPN.CalloutDriver.x64.cab +.Set DestinationDir=ProtonVPN.CalloutDriver.x64 + +bin\Release\x64\ProtonVPN.CalloutDriver\protonvpn.calloutdriver.cat +bin\Release\x64\ProtonVPN.CalloutDriver\ProtonVPN.CalloutDriver.inf +bin\Release\x64\ProtonVPN.CalloutDriver\ProtonVPN.CalloutDriver.pdb +bin\Release\x64\ProtonVPN.CalloutDriver\ProtonVPN.CalloutDriver.sys \ No newline at end of file diff --git a/src/ProtonVPN.CalloutDriver/README.md b/src/ProtonVPN.CalloutDriver/README.md new file mode 100644 index 00000000..966c67e3 --- /dev/null +++ b/src/ProtonVPN.CalloutDriver/README.md @@ -0,0 +1,15 @@ +## Release + +### x64 + +* Make sure Sign mode is set to **off** in project's settings (Driver Signing -> General -> Sign mode) +* Compile x64 release build +* copy file `src\ProtonVPN.CalloutDriver\bin\Release\x64\ProtonVPN.CalloutDriver.pdb` to `src\ProtonVPN.CalloutDriver\bin\Release\x64\ProtonVPN.CalloutDriver\ProtonVPN.CalloutDriver.pdb` +* `Sign src\ProtonVPN.CalloutDriver\bin\Release\x64\ProtonVPN.CalloutDriver\ProtonVPN.CalloutDriver.sys` with EV certificate and replace this file with a signed version of it. +* Create cab file (run this command from cmd at src\ProtonVPN.CalloutDriver) ```MakeCab /f .\ProtonVPN.CalloutDriver.x64.ddf``` +* Sign `disk1\ProtonVPN.CalloutDriver.x64.cab` with EV certificate. +* Upload `ProtonVPN.CalloutDriver.x64.cab` to Microsoft Partner Center https://partner.microsoft.com/en-us/dashboard/hardware/driver/New + +### arm64 + +The steps for building arm64 are identical to the x64 release process, except for substituting x64 with arm64. \ No newline at end of file diff --git a/src/ProtonVPN.CalloutDriver/ReadMe.txt b/src/ProtonVPN.CalloutDriver/ReadMe.txt deleted file mode 100644 index 1ab1af2c..00000000 --- a/src/ProtonVPN.CalloutDriver/ReadMe.txt +++ /dev/null @@ -1,33 +0,0 @@ -======================================================================== - ProtonVPN.CalloutDriver Project Overview -======================================================================== - -This file contains a summary of what you will find in each of the files that make up your project. - -ProtonVPN.CalloutDriver.vcxproj - This is the main project file for projects generated using an Application Wizard. - It contains information about the version of the product that generated the file, and - information about the platforms, configurations, and project features selected with the - Application Wizard. - -ProtonVPN.CalloutDriver.vcxproj.filters - This is the filters file for VC++ projects generated using an Application Wizard. - It contains information about the association between the files in your project - and the filters. This association is used in the IDE to show grouping of files with - similar extensions under a specific node (for e.g. ".cpp" files are associated with the - "Source Files" filter). - -Public.h - Header file to be shared with applications. - -Driver.c & Driver.h - DriverEntry and WDFDRIVER related functionality and callbacks. - -Device.c & Device.h - WDFDEVICE related functionality and callbacks. - -Callout.c & Callout.h - The WFP callout related functionality and callbacks. - -Trace.h - Definitions for WPP tracing. \ No newline at end of file diff --git a/src/ProtonVPN.CalloutDriver/Resources/VersionInfo.rc b/src/ProtonVPN.CalloutDriver/Resources/VersionInfo.rc index 75ba279106df57005ea1309583d43029ed17ce63..6982baea6b172b28e5776d40d0e69014db4eb3e3 100644 GIT binary patch delta 128 zcmaDQbVO*w1tvzb$rqU%8O=5eGk;{_b!JFp$N|E9hEj%-$)3i^h#*M1tvz*$rqU%8BI3}Gk;_f4`wJ}$YIE2C}B`w2w^B?$OFPuhMdWQY~ie? z40;R(lh?CKPHtnf!J|@r@;5dkW@840$#Yq?Cx@}yh$2iyHgi6ECSFr)I1GS}%jLKZ E05HlaJpcdz diff --git a/src/Tests/ProtonVPN.App.Tests/ProtonVPN.App.Tests.csproj b/src/Tests/ProtonVPN.App.Tests/ProtonVPN.App.Tests.csproj index ddafe0b8..2bd9952f 100644 --- a/src/Tests/ProtonVPN.App.Tests/ProtonVPN.App.Tests.csproj +++ b/src/Tests/ProtonVPN.App.Tests/ProtonVPN.App.Tests.csproj @@ -1,6 +1,6 @@  - net8.0-windows10.0.17763.0 + net8.0-windows10.0.19041.0 Library false true diff --git a/src/Tests/ProtonVPN.IntegrationTests/ProtonVPN.IntegrationTests.csproj b/src/Tests/ProtonVPN.IntegrationTests/ProtonVPN.IntegrationTests.csproj index 12fb6afa..e08c189d 100644 --- a/src/Tests/ProtonVPN.IntegrationTests/ProtonVPN.IntegrationTests.csproj +++ b/src/Tests/ProtonVPN.IntegrationTests/ProtonVPN.IntegrationTests.csproj @@ -1,6 +1,6 @@  - net8.0-windows10.0.17763.0 + net8.0-windows10.0.19041.0 Library false ..\..\bin\ diff --git a/src/Tests/ProtonVPN.UI.Tests/ProtonVPN.UI.Tests.csproj b/src/Tests/ProtonVPN.UI.Tests/ProtonVPN.UI.Tests.csproj index 8a210ae9..ff8768da 100644 --- a/src/Tests/ProtonVPN.UI.Tests/ProtonVPN.UI.Tests.csproj +++ b/src/Tests/ProtonVPN.UI.Tests/ProtonVPN.UI.Tests.csproj @@ -1,6 +1,6 @@  - net8.0-windows10.0.17763.0 + net8.0-windows10.0.19041.0 Library false true From 19274b6274842189cd80ae2d0ff55528aa9becce Mon Sep 17 00:00:00 2001 From: Eduardo Abreu Date: Wed, 20 Nov 2024 13:54:04 +0100 Subject: [PATCH 04/12] Change jitter to be positive only [VPNWIN-2483] --- .../Extensions/TimeSpanExtensions.cs | 2 +- .../Extensions/TimeSpanExtensionsTest.cs | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/ProtonVPN.Common/Extensions/TimeSpanExtensions.cs b/src/ProtonVPN.Common/Extensions/TimeSpanExtensions.cs index a839437a..e437d682 100644 --- a/src/ProtonVPN.Common/Extensions/TimeSpanExtensions.cs +++ b/src/ProtonVPN.Common/Extensions/TimeSpanExtensions.cs @@ -31,7 +31,7 @@ public static TimeSpan RandomizedWithDeviation(this TimeSpan value, double devia Ensure.IsTrue(value > TimeSpan.Zero, $"{nameof(value)} must be positive"); Ensure.IsTrue(deviation is >= 0 and < 1, $"{nameof(deviation)} must be between zero and one"); - return value + TimeSpan.FromMilliseconds(value.TotalMilliseconds * deviation * (2.0 * Random.NextDouble() - 1.0)); + return value + TimeSpan.FromMilliseconds(value.TotalMilliseconds * deviation * Random.NextDouble()); } public static TimeSpan Min(TimeSpan value1, TimeSpan value2) diff --git a/src/Tests/ProtonVPN.Common.Tests/Extensions/TimeSpanExtensionsTest.cs b/src/Tests/ProtonVPN.Common.Tests/Extensions/TimeSpanExtensionsTest.cs index 1c9927d1..a9b727ee 100644 --- a/src/Tests/ProtonVPN.Common.Tests/Extensions/TimeSpanExtensionsTest.cs +++ b/src/Tests/ProtonVPN.Common.Tests/Extensions/TimeSpanExtensionsTest.cs @@ -44,26 +44,33 @@ public void RandomizedWithDeviation_ShouldBe_Value_WhenDeviation_IsZero() public void RandomizedWithDeviation_ShouldBe_WithinDeviation() { // Arrange + int numOfGenerations = 100000; TimeSpan interval = TimeSpan.FromSeconds(20); const double deviation = 0.2; - TimeSpan minValue = interval; - TimeSpan maxValue = interval; + TimeSpan? minValue = null; + TimeSpan? maxValue = null; TimeSpan sumValue = TimeSpan.Zero; // Act - for (int i = 0; i < 1000; i++) + for (int i = 0; i < numOfGenerations; i++) { TimeSpan result = interval.RandomizedWithDeviation(deviation); - if (result < minValue) minValue = result; - if (result > maxValue) maxValue = result; + if (minValue is null || result < minValue) + { + minValue = result; + } + if (maxValue is null || result > maxValue) + { + maxValue = result; + } sumValue += result; } - TimeSpan medianValue = TimeSpan.FromMilliseconds(sumValue.TotalMilliseconds / 1000.0); + TimeSpan medianValue = TimeSpan.FromMilliseconds(sumValue.TotalMilliseconds / numOfGenerations); // Assert - minValue.Should().BeCloseTo(TimeSpan.FromSeconds(16), TimeSpan.FromMilliseconds(100)); - medianValue.Should().BeCloseTo(interval, TimeSpan.FromMilliseconds(300)); + minValue.Should().BeCloseTo(TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(100)); + medianValue.Should().BeCloseTo(TimeSpan.FromSeconds(22), TimeSpan.FromMilliseconds(100)); maxValue.Should().BeCloseTo(TimeSpan.FromSeconds(24), TimeSpan.FromMilliseconds(100)); } } From 94473f5216fcdd302761bb4dc7c9bc31cfeac991 Mon Sep 17 00:00:00 2001 From: Eduardo Abreu Date: Thu, 21 Nov 2024 13:43:30 +0100 Subject: [PATCH 05/12] Change streamed state logs from Debug to Info [VPNWIN-2500] --- .../Core/Service/Vpn/ClientControllerListener.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ProtonVPN.App/Core/Service/Vpn/ClientControllerListener.cs b/src/ProtonVPN.App/Core/Service/Vpn/ClientControllerListener.cs index 099984af..2134f5c2 100644 --- a/src/ProtonVPN.App/Core/Service/Vpn/ClientControllerListener.cs +++ b/src/ProtonVPN.App/Core/Service/Vpn/ClientControllerListener.cs @@ -101,7 +101,7 @@ private async Task StartVpnStateListenerAsync() await foreach (VpnStateIpcEntity state in _grpcClient.ClientController.StreamVpnStateChangeAsync()) { - _logger.Debug($"Received VPN Status '{state.Status}', " + + _logger.Info($"Received VPN Status '{state.Status}', " + $"NetworkBlocked: {state.NetworkBlocked} Error: '{state.Error}', EndpointIp: '{state.EndpointIp}', " + $"Label: '{state.Label}', VpnProtocol: '{state.VpnProtocol}', OpenVpnAdapter: '{state.OpenVpnAdapterType}'"); InvokeOnUiThread(() => { _clientControllerEventHandler.InvokeVpnStateChanged(state); }); @@ -126,7 +126,7 @@ private async Task StartPortForwardingStateListenerAsync() logMessage.Append($", Port pair {mappedPort.InternalPort}->{mappedPort.ExternalPort}, expiring in " + $"{mappedPort.Lifetime} at {mappedPort.ExpirationDateUtc}"); } - _logger.Debug(logMessage.ToString()); + _logger.Info(logMessage.ToString()); InvokeOnUiThread(() => _clientControllerEventHandler.InvokePortForwardingStateChanged(state)); } } @@ -136,7 +136,7 @@ private async Task StartConnectionDetailsListenerAsync() await foreach (ConnectionDetailsIpcEntity connectionDetails in _grpcClient.ClientController.StreamConnectionDetailsChangeAsync()) { - _logger.Debug($"Received connection details change while " + + _logger.Info($"Received connection details change while " + $"connected to server with IP '{connectionDetails.ServerIpAddress}'"); InvokeOnUiThread(() => _clientControllerEventHandler.InvokeConnectionDetailsChanged(connectionDetails)); } @@ -147,7 +147,7 @@ private async Task StartUpdateStateListenerAsync() await foreach (UpdateStateIpcEntity state in _grpcClient.ClientController.StreamUpdateStateChangeAsync()) { - _logger.Debug( + _logger.Info( $"Received update state change with status {state.Status}."); InvokeOnUiThread(() => _clientControllerEventHandler.InvokeUpdateStateChanged(state)); } @@ -158,7 +158,7 @@ private async Task StartNetShieldStatisticListenerAsync() await foreach (NetShieldStatisticIpcEntity netShieldStatistic in _grpcClient.ClientController.StreamNetShieldStatisticChangeAsync()) { - _logger.Debug( + _logger.Info( $"Received NetShield statistic change with timestamp '{netShieldStatistic.TimestampUtc}' " + $"[Ads: '{netShieldStatistic.NumOfAdvertisementUrlsBlocked}']" + $"[Malware: '{netShieldStatistic.NumOfMaliciousUrlsBlocked}']" + @@ -171,7 +171,7 @@ private async Task StartOpenWindowListenerAsync() { await foreach (string args in _grpcClient.ClientController.StreamOpenWindowAsync()) { - _logger.Debug("Received open window request."); + _logger.Info("Received open window request."); await ProcessCommandArgumentsAsync(args); InvokeOnUiThread(() => _clientControllerEventHandler.InvokeOpenWindowInvoked()); } From 8a41a2c6f71244bd5e4b464ec351f8f5ec1e2b51 Mon Sep 17 00:00:00 2001 From: Eduardo Abreu Date: Thu, 21 Nov 2024 14:26:09 +0100 Subject: [PATCH 06/12] Log API headers --- .../ProtonVPN.Api/Handlers/LoggingHandler.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Api/ProtonVPN.Api/Handlers/LoggingHandler.cs b/src/Api/ProtonVPN.Api/Handlers/LoggingHandler.cs index 25dd48be..ba63b047 100644 --- a/src/Api/ProtonVPN.Api/Handlers/LoggingHandler.cs +++ b/src/Api/ProtonVPN.Api/Handlers/LoggingHandler.cs @@ -18,7 +18,9 @@ */ using System; +using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using ProtonVPN.Common.Extensions; @@ -47,8 +49,18 @@ protected override async Task SendAsync( try { _logger.Info(req); + +#if DEBUG + LogHttpHeaders($"{req} request", request.Headers); +#endif + HttpResponseMessage result = await base.SendAsync(request, cancellationToken); _logger.Info($"{req}: {(int)result.StatusCode} {result.StatusCode}"); + +#if DEBUG + LogHttpHeaders($"{req} response", result.Headers); +#endif + return result; } catch (Exception ex) @@ -57,5 +69,11 @@ protected override async Task SendAsync( throw; } } + + private void LogHttpHeaders(string req, HttpHeaders headers) + { + string mergedHeaders = string.Join(',', headers.Select(kvp => $"{kvp.Key}: [{string.Join("],[", kvp.Value)}]")); + _logger.Debug($"{req} headers: {mergedHeaders}"); + } } } \ No newline at end of file From 8604aa2f2cccd67cf32807f0985f060297f90799 Mon Sep 17 00:00:00 2001 From: Eduardo Abreu Date: Fri, 22 Nov 2024 11:00:33 +0000 Subject: [PATCH 07/12] Reset Logicals timestamp when client has no servers on app start [VPNWIN-2497] --- src/ProtonVPN.App/Core/Bootstraper.cs | 5 ++++ .../Servers/ILogicalsTimestampResetter.cs | 26 +++++++++++++++++++ .../Servers/LogicalsTimestampResetter.cs | 10 +++---- 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 src/ProtonVPN.App/Servers/ILogicalsTimestampResetter.cs diff --git a/src/ProtonVPN.App/Core/Bootstraper.cs b/src/ProtonVPN.App/Core/Bootstraper.cs index ea1e1808..34468fa6 100644 --- a/src/ProtonVPN.App/Core/Bootstraper.cs +++ b/src/ProtonVPN.App/Core/Bootstraper.cs @@ -89,6 +89,7 @@ using ProtonVPN.ProcessCommunication.Contracts; using ProtonVPN.ProcessCommunication.Installers; using ProtonVPN.QuickLaunch; +using ProtonVPN.Servers; using ProtonVPN.Settings; using ProtonVPN.Settings.Migrations; using ProtonVPN.Sidebar; @@ -270,6 +271,10 @@ private void LoadServersFromCache() { Resolve().Load(servers); } + else + { + Resolve().Reset(); + } } private async Task IsUserValid() diff --git a/src/ProtonVPN.App/Servers/ILogicalsTimestampResetter.cs b/src/ProtonVPN.App/Servers/ILogicalsTimestampResetter.cs new file mode 100644 index 00000000..6478a81f --- /dev/null +++ b/src/ProtonVPN.App/Servers/ILogicalsTimestampResetter.cs @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Proton AG + * + * This file is part of ProtonVPN. + * + * ProtonVPN is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonVPN is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonVPN. If not, see . + */ + +namespace ProtonVPN.Servers +{ + public interface ILogicalsTimestampResetter + { + void Reset(); + } +} diff --git a/src/ProtonVPN.App/Servers/LogicalsTimestampResetter.cs b/src/ProtonVPN.App/Servers/LogicalsTimestampResetter.cs index 863fd369..bf3e7d0e 100644 --- a/src/ProtonVPN.App/Servers/LogicalsTimestampResetter.cs +++ b/src/ProtonVPN.App/Servers/LogicalsTimestampResetter.cs @@ -25,7 +25,7 @@ namespace ProtonVPN.Servers { - public class LogicalsTimestampResetter : ILoggedInAware, ILogoutAware, IVpnPlanAware + public class LogicalsTimestampResetter : ILogicalsTimestampResetter, ILoggedInAware, ILogoutAware, IVpnPlanAware { private readonly IAppSettings _appSettings; @@ -34,14 +34,14 @@ public LogicalsTimestampResetter(IAppSettings appSettings) _appSettings = appSettings; } - public void OnUserLoggedIn() + public void Reset() { - Reset(); + _appSettings.LogicalsLastModifiedDate = DateTimeOffset.UnixEpoch; } - private void Reset() + public void OnUserLoggedIn() { - _appSettings.LogicalsLastModifiedDate = DateTimeOffset.UnixEpoch; + Reset(); } public void OnUserLoggedOut() From 75cab129e509eb1a426729040e420958a5b55b66 Mon Sep 17 00:00:00 2001 From: Eduardo Abreu Date: Fri, 22 Nov 2024 15:10:42 +0000 Subject: [PATCH 08/12] Implement relays; Add x-pm-country header to Logicals and Loads [VPNWIN-2481][VPNWIN-2482] --- src/Api/ProtonVPN.Api.Contracts/IApiClient.cs | 6 +-- .../Servers/EntryPerProtocolEntryResponse.cs | 29 +++++++++++ .../Servers/EntryPerProtocolResponse.cs | 41 +++++++++++++++ .../Servers/PhysicalServerResponse.cs | 2 + src/Api/ProtonVPN.Api.Tests/ApiClientTest.cs | 4 +- src/Api/ProtonVPN.Api/ApiClient.cs | 20 +++----- src/Api/ProtonVPN.Api/BaseApiClient.cs | 6 ++- .../Entities/Vpn/VpnServerIpcEntity.cs | 3 ++ .../Vpn/VpnServerMapperTest.cs | 50 ++++++++++++++++++- .../Vpn/VpnServerMapper.cs | 12 ++++- .../Vpn/Connectors/GuestHoleConnector.cs | 3 +- .../Vpn/Connectors/ProfileConnector.cs | 2 +- src/ProtonVPN.Common/Vpn/VpnHost.cs | 26 ++++++++-- src/ProtonVPN.Core/Servers/ApiServers.cs | 11 ++-- .../Servers/Models/PhysicalServer.cs | 7 ++- src/ProtonVPN.Core/Servers/ServerManager.cs | 44 ++++++++++++++-- .../Servers/Specs/ServerByEntryIp.cs | 7 ++- .../Connection/VpnEndpointScanner.cs | 43 ++++++++++------ src/ProtonVPN.Vpn/OpenVpn/TcpPortScanner.cs | 5 +- .../Vpn/Connectors/ProfileConnectorTest.cs | 6 +-- .../ProtonVPN.Common.Tests/Vpn/VpnHostTest.cs | 45 +++++++++++++++-- .../Connection/HandlingRequestsWrapperTest.cs | 2 +- .../Arguments/EndpointArgumentsTest.cs | 8 +-- .../ServerValidation/ServerValidatorTest.cs | 15 ++++-- .../WireGuard/WireGuardConnectionTest.cs | 2 +- 25 files changed, 326 insertions(+), 73 deletions(-) create mode 100644 src/Api/ProtonVPN.Api.Contracts/Servers/EntryPerProtocolEntryResponse.cs create mode 100644 src/Api/ProtonVPN.Api.Contracts/Servers/EntryPerProtocolResponse.cs diff --git a/src/Api/ProtonVPN.Api.Contracts/IApiClient.cs b/src/Api/ProtonVPN.Api.Contracts/IApiClient.cs index b2673b9b..a7976e3d 100644 --- a/src/Api/ProtonVPN.Api.Contracts/IApiClient.cs +++ b/src/Api/ProtonVPN.Api.Contracts/IApiClient.cs @@ -45,13 +45,13 @@ public interface IApiClient : IClientBase Task> GetServerAsync(string serverId); Task> GetVpnInfoResponse(); Task> GetLogoutResponse(); - Task> GetServersAsync(string ip); + Task> GetServersAsync(string countryCode, string ip); Task> GetReportAnIssueFormData(); - Task> GetServerLoadsAsync(string ip); + Task> GetServerLoadsAsync(string countryCode, string ip); Task> GetLocationDataAsync(); Task> ReportBugAsync(IEnumerable> fields, IEnumerable files); Task> GetSessions(); - Task> GetVpnConfig(string country, string ip); + Task> GetVpnConfig(string countryCode, string ip); Task> GetAnnouncementsAsync(AnnouncementsRequest request); Task> GetStreamingServicesAsync(); Task> CheckAuthenticationServerStatusAsync(); diff --git a/src/Api/ProtonVPN.Api.Contracts/Servers/EntryPerProtocolEntryResponse.cs b/src/Api/ProtonVPN.Api.Contracts/Servers/EntryPerProtocolEntryResponse.cs new file mode 100644 index 00000000..2b2f221f --- /dev/null +++ b/src/Api/ProtonVPN.Api.Contracts/Servers/EntryPerProtocolEntryResponse.cs @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Proton AG + * + * This file is part of ProtonVPN. + * + * ProtonVPN is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonVPN is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonVPN. If not, see . + */ + +using Newtonsoft.Json; + +namespace ProtonVPN.Api.Contracts.Servers +{ + public class EntryPerProtocolEntryResponse + { + [JsonProperty("IPv4")] + public string Ipv4 { get; set; } + } +} diff --git a/src/Api/ProtonVPN.Api.Contracts/Servers/EntryPerProtocolResponse.cs b/src/Api/ProtonVPN.Api.Contracts/Servers/EntryPerProtocolResponse.cs new file mode 100644 index 00000000..387db667 --- /dev/null +++ b/src/Api/ProtonVPN.Api.Contracts/Servers/EntryPerProtocolResponse.cs @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Proton AG + * + * This file is part of ProtonVPN. + * + * ProtonVPN is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonVPN is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonVPN. If not, see . + */ + +using Newtonsoft.Json; + +namespace ProtonVPN.Api.Contracts.Servers +{ + public class EntryPerProtocolResponse + { + [JsonProperty("WireGuardUDP")] + public EntryPerProtocolEntryResponse WireGuardUdp { get; set; } + + [JsonProperty("WireGuardTCP")] + public EntryPerProtocolEntryResponse WireGuardTcp { get; set; } + + [JsonProperty("WireGuardTLS")] + public EntryPerProtocolEntryResponse WireGuardTls { get; set; } + + [JsonProperty("OpenVPNUDP")] + public EntryPerProtocolEntryResponse OpenVpnUdp { get; set; } + + [JsonProperty("OpenVPNTCP")] + public EntryPerProtocolEntryResponse OpenVpnTcp { get; set; } + } +} diff --git a/src/Api/ProtonVPN.Api.Contracts/Servers/PhysicalServerResponse.cs b/src/Api/ProtonVPN.Api.Contracts/Servers/PhysicalServerResponse.cs index 79814b5d..c4bbe0c6 100644 --- a/src/Api/ProtonVPN.Api.Contracts/Servers/PhysicalServerResponse.cs +++ b/src/Api/ProtonVPN.Api.Contracts/Servers/PhysicalServerResponse.cs @@ -41,5 +41,7 @@ public class PhysicalServerResponse public string X25519PublicKey; public string Signature; + + public EntryPerProtocolResponse EntryPerProtocol { get; set; } } } \ No newline at end of file diff --git a/src/Api/ProtonVPN.Api.Tests/ApiClientTest.cs b/src/Api/ProtonVPN.Api.Tests/ApiClientTest.cs index 535d7688..0fab648e 100644 --- a/src/Api/ProtonVPN.Api.Tests/ApiClientTest.cs +++ b/src/Api/ProtonVPN.Api.Tests/ApiClientTest.cs @@ -26,8 +26,8 @@ using ProtonVPN.Api.Contracts; using ProtonVPN.Api.Contracts.Servers; using ProtonVPN.Common.Configuration; -using ProtonVPN.Logging.Contracts; using ProtonVPN.Core.Settings; +using ProtonVPN.Logging.Contracts; using RichardSzalay.MockHttp; namespace ProtonVPN.Api.Tests @@ -78,7 +78,7 @@ public async Task ServerListDownloaded() Content = new StringContent("{'Code' : '1000', 'Servers': []}") }); - ApiResponseResult response = await _apiClient.GetServersAsync("127.0.0.0"); + ApiResponseResult response = await _apiClient.GetServersAsync(countryCode: "CH", ip: "127.0.0.0"); response.Success.Should().BeTrue(); } diff --git a/src/Api/ProtonVPN.Api/ApiClient.cs b/src/Api/ProtonVPN.Api/ApiClient.cs index a6014cc0..52ef0964 100644 --- a/src/Api/ProtonVPN.Api/ApiClient.cs +++ b/src/Api/ProtonVPN.Api/ApiClient.cs @@ -35,7 +35,6 @@ using ProtonVPN.Api.Contracts.VpnConfig; using ProtonVPN.Api.Contracts.VpnSessions; using ProtonVPN.Common.Configuration; -using ProtonVPN.Common.Extensions; using ProtonVPN.Common.OS.Net.Http; using ProtonVPN.Common.StatisticalEvents; using ProtonVPN.Core.Settings; @@ -105,19 +104,20 @@ public async Task> GetLogoutResponse() return await SendRequest(request, "Logout"); } - public async Task> GetServersAsync(string ip) + public async Task> GetServersAsync(string countryCode, string ip) { - HttpRequestMessage request = GetAuthorizedRequest(HttpMethod.Get, - "vpn/logicals?SignServer=Server.EntryIP,Server.Label", ip); + HttpRequestMessage request = GetAuthorizedRequestWithLocation(HttpMethod.Get, "vpn/logicals?" + + "SignServer=Server.EntryIP,Server.Label&" + + "WithEntriesForProtocols=WireGuardUDP,WireGuardTCP,WireGuardTLS,OpenVPNUDP,OpenVPNTCP", countryCode, ip); request.SetRetryCount(SERVERS_RETRY_COUNT); request.SetCustomTimeout(TimeSpan.FromSeconds(SERVERS_TIMEOUT_IN_SECONDS)); request.Headers.IfModifiedSince = AppSettings.LogicalsLastModifiedDate; return await SendRequest(request, "Get servers"); } - public async Task> GetServerLoadsAsync(string ip) + public async Task> GetServerLoadsAsync(string countryCode, string ip) { - HttpRequestMessage request = GetAuthorizedRequest(HttpMethod.Get, "vpn/loads", ip); + HttpRequestMessage request = GetAuthorizedRequestWithLocation(HttpMethod.Get, "vpn/loads", countryCode, ip); return await SendRequest(request, "Get server loads"); } @@ -163,13 +163,9 @@ public async Task> GetSessions() return await SendRequest(request, "Get sessions"); } - public async Task> GetVpnConfig(string country, string ip) + public async Task> GetVpnConfig(string countryCode, string ip) { - HttpRequestMessage request = GetAuthorizedRequest(HttpMethod.Get, "vpn/v2/clientconfig", ip); - if (!country.IsNullOrEmpty()) - { - request.Headers.Add("x-pm-country", country); - } + HttpRequestMessage request = GetAuthorizedRequestWithLocation(HttpMethod.Get, "vpn/v2/clientconfig", countryCode, ip); return await SendRequest(request, "Get VPN config"); } diff --git a/src/Api/ProtonVPN.Api/BaseApiClient.cs b/src/Api/ProtonVPN.Api/BaseApiClient.cs index 45a918f3..c3bd3bd4 100644 --- a/src/Api/ProtonVPN.Api/BaseApiClient.cs +++ b/src/Api/ProtonVPN.Api/BaseApiClient.cs @@ -154,9 +154,13 @@ protected HttpRequestMessage GetAuthorizedRequest(HttpMethod method, string requ return request; } - protected HttpRequestMessage GetAuthorizedRequest(HttpMethod method, string requestUri, string ip) + protected HttpRequestMessage GetAuthorizedRequestWithLocation(HttpMethod method, string requestUri, string countryCode, string ip) { HttpRequestMessage request = GetAuthorizedRequest(method, requestUri); + if (!countryCode.IsNullOrEmpty()) + { + request.Headers.Add("x-pm-country", countryCode); + } if (!ip.IsNullOrEmpty()) { request.Headers.Add("x-pm-netzone", ip); diff --git a/src/ProcessCommunication/ProtonVPN.ProcessCommunication.Contracts/Entities/Vpn/VpnServerIpcEntity.cs b/src/ProcessCommunication/ProtonVPN.ProcessCommunication.Contracts/Entities/Vpn/VpnServerIpcEntity.cs index 74fcaa42..46e9a001 100644 --- a/src/ProcessCommunication/ProtonVPN.ProcessCommunication.Contracts/Entities/Vpn/VpnServerIpcEntity.cs +++ b/src/ProcessCommunication/ProtonVPN.ProcessCommunication.Contracts/Entities/Vpn/VpnServerIpcEntity.cs @@ -38,5 +38,8 @@ public class VpnServerIpcEntity [DataMember(Order = 5)] public string Signature { get; set; } + + [DataMember(Order = 6)] + public Dictionary RelayIpByProtocol { get; set; } } } \ No newline at end of file diff --git a/src/ProcessCommunication/ProtonVPN.ProcessCommunication.EntityMapping.Tests/Vpn/VpnServerMapperTest.cs b/src/ProcessCommunication/ProtonVPN.ProcessCommunication.EntityMapping.Tests/Vpn/VpnServerMapperTest.cs index 338c2b97..e979de79 100644 --- a/src/ProcessCommunication/ProtonVPN.ProcessCommunication.EntityMapping.Tests/Vpn/VpnServerMapperTest.cs +++ b/src/ProcessCommunication/ProtonVPN.ProcessCommunication.EntityMapping.Tests/Vpn/VpnServerMapperTest.cs @@ -19,6 +19,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; +using ProtonVPN.Common.Networking; using ProtonVPN.Common.Vpn; using ProtonVPN.Crypto; using ProtonVPN.EntityMapping.Contracts; @@ -50,6 +51,11 @@ public void Initialize() _expectedPublicKey = new PublicKey("PVPN", KeyAlgorithm.Unknown); _entityMapper.Map(Arg.Any()) .Returns(_expectedPublicKey); + + _entityMapper.Map(Arg.Any()) + .Returns(x => (VpnProtocolIpcEntity)(int)x.Arg()); + _entityMapper.Map(Arg.Any()) + .Returns(x => (VpnProtocol)(int)x.Arg()); } [TestCleanup] @@ -63,14 +69,15 @@ public void Cleanup() } [TestMethod] - public void TestMapLeftToRight() + public void TestMapLeftToRight_WithNullRelayIpByProtocol() { VpnHost entityToTest = new( name: "protonvpn.com", ip: "192.168.0.0", label: DateTime.UtcNow.Millisecond.ToString(), x25519PublicKey: new PublicKey("PVPN", KeyAlgorithm.Unknown), - signature: DateTime.UtcNow.Ticks.ToString()); + signature: DateTime.UtcNow.Ticks.ToString(), + relayIpByProtocol: null); VpnServerIpcEntity result = _mapper.Map(entityToTest); @@ -80,6 +87,45 @@ public void TestMapLeftToRight() Assert.AreEqual(entityToTest.Label, result.Label); Assert.AreEqual(_expectedServerPublicKeyIpcEntity, result.X25519PublicKey); Assert.AreEqual(entityToTest.Signature, result.Signature); + Assert.IsNull(result.RelayIpByProtocol); + } + + [TestMethod] + public void TestMapLeftToRight_WithRelayIpByProtocol() + { + Dictionary relayIpByProtocol = new() + { + { VpnProtocol.WireGuardUdp, "1.1.1.1" }, + { VpnProtocol.WireGuardTcp, "2.2.2.2" }, + { VpnProtocol.WireGuardTls, "3.3.3.3" }, + { VpnProtocol.OpenVpnUdp, "4.4.4.4" }, + { VpnProtocol.OpenVpnTcp, "5.5.5.5" } + }; + + VpnHost entityToTest = new( + name: "protonvpn.com", + ip: "192.168.0.0", + label: DateTime.UtcNow.Millisecond.ToString(), + x25519PublicKey: new PublicKey("PVPN", KeyAlgorithm.Unknown), + signature: DateTime.UtcNow.Ticks.ToString(), + relayIpByProtocol: relayIpByProtocol); + + VpnServerIpcEntity result = _mapper.Map(entityToTest); + + Assert.IsNotNull(result); + Assert.AreEqual(entityToTest.Name, result.Name); + Assert.AreEqual(entityToTest.Ip, result.Ip); + Assert.AreEqual(entityToTest.Label, result.Label); + Assert.AreEqual(_expectedServerPublicKeyIpcEntity, result.X25519PublicKey); + Assert.AreEqual(entityToTest.Signature, result.Signature); + + Assert.IsNotNull(result.RelayIpByProtocol); + Assert.AreEqual(relayIpByProtocol.Count, result.RelayIpByProtocol.Count); + Assert.AreEqual(relayIpByProtocol[VpnProtocol.WireGuardUdp], result.RelayIpByProtocol[VpnProtocolIpcEntity.WireGuardUdp]); + Assert.AreEqual(relayIpByProtocol[VpnProtocol.WireGuardTcp], result.RelayIpByProtocol[VpnProtocolIpcEntity.WireGuardTcp]); + Assert.AreEqual(relayIpByProtocol[VpnProtocol.WireGuardTls], result.RelayIpByProtocol[VpnProtocolIpcEntity.WireGuardTls]); + Assert.AreEqual(relayIpByProtocol[VpnProtocol.OpenVpnUdp], result.RelayIpByProtocol[VpnProtocolIpcEntity.OpenVpnUdp]); + Assert.AreEqual(relayIpByProtocol[VpnProtocol.OpenVpnTcp], result.RelayIpByProtocol[VpnProtocolIpcEntity.OpenVpnTcp]); } [TestMethod] diff --git a/src/ProcessCommunication/ProtonVPN.ProcessCommunication.EntityMapping/Vpn/VpnServerMapper.cs b/src/ProcessCommunication/ProtonVPN.ProcessCommunication.EntityMapping/Vpn/VpnServerMapper.cs index 33a68e3e..29fc9281 100644 --- a/src/ProcessCommunication/ProtonVPN.ProcessCommunication.EntityMapping/Vpn/VpnServerMapper.cs +++ b/src/ProcessCommunication/ProtonVPN.ProcessCommunication.EntityMapping/Vpn/VpnServerMapper.cs @@ -17,6 +17,7 @@ * along with ProtonVPN. If not, see . */ +using ProtonVPN.Common.Networking; using ProtonVPN.Common.Vpn; using ProtonVPN.Crypto; using ProtonVPN.EntityMapping.Contracts; @@ -43,6 +44,9 @@ public VpnServerIpcEntity Map(VpnHost leftEntity) Label = leftEntity.Label, X25519PublicKey = _entityMapper.Map(leftEntity.X25519PublicKey), Signature = leftEntity.Signature, + RelayIpByProtocol = leftEntity.RelayIpByProtocol?.ToDictionary( + kvp => _entityMapper.Map(kvp.Key), + kvp => kvp.Value) }; } @@ -53,7 +57,13 @@ public VpnHost Map(VpnServerIpcEntity rightEntity) throw new ArgumentNullException(nameof(VpnServerIpcEntity), $"The {nameof(VpnServerIpcEntity)} parameter cannot be mapped from null to {nameof(VpnHost)}."); } + + Dictionary relayIpByProtocol = rightEntity.RelayIpByProtocol?.ToDictionary( + kvp => _entityMapper.Map(kvp.Key), + kvp => kvp.Value); + return new(rightEntity.Name, rightEntity.Ip, rightEntity.Label, - _entityMapper.Map(rightEntity.X25519PublicKey), rightEntity.Signature); + _entityMapper.Map(rightEntity.X25519PublicKey), + rightEntity.Signature, relayIpByProtocol); } } \ No newline at end of file diff --git a/src/ProtonVPN.App/Vpn/Connectors/GuestHoleConnector.cs b/src/ProtonVPN.App/Vpn/Connectors/GuestHoleConnector.cs index 96d48ec8..91d7b938 100644 --- a/src/ProtonVPN.App/Vpn/Connectors/GuestHoleConnector.cs +++ b/src/ProtonVPN.App/Vpn/Connectors/GuestHoleConnector.cs @@ -124,7 +124,8 @@ public IReadOnlyList Servers() server.Ip, server.Label, new PublicKey(server.X25519PublicKey, KeyAlgorithm.X25519), - server.Signature)) + server.Signature, + null)) .OrderBy(_ => _random.Next()) .ToList() : new List(); diff --git a/src/ProtonVPN.App/Vpn/Connectors/ProfileConnector.cs b/src/ProtonVPN.App/Vpn/Connectors/ProfileConnector.cs index 54f12ce1..dd12f497 100644 --- a/src/ProtonVPN.App/Vpn/Connectors/ProfileConnector.cs +++ b/src/ProtonVPN.App/Vpn/Connectors/ProfileConnector.cs @@ -523,7 +523,7 @@ private IReadOnlyList GetValidServers(IEnumerable servers) return servers .SelectMany(s => s.Servers.OrderBy(_ => _random.Next())) .Where(s => s.Status != 0) - .Select(s => new VpnHost(s.Domain, s.EntryIp, s.Label, GetServerPublicKey(s), s.Signature)) + .Select(s => new VpnHost(s.Domain, s.EntryIp, s.Label, GetServerPublicKey(s), s.Signature, s.RelayIpByProtocol)) .Distinct(s => (s.Ip, s.Label)) .ToList(); } diff --git a/src/ProtonVPN.Common/Vpn/VpnHost.cs b/src/ProtonVPN.Common/Vpn/VpnHost.cs index 18fe208c..4c071a03 100644 --- a/src/ProtonVPN.Common/Vpn/VpnHost.cs +++ b/src/ProtonVPN.Common/Vpn/VpnHost.cs @@ -21,21 +21,32 @@ using ProtonVPN.Common.Helpers; using System; using ProtonVPN.Crypto; +using ProtonVPN.Common.Networking; +using System.Collections.Generic; namespace ProtonVPN.Common.Vpn { public struct VpnHost { - public VpnHost(string name, string ip, string label, PublicKey x25519PublicKey, string signature) + public VpnHost(string name, string ip, string label, PublicKey x25519PublicKey, string signature, Dictionary relayIpByProtocol) { AssertHostNameIsValid(name); AssertIpAddressIsValid(ip); + if (relayIpByProtocol is not null) + { + foreach (KeyValuePair protocolIpPair in relayIpByProtocol) + { + AssertIpAddressIsValid(protocolIpPair.Value); + } + } + Name = name; Ip = ip; Label = label; X25519PublicKey = x25519PublicKey; Signature = signature; + RelayIpByProtocol = relayIpByProtocol; } public string Name { get; } @@ -48,8 +59,17 @@ public VpnHost(string name, string ip, string label, PublicKey x25519PublicKey, public string Signature { get; } + public Dictionary RelayIpByProtocol { get; } + public bool IsEmpty() => string.IsNullOrEmpty(Name) && string.IsNullOrEmpty(Ip); + public string GetIp(VpnProtocol protocol) + { + return RelayIpByProtocol is not null && RelayIpByProtocol.TryGetValue(protocol, out string relayIp) + ? relayIp + : Ip; + } + private static void AssertHostNameIsValid(string hostName) { Ensure.NotEmpty(hostName, nameof(hostName)); @@ -63,9 +83,7 @@ private static void AssertHostNameIsValid(string hostName) private static void AssertIpAddressIsValid(string ip) { - Ensure.NotEmpty(ip, nameof(ip)); - - if (!ip.IsValidIpAddress()) + if (!string.IsNullOrEmpty(ip) && !ip.IsValidIpAddress()) { throw new ArgumentException($"Invalid argument {nameof(ip)} value: {ip}"); } diff --git a/src/ProtonVPN.Core/Servers/ApiServers.cs b/src/ProtonVPN.Core/Servers/ApiServers.cs index f2f347b0..83fb36a0 100644 --- a/src/ProtonVPN.Core/Servers/ApiServers.cs +++ b/src/ProtonVPN.Core/Servers/ApiServers.cs @@ -38,25 +38,29 @@ public class ApiServers : IApiServers private readonly IApiClient _apiClient; private readonly IUserLocationService _userLocationService; private readonly IAppSettings _appSettings; + private readonly IUserStorage _userStorage; public ApiServers( ILogger logger, IApiClient apiClient, IUserLocationService userLocationService, - IAppSettings appSettings) + IAppSettings appSettings, + IUserStorage userStorage) { _logger = logger; _apiClient = apiClient; _userLocationService = userLocationService; _appSettings = appSettings; + _userStorage = userStorage; } public async Task> GetServersAsync() { try { + string countryCode = _userStorage.GetLocation().Country; string ip = await _userLocationService.GetTruncatedIpAddressAsync(); - ApiResponseResult response = await _apiClient.GetServersAsync(ip); + ApiResponseResult response = await _apiClient.GetServersAsync(countryCode, ip); if (response.LastModified.HasValue) { @@ -90,8 +94,9 @@ public async Task> GetLoadsAsync() { try { + string countryCode = _userStorage.GetLocation().Country; string ip = await _userLocationService.GetTruncatedIpAddressAsync(); - ApiResponseResult response = await _apiClient.GetServerLoadsAsync(ip); + ApiResponseResult response = await _apiClient.GetServerLoadsAsync(countryCode, ip); if (response.Success) { return response.Value.Servers; diff --git a/src/ProtonVPN.Core/Servers/Models/PhysicalServer.cs b/src/ProtonVPN.Core/Servers/Models/PhysicalServer.cs index 65573b33..9b8041c8 100644 --- a/src/ProtonVPN.Core/Servers/Models/PhysicalServer.cs +++ b/src/ProtonVPN.Core/Servers/Models/PhysicalServer.cs @@ -17,6 +17,9 @@ * along with ProtonVPN. If not, see . */ +using System.Collections.Generic; +using ProtonVPN.Common.Networking; + namespace ProtonVPN.Core.Servers.Models { public class PhysicalServer @@ -29,9 +32,10 @@ public class PhysicalServer public sbyte Status { get; } public string X25519PublicKey { get; } public string Signature { get; } + public Dictionary RelayIpByProtocol { get; } public PhysicalServer(string id, string entryIp, string exitIp, string domain, string label, sbyte status, - string x25519PublicKey, string signature) + string x25519PublicKey, string signature, Dictionary relayIpByProtocol) { Id = id; EntryIp = entryIp; @@ -41,6 +45,7 @@ public PhysicalServer(string id, string entryIp, string exitIp, string domain, s Status = status; X25519PublicKey = x25519PublicKey; Signature = signature; + RelayIpByProtocol = relayIpByProtocol; } } } \ No newline at end of file diff --git a/src/ProtonVPN.Core/Servers/ServerManager.cs b/src/ProtonVPN.Core/Servers/ServerManager.cs index 6f8db093..f2879de8 100644 --- a/src/ProtonVPN.Core/Servers/ServerManager.cs +++ b/src/ProtonVPN.Core/Servers/ServerManager.cs @@ -103,7 +103,7 @@ public virtual void UpdateLoads(IReadOnlyCollection serve } } - public IReadOnlyCollection GetServers(ISpecification spec, + public IReadOnlyCollection GetServers(ISpecification spec, Features orderBy = Features.None) { sbyte userTier = _userStorage.GetUser().MaxTier; @@ -140,7 +140,9 @@ public Server GetServerByEntryIpAndLabel(string entryIp, string label) { foreach (PhysicalServer physicalServer in server.Servers) { - if (entryIp == physicalServer.EntryIp && (string.IsNullOrEmpty(label) || label == physicalServer.Label)) + if ((entryIp == physicalServer.EntryIp || + (physicalServer.RelayIpByProtocol is not null && physicalServer.RelayIpByProtocol.ContainsValue(entryIp))) && + (string.IsNullOrEmpty(label) || label == physicalServer.Label)) { Server clone = server.Clone(); clone.ExitIp = physicalServer.ExitIp; @@ -340,6 +342,9 @@ private static Server Map(LogicalServerResponse item) private static PhysicalServer Map(PhysicalServerResponse server) { + Dictionary relayIpByProtocol = server.EntryPerProtocol is not null + ? Map(server.EntryPerProtocol) + : null; return new( id: server.Id, entryIp: server.EntryIp, @@ -348,7 +353,40 @@ private static PhysicalServer Map(PhysicalServerResponse server) label: server.Label, status: server.Status, x25519PublicKey: server.X25519PublicKey, - signature: server.Signature); + signature: server.Signature, + relayIpByProtocol: relayIpByProtocol); + } + + private static Dictionary Map(EntryPerProtocolResponse entryPerProtocol) + { + Dictionary relayIpByProtocol = new(); + + if (!string.IsNullOrWhiteSpace(entryPerProtocol.WireGuardUdp?.Ipv4)) + { + relayIpByProtocol.Add(VpnProtocol.WireGuardUdp, entryPerProtocol.WireGuardUdp.Ipv4); + } + + if (!string.IsNullOrWhiteSpace(entryPerProtocol.WireGuardTcp?.Ipv4)) + { + relayIpByProtocol.Add(VpnProtocol.WireGuardTcp, entryPerProtocol.WireGuardTcp.Ipv4); + } + + if (!string.IsNullOrWhiteSpace(entryPerProtocol.WireGuardTls?.Ipv4)) + { + relayIpByProtocol.Add(VpnProtocol.WireGuardTls, entryPerProtocol.WireGuardTls.Ipv4); + } + + if (!string.IsNullOrWhiteSpace(entryPerProtocol.OpenVpnUdp?.Ipv4)) + { + relayIpByProtocol.Add(VpnProtocol.OpenVpnUdp, entryPerProtocol.OpenVpnUdp.Ipv4); + } + + if (!string.IsNullOrWhiteSpace(entryPerProtocol.OpenVpnTcp?.Ipv4)) + { + relayIpByProtocol.Add(VpnProtocol.OpenVpnTcp, entryPerProtocol.OpenVpnTcp.Ipv4); + } + + return relayIpByProtocol; } /// diff --git a/src/ProtonVPN.Core/Servers/Specs/ServerByEntryIp.cs b/src/ProtonVPN.Core/Servers/Specs/ServerByEntryIp.cs index 1be97239..4878c3dc 100644 --- a/src/ProtonVPN.Core/Servers/Specs/ServerByEntryIp.cs +++ b/src/ProtonVPN.Core/Servers/Specs/ServerByEntryIp.cs @@ -34,7 +34,12 @@ public ServerByEntryIp(string ip) public override bool IsSatisfiedBy(LogicalServerResponse item) { - return item.Servers.Any(s => s.EntryIp == _ip); + return item.Servers.Any(s => s.EntryIp == _ip + || s.EntryPerProtocol?.WireGuardUdp?.Ipv4 == _ip + || s.EntryPerProtocol?.WireGuardTcp?.Ipv4 == _ip + || s.EntryPerProtocol?.WireGuardTls?.Ipv4 == _ip + || s.EntryPerProtocol?.OpenVpnUdp?.Ipv4 == _ip + || s.EntryPerProtocol?.OpenVpnTcp?.Ipv4 == _ip); } } } \ No newline at end of file diff --git a/src/ProtonVPN.Vpn/Connection/VpnEndpointScanner.cs b/src/ProtonVPN.Vpn/Connection/VpnEndpointScanner.cs index 355f3467..17d7372b 100644 --- a/src/ProtonVPN.Vpn/Connection/VpnEndpointScanner.cs +++ b/src/ProtonVPN.Vpn/Connection/VpnEndpointScanner.cs @@ -153,48 +153,59 @@ private IList> EndpointCandidates( continue; } - foreach (int port in ports[preferredProtocol]) + string ip = endpoint.Server.GetIp(preferredProtocol); + + if (string.IsNullOrWhiteSpace(ip)) + { + _logger.Info($"There is no entry IP for {preferredProtocol} protocol."); + } + else { - list.Add(GetPortAliveAsync(new VpnEndpoint(endpoint.Server, preferredProtocol, port), cancellationToken)); + foreach (int port in ports[preferredProtocol]) + { + list.Add(GetPortAliveAsync(ip, endpoint.Server, preferredProtocol, port, cancellationToken)); + } } } return list; } - private async Task GetPortAliveAsync(VpnEndpoint endpoint, CancellationToken cancellationToken) + private async Task GetPortAliveAsync(string ip, VpnHost server, VpnProtocol protocol, int port, + CancellationToken cancellationToken) { - _logger.Info($"Pinging VPN endpoint {endpoint.Server.Ip}:{endpoint.Port} for {endpoint.VpnProtocol} protocol."); + + _logger.Info($"Pinging VPN endpoint {ip}:{port} for {protocol} protocol."); bool isAlive = false; - switch (endpoint.VpnProtocol) + switch (protocol) { case VpnProtocol.OpenVpnTcp: case VpnProtocol.WireGuardTcp: case VpnProtocol.WireGuardTls: - isAlive = await IsTcpEndpointAliveAsync(endpoint, cancellationToken); + isAlive = await IsTcpEndpointAliveAsync(ip, port, cancellationToken); break; case VpnProtocol.OpenVpnUdp: case VpnProtocol.WireGuardUdp: - isAlive = await IsUdpEndpointAliveAsync(endpoint, cancellationToken); + isAlive = await IsUdpEndpointAliveAsync(ip, port, server.X25519PublicKey.Base64, cancellationToken); break; } - return isAlive ? endpoint : VpnEndpoint.Empty; + return isAlive ? new VpnEndpoint(new VpnHost(server.Name, ip, server.Label, server.X25519PublicKey, server.Signature, null), + protocol, port) : VpnEndpoint.Empty; } - private async Task IsTcpEndpointAliveAsync(VpnEndpoint endpoint, CancellationToken cancellationToken) + private async Task IsTcpEndpointAliveAsync(string ip, int port, CancellationToken cancellationToken) { - return await IsEndpointAliveAsync(async timeoutTask => await _tcpPortScanner.IsAliveAsync(endpoint, timeoutTask), cancellationToken); + return await IsEndpointAliveAsync(async timeoutTask => + await _tcpPortScanner.IsAliveAsync(ip, port, timeoutTask), cancellationToken); } - private async Task IsUdpEndpointAliveAsync(VpnEndpoint endpoint, CancellationToken cancellationToken) + private async Task IsUdpEndpointAliveAsync(string ip, int port, string serverKeyBase64, + CancellationToken cancellationToken) { - return await IsEndpointAliveAsync(async timeoutTask => await _udpPingClient.Ping( - endpoint.Server.Ip, - endpoint.Port, - endpoint.Server.X25519PublicKey.Base64, - timeoutTask), cancellationToken); + return await IsEndpointAliveAsync(async timeoutTask => + await _udpPingClient.Ping(ip, port, serverKeyBase64, timeoutTask), cancellationToken); } private async Task IsEndpointAliveAsync(Func> func, CancellationToken cancellationToken) diff --git a/src/ProtonVPN.Vpn/OpenVpn/TcpPortScanner.cs b/src/ProtonVPN.Vpn/OpenVpn/TcpPortScanner.cs index 8fc46538..7144d3b2 100644 --- a/src/ProtonVPN.Vpn/OpenVpn/TcpPortScanner.cs +++ b/src/ProtonVPN.Vpn/OpenVpn/TcpPortScanner.cs @@ -22,15 +22,14 @@ using System.Net.Sockets; using System.Threading.Tasks; using ProtonVPN.Common.Extensions; -using ProtonVPN.Vpn.Common; namespace ProtonVPN.Vpn.OpenVpn { public class TcpPortScanner { - public async Task IsAliveAsync(VpnEndpoint vpnEndpoint, Task timeoutTask) + public async Task IsAliveAsync(string ip, int port, Task timeoutTask) { - IPEndPoint endpoint = new(IPAddress.Parse(vpnEndpoint.Server.Ip), vpnEndpoint.Port); + IPEndPoint endpoint = new(IPAddress.Parse(ip), port); using Socket socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try diff --git a/src/Tests/ProtonVPN.App.Tests/Vpn/Connectors/ProfileConnectorTest.cs b/src/Tests/ProtonVPN.App.Tests/Vpn/Connectors/ProfileConnectorTest.cs index f71e4437..f033dbe1 100644 --- a/src/Tests/ProtonVPN.App.Tests/Vpn/Connectors/ProfileConnectorTest.cs +++ b/src/Tests/ProtonVPN.App.Tests/Vpn/Connectors/ProfileConnectorTest.cs @@ -127,17 +127,17 @@ private void InitializeArrangeVariables() _standardPhysicalServers = new List { new(id: "Standard-PS", entryIp: "192.168.0.1", exitIp: "192.168.1.1", - domain: "standard.protonvpn.ps", status: 1, label: string.Empty, x25519PublicKey: string.Empty, signature: string.Empty) + domain: "standard.protonvpn.ps", status: 1, label: string.Empty, x25519PublicKey: string.Empty, signature: string.Empty, relayIpByProtocol: null) }; _p2pPhysicalServers = new List { new(id: "P2P-PS", entryIp: "192.168.0.2", exitIp: "192.168.1.2", - domain: "p2p.protonvpn.ps", status: 1, label: string.Empty, x25519PublicKey: string.Empty, signature: string.Empty) + domain: "p2p.protonvpn.ps", status: 1, label: string.Empty, x25519PublicKey: string.Empty, signature: string.Empty, relayIpByProtocol: null) }; _torPhysicalServers = new List { new(id: "Tor-PS", entryIp: "192.168.0.3", exitIp: "192.168.1.3", - domain: "tor.protonvpn.ps", status: 1, label: string.Empty, x25519PublicKey: string.Empty, signature: string.Empty) + domain: "tor.protonvpn.ps", status: 1, label: string.Empty, x25519PublicKey: string.Empty, signature: string.Empty, relayIpByProtocol: null) }; _standardServer = new Server(id: "Standard-S", name: "Standard", city: "City", entryCountry: "CH", exitCountry: "CH", domain: "standard.protonvpn.s", status: 1, tier: ServerTiers.Basic, diff --git a/src/Tests/ProtonVPN.Common.Tests/Vpn/VpnHostTest.cs b/src/Tests/ProtonVPN.Common.Tests/Vpn/VpnHostTest.cs index 9e053eca..7ad5d126 100644 --- a/src/Tests/ProtonVPN.Common.Tests/Vpn/VpnHostTest.cs +++ b/src/Tests/ProtonVPN.Common.Tests/Vpn/VpnHostTest.cs @@ -18,9 +18,11 @@ */ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using ProtonVPN.Common.Networking; using ProtonVPN.Common.Vpn; namespace ProtonVPN.Common.Tests.Vpn @@ -34,7 +36,7 @@ public void Name_ShouldBe_Name() { // Arrange const string expected = "server-1.protonvpn.com"; - VpnHost host = new(expected, "127.0.0.1", string.Empty, null, string.Empty); + VpnHost host = new(expected, "127.0.0.1", string.Empty, null, string.Empty, null); // Act string result = host.Name; @@ -48,7 +50,7 @@ public void Ip_ShouldBe_Ip() { // Arrange const string expected = "44.55.66.77"; - VpnHost host = new("server-1.protonvpn.com", expected, string.Empty, null, string.Empty); + VpnHost host = new("server-1.protonvpn.com", expected, string.Empty, null, string.Empty, null); // Act string result = host.Ip; @@ -74,7 +76,7 @@ public void IsEmpty_ShouldBeTrue_WhenDefault() public void IsEmpty_ShouldBeTrue_WhenNew() { // Arrange - VpnHost host = new("name.com", "0.0.0.0", string.Empty, null, string.Empty); + VpnHost host = new("name.com", "0.0.0.0", string.Empty, null, string.Empty, null); // Act bool result = host.IsEmpty(); @@ -91,7 +93,7 @@ public void IsEmpty_ShouldBeTrue_WhenNew() public void VpnHost_ShouldThrow_WhenNameIsNotValid(string name) { // Act - Action action = () => new VpnHost(name, "127.0.0.1", string.Empty, null, string.Empty); + Action action = () => new VpnHost(name, "127.0.0.1", string.Empty, null, string.Empty, null); // Assert action.Should().Throw(); @@ -100,6 +102,19 @@ public void VpnHost_ShouldThrow_WhenNameIsNotValid(string name) [DataTestMethod] [DataRow(null)] [DataRow("")] + public void VpnHost_ShouldNotThrow_WhenIpIsNullOrEmpty(string ip) + { + // Act + VpnHost host = new("test.server.com", ip, string.Empty, null, string.Empty, null); + + // Act + string result = host.Ip; + + // Assert + result.Should().Be(ip); + } + + [DataTestMethod] [DataRow("158.159.247")] [DataRow("127.0.0.4 ")] [DataRow("-127.0.0.4")] @@ -108,7 +123,27 @@ public void VpnHost_ShouldThrow_WhenNameIsNotValid(string name) public void VpnHost_ShouldThrow_WhenIpIsNotValid(string ip) { // Act - Action action = () => new VpnHost("test.server.com", ip, string.Empty, null, string.Empty); + Action action = () => new VpnHost("test.server.com", ip, string.Empty, null, string.Empty, null); + + // Assert + action.Should().Throw(); + } + + [DataTestMethod] + [DataRow("158.159.247")] + [DataRow("127.0.0.4 ")] + [DataRow("-127.0.0.4")] + [DataRow("\"27.0.0.4")] + [DataRow("227.0.0.4\"")] + public void VpnHost_ShouldThrow_WhenRelayIpIsNotValid(string ip) + { + Dictionary dictionary = new Dictionary() + { + { VpnProtocol.WireGuardUdp, ip } + }; + + // Act + Action action = () => new VpnHost("test.server.com", null, string.Empty, null, string.Empty, dictionary); // Assert action.Should().Throw(); diff --git a/src/Tests/ProtonVPN.Vpn.Tests/Connection/HandlingRequestsWrapperTest.cs b/src/Tests/ProtonVPN.Vpn.Tests/Connection/HandlingRequestsWrapperTest.cs index b52a5313..db6327af 100644 --- a/src/Tests/ProtonVPN.Vpn.Tests/Connection/HandlingRequestsWrapperTest.cs +++ b/src/Tests/ProtonVPN.Vpn.Tests/Connection/HandlingRequestsWrapperTest.cs @@ -53,7 +53,7 @@ public void TestInitialize() _taskQueue = new TaskQueue(); _origin = Substitute.For(); - _endpoint = new VpnEndpoint(new VpnHost("proton.vpn", "135.27.46.203", string.Empty, null, string.Empty), VpnProtocol.OpenVpnTcp, 777); + _endpoint = new VpnEndpoint(new VpnHost("proton.vpn", "135.27.46.203", string.Empty, null, string.Empty, null), VpnProtocol.OpenVpnTcp, 777); _credentials = new VpnCredentials("cert", new AsymmetricKeyPair( new SecretKey("U2VjcmV0S2V5", KeyAlgorithm.Unknown), diff --git a/src/Tests/ProtonVPN.Vpn.Tests/OpenVpn/Arguments/EndpointArgumentsTest.cs b/src/Tests/ProtonVPN.Vpn.Tests/OpenVpn/Arguments/EndpointArgumentsTest.cs index 31dbf5f3..39683e39 100644 --- a/src/Tests/ProtonVPN.Vpn.Tests/OpenVpn/Arguments/EndpointArgumentsTest.cs +++ b/src/Tests/ProtonVPN.Vpn.Tests/OpenVpn/Arguments/EndpointArgumentsTest.cs @@ -39,7 +39,7 @@ public void Enumerable_ShouldContain_ExpectedNumberOfOptions() { // Arrange VpnEndpoint endpoint = - new(new VpnHost("abc.com", "4.5.6.7", string.Empty, null, string.Empty), VpnProtocol.OpenVpnUdp, 48965); + new(new VpnHost("abc.com", "4.5.6.7", string.Empty, null, string.Empty, null), VpnProtocol.OpenVpnUdp, 48965); OpenVpnEndpointArguments subject = new(endpoint); // Act @@ -54,7 +54,7 @@ public void Enumerable_ShouldContain_RemoteOption() { // Arrange VpnEndpoint endpoint = - new(new VpnHost("abc.com", "11.22.33.44", string.Empty, null, string.Empty), VpnProtocol.OpenVpnUdp, 61874); + new(new VpnHost("abc.com", "11.22.33.44", string.Empty, null, string.Empty, null), VpnProtocol.OpenVpnUdp, 61874); OpenVpnEndpointArguments subject = new(endpoint); // Act @@ -71,7 +71,7 @@ public void Enumerable_ShouldMap_VpnProtocol(VpnProtocol protocol, string expect { // Arrange VpnEndpoint endpoint = - new(new VpnHost("abc.com", "7.7.7.7", string.Empty, null, string.Empty), protocol, 44444); + new(new VpnHost("abc.com", "7.7.7.7", string.Empty, null, string.Empty, null), protocol, 44444); OpenVpnEndpointArguments subject = new(endpoint); // Act @@ -87,7 +87,7 @@ public void Enumerable_ShouldThrow_WhenProtocolIsNotSupported(VpnProtocol protoc { // Arrange VpnEndpoint endpoint = - new(new VpnHost("abc.com", "1.2.3.4", string.Empty, null, string.Empty), protocol, 54321); + new(new VpnHost("abc.com", "1.2.3.4", string.Empty, null, string.Empty, null), protocol, 54321); OpenVpnEndpointArguments subject = new(endpoint); // Act diff --git a/src/Tests/ProtonVPN.Vpn.Tests/ServerValidation/ServerValidatorTest.cs b/src/Tests/ProtonVPN.Vpn.Tests/ServerValidation/ServerValidatorTest.cs index 409b0f7f..1b0d4656 100644 --- a/src/Tests/ProtonVPN.Vpn.Tests/ServerValidation/ServerValidatorTest.cs +++ b/src/Tests/ProtonVPN.Vpn.Tests/ServerValidation/ServerValidatorTest.cs @@ -94,7 +94,8 @@ private VpnHost CreateVpnHost() ip: SERVER_IP, label: SERVER_LABEL, x25519PublicKey: CreatePublicKey(), - signature: SERVER_SIGNATURE); + signature: SERVER_SIGNATURE, + relayIpByProtocol: null); } private PublicKey CreatePublicKey() @@ -112,7 +113,8 @@ public void TestValidate_WithNullLabel() ip: SERVER_IP, label: null, x25519PublicKey: CreatePublicKey(), - signature: SERVER_SIGNATURE); + signature: SERVER_SIGNATURE, + relayIpByProtocol: null); VpnError error = _serverValidator.Validate(server); @@ -129,7 +131,8 @@ public void TestValidate_WithoutPublicKey() ip: SERVER_IP, label: SERVER_LABEL, x25519PublicKey: null, - signature: SERVER_SIGNATURE); + signature: SERVER_SIGNATURE, + relayIpByProtocol: null); VpnError error = _serverValidator.Validate(server); @@ -144,7 +147,8 @@ public void TestValidate_X25519PublicKeyIsTooShort() ip: SERVER_IP, label: SERVER_LABEL, x25519PublicKey: new PublicKey(new byte[1] { 1 }, KeyAlgorithm.X25519), - signature: SERVER_SIGNATURE); + signature: SERVER_SIGNATURE, + relayIpByProtocol: null); VpnError error = _serverValidator.Validate(server); @@ -166,7 +170,8 @@ private VpnHost CreateVpnHostBySignature(string signature) ip: SERVER_IP, label: SERVER_LABEL, x25519PublicKey: CreatePublicKey(), - signature: signature); + signature: signature, + relayIpByProtocol: null); } [TestMethod] diff --git a/src/Tests/ProtonVPN.Vpn.Tests/WireGuard/WireGuardConnectionTest.cs b/src/Tests/ProtonVPN.Vpn.Tests/WireGuard/WireGuardConnectionTest.cs index be86fd3c..65b63e08 100644 --- a/src/Tests/ProtonVPN.Vpn.Tests/WireGuard/WireGuardConnectionTest.cs +++ b/src/Tests/ProtonVPN.Vpn.Tests/WireGuard/WireGuardConnectionTest.cs @@ -53,7 +53,7 @@ public void ConnectShouldFireErrorEventWhenServerPublicKeyIsNullOrEmpty() // Act wireGuardConnection.Connect( - new VpnEndpoint(new VpnHost("host", "127.0.0.1", "", null, signature: string.Empty), VpnProtocol.WireGuardUdp), + new VpnEndpoint(new VpnHost("host", "127.0.0.1", "", null, signature: string.Empty, null), VpnProtocol.WireGuardUdp), new VpnCredentials("cert", new AsymmetricKeyPair( new SecretKey("U2VjcmV0S2V5", KeyAlgorithm.Unknown), From ac85c7edaa7df17229c79f19b9892aad0f4b96d9 Mon Sep 17 00:00:00 2001 From: sa-l10n-translation Date: Mon, 25 Nov 2024 06:03:24 +0000 Subject: [PATCH 09/12] i18n: Upgrade translations from crowdin (75cab129). --- .../Properties/.locale-state.metadata | 2 +- .../Properties/Resources.el-GR.resx | 4 ++-- .../Properties/Resources.fi-FI.resx | 4 ++-- .../Properties/Resources.pt-PT.resx | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ProtonVPN.Translations/Properties/.locale-state.metadata b/src/ProtonVPN.Translations/Properties/.locale-state.metadata index 62c73181..684b21a0 100644 --- a/src/ProtonVPN.Translations/Properties/.locale-state.metadata +++ b/src/ProtonVPN.Translations/Properties/.locale-state.metadata @@ -1,4 +1,4 @@ { "project": "windows-vpn", - "locale": "b0e0e013e20de5c7795a58f4eb856690278229a5" + "locale": "da78a1a220dcb37eb510e08323cd3097b931f049" } \ No newline at end of file diff --git a/src/ProtonVPN.Translations/Properties/Resources.el-GR.resx b/src/ProtonVPN.Translations/Properties/Resources.el-GR.resx index ce7f7231..e849cf4e 100644 --- a/src/ProtonVPN.Translations/Properties/Resources.el-GR.resx +++ b/src/ProtonVPN.Translations/Properties/Resources.el-GR.resx @@ -2594,7 +2594,7 @@ The label for server status in Server Load popup. Popup opens by pressing mouse button on server load image in Countries tab of Sidebar section of main app window. - Μέσω {0} από {1} + Μέσω {0} σε {1} The heading of Secure Core server info displayed in Server Load popup. The {0} is a placeholder for entry country, {1} - exit country. Popup opens by pressing mouse button on server load image in Countries tab of Sidebar section of main app window. @@ -3225,7 +3225,7 @@ This setting instructs the Proton VPN to start automatically when the user logs Αναβαθμίστε την εμπειρία παιχνιδιού σας - Φραγή διαφημίσεων και ιχνηλατών + Αποκλεισμός διαφημίσεων και ιχνηλατών Περιήγηση με ύψιστη ταχύτητα diff --git a/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx b/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx index 7dbb9285..63c16370 100644 --- a/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx +++ b/src/ProtonVPN.Translations/Properties/Resources.fi-FI.resx @@ -1628,7 +1628,7 @@ Ota se käyttöön seuraamalla <Hyperlink Command="{Binding OpenArticleComman The text displayed in Disconnect modal window if TAP error is detected - VPN-palvelimen varmenteen vahvistuksessa on ongelma, joka saattaa viitata siihen, että verkkoyhteyttä on peukaloitu tai palvelimella on asetusongelma. Pyydä tueltamme apua lähettämällä <Hyperlink Command="{Binding ReportBugCommand}"><Run Text="ongelmaraportti"/></Hyperlink>. + VPN-palvelimen varmenteen vahvistuksessa on ongelma, joka saattaa viitata siihen, että verkkoyhteyttä on peukaloitu tai palvelimella on asetusongelma. Pyydä asiakastueltamme apua lähettämällä <Hyperlink Command="{Binding ReportBugCommand}"><Run Text="ongelmaraportti"/></Hyperlink>. The error description displayed in Disconnect modal window if VPN server certificate validation error has occurred @@ -2488,7 +2488,7 @@ Ota se käyttöön seuraamalla <Hyperlink Command="{Binding OpenArticleComman Mainossivustot käyttävät evästeitä ja seurantoja kohdistettuun markkinointiin. - Tiedonsiirtoa säästetty + Säästetty NetShieldin estämien mainosten, seurantojen ja haittaohjelmien arvioitu yhteiskoko. diff --git a/src/ProtonVPN.Translations/Properties/Resources.pt-PT.resx b/src/ProtonVPN.Translations/Properties/Resources.pt-PT.resx index 4bff1cda..5fe80a38 100644 --- a/src/ProtonVPN.Translations/Properties/Resources.pt-PT.resx +++ b/src/ProtonVPN.Translations/Properties/Resources.pt-PT.resx @@ -354,7 +354,7 @@ Actualizar - Feito + Terminado Saiba mais @@ -2041,7 +2041,7 @@ Siga <Hyperlink Command="{Binding OpenArticleCommand}"><Run Text="these The label for uploaded data amount in Speed Graph on Map in main app window - Faça "upgrade" para o plano Plus + Melhore para o plano Plus LIGADO @@ -2601,7 +2601,7 @@ Siga <Hyperlink Command="{Binding OpenArticleCommand}"><Run Text="these Obtenha uma cobertura global com a VPN Plus - Não é o país que pretendia? Faça 'upgrade' para escolher qualquer servidor + Não é o país que pretendia? Melhore para escolher qualquer servidor. Ligar @@ -2616,7 +2616,7 @@ Siga <Hyperlink Command="{Binding OpenArticleCommand}"><Run Text="these The text on Upgrade button in Server list in Countries tab of Sidebar in main app window (opens Upgrade Required Upsell window) - Faça 'upgrade' para o plano Plus + Melhore para o plano Plus via @@ -2884,7 +2884,7 @@ This setting instructs the Proton VPN to start automatically when the user logs The title of General tab in Settings window - Faça 'upgrade' para o plano Plus + Melhore para o plano Plus A message insde a tooltip which is displayed on mouse hover in the settings next to a paid feature. From 6dc6c267b3e5489fb5204f93131aeeda473504e2 Mon Sep 17 00:00:00 2001 From: Eduardo Abreu Date: Fri, 15 Nov 2024 18:14:52 +0100 Subject: [PATCH 10/12] Remove network interface disabler/enabler --- src/ProtonVPN.Service/Start/Bootstrapper.cs | 10 - src/ProtonVPN.Service/Start/ServiceModule.cs | 4 - src/ProtonVPN.Vpn/Config/Module.cs | 5 +- .../Connection/NetworkAdapterStatusWrapper.cs | 38 +-- .../Networks/Adapters/INetworkAdapter.cs | 34 --- .../Adapters/INetworkAdaptersLoader.cs | 28 -- .../Networks/Adapters/NetConnectionStatus.cs | 38 --- .../Networks/Adapters/NetworkAdapter.cs | 107 -------- .../Adapters/NetworkAdaptersLoader.cs | 40 --- .../Networks/INetworkAdapterManager.cs | 27 -- .../Networks/NetworkAdapterManager.cs | 248 ------------------ 11 files changed, 8 insertions(+), 571 deletions(-) delete mode 100644 src/ProtonVPN.Vpn/Networks/Adapters/INetworkAdapter.cs delete mode 100644 src/ProtonVPN.Vpn/Networks/Adapters/INetworkAdaptersLoader.cs delete mode 100644 src/ProtonVPN.Vpn/Networks/Adapters/NetConnectionStatus.cs delete mode 100644 src/ProtonVPN.Vpn/Networks/Adapters/NetworkAdapter.cs delete mode 100644 src/ProtonVPN.Vpn/Networks/Adapters/NetworkAdaptersLoader.cs delete mode 100644 src/ProtonVPN.Vpn/Networks/INetworkAdapterManager.cs delete mode 100644 src/ProtonVPN.Vpn/Networks/NetworkAdapterManager.cs diff --git a/src/ProtonVPN.Service/Start/Bootstrapper.cs b/src/ProtonVPN.Service/Start/Bootstrapper.cs index cfafc975..94eede62 100644 --- a/src/ProtonVPN.Service/Start/Bootstrapper.cs +++ b/src/ProtonVPN.Service/Start/Bootstrapper.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; using System.ServiceProcess; using Autofac; using ProtonVPN.Api.Installers; @@ -38,7 +37,6 @@ using ProtonVPN.Service.Vpn; using ProtonVPN.Update.Installers; using ProtonVPN.Vpn.Common; -using ProtonVPN.Vpn.Networks; using ProtonVPN.Vpn.OpenVpn; namespace ProtonVPN.Service.Start @@ -87,7 +85,6 @@ private void Start() RegisterEvents(); Resolve().Clean(config.ServiceLogFolder, 10); - FixNetworkAdapters(); VpnService vpnService = Resolve(); ServiceBase.Run(vpnService); @@ -96,13 +93,6 @@ private void Start() logger.Info("= ProtonVPN Service has exited ="); } - private void FixNetworkAdapters() - { - INetworkAdapterManager networkAdapterManager = Resolve(); - networkAdapterManager.DisableDuplicatedWireGuardAdapters(); - networkAdapterManager.EnableOpenVpnAdapters(); - } - private void RegisterEvents() { Resolve().StateChanged += (_, e) => diff --git a/src/ProtonVPN.Service/Start/ServiceModule.cs b/src/ProtonVPN.Service/Start/ServiceModule.cs index f4c2f4c8..56b380ed 100644 --- a/src/ProtonVPN.Service/Start/ServiceModule.cs +++ b/src/ProtonVPN.Service/Start/ServiceModule.cs @@ -47,8 +47,6 @@ using ProtonVPN.Service.Vpn; using ProtonVPN.Vpn.Common; using ProtonVPN.Vpn.Connection; -using ProtonVPN.Vpn.Networks; -using ProtonVPN.Vpn.Networks.Adapters; using Module = Autofac.Module; namespace ProtonVPN.Service.Start @@ -128,8 +126,6 @@ protected override void Load(ContainerBuilder builder) .AsSelf() .SingleInstance(); - builder.RegisterType().AsImplementedInterfaces().SingleInstance(); - builder.RegisterType().AsImplementedInterfaces().SingleInstance(); builder.RegisterType().AsImplementedInterfaces().SingleInstance(); builder.RegisterType().AsImplementedInterfaces().SingleInstance(); builder.RegisterType().AsImplementedInterfaces().SingleInstance(); diff --git a/src/ProtonVPN.Vpn/Config/Module.cs b/src/ProtonVPN.Vpn/Config/Module.cs index ab38594e..dad486cd 100644 --- a/src/ProtonVPN.Vpn/Config/Module.cs +++ b/src/ProtonVPN.Vpn/Config/Module.cs @@ -19,13 +19,13 @@ using Autofac; using ProtonVPN.Common.Configuration; -using ProtonVPN.Logging.Contracts; using ProtonVPN.Common.OS.Net; using ProtonVPN.Common.OS.Processes; using ProtonVPN.Common.OS.Services; using ProtonVPN.Common.Threading; using ProtonVPN.Crypto; using ProtonVPN.IssueReporting.Contracts; +using ProtonVPN.Logging.Contracts; using ProtonVPN.Vpn.Common; using ProtonVPN.Vpn.Connection; using ProtonVPN.Vpn.Gateways; @@ -33,7 +33,6 @@ using ProtonVPN.Vpn.Management; using ProtonVPN.Vpn.NetShield; using ProtonVPN.Vpn.NetworkAdapters; -using ProtonVPN.Vpn.Networks; using ProtonVPN.Vpn.OpenVpn; using ProtonVPN.Vpn.PortMapping; using ProtonVPN.Vpn.PortMapping.Serializers.Common; @@ -93,7 +92,6 @@ private void RegisterPortMapping(ContainerBuilder builder) public IVpnConnection GetVpnConnection(IComponentContext c) { ILogger logger = c.Resolve(); - INetworkAdapterManager networkAdapterManager = c.Resolve(); INetworkInterfaceLoader networkInterfaceLoader = c.Resolve(); ITaskQueue taskQueue = c.Resolve(); IEndpointScanner endpointScanner = c.Resolve(); @@ -121,7 +119,6 @@ public IVpnConnection GetVpnConnection(IComponentContext c) new NetworkAdapterStatusWrapper( logger, issueReporter, - networkAdapterManager, networkInterfaceLoader, c.Resolve(), c.Resolve(), diff --git a/src/ProtonVPN.Vpn/Connection/NetworkAdapterStatusWrapper.cs b/src/ProtonVPN.Vpn/Connection/NetworkAdapterStatusWrapper.cs index 4a1a01ae..6d1754df 100644 --- a/src/ProtonVPN.Vpn/Connection/NetworkAdapterStatusWrapper.cs +++ b/src/ProtonVPN.Vpn/Connection/NetworkAdapterStatusWrapper.cs @@ -20,18 +20,17 @@ using System; using ProtonVPN.Common; using ProtonVPN.Common.Extensions; -using ProtonVPN.Logging.Contracts; -using ProtonVPN.Logging.Contracts.Events.ConnectLogs; -using ProtonVPN.Logging.Contracts.Events.DisconnectLogs; -using ProtonVPN.Logging.Contracts.Events.NetworkLogs; using ProtonVPN.Common.Networking; using ProtonVPN.Common.OS.Net; using ProtonVPN.Common.OS.Net.NetworkInterface; using ProtonVPN.Common.Vpn; using ProtonVPN.IssueReporting.Contracts; +using ProtonVPN.Logging.Contracts; +using ProtonVPN.Logging.Contracts.Events.ConnectLogs; +using ProtonVPN.Logging.Contracts.Events.DisconnectLogs; +using ProtonVPN.Logging.Contracts.Events.NetworkLogs; using ProtonVPN.Vpn.Common; using ProtonVPN.Vpn.NetworkAdapters; -using ProtonVPN.Vpn.Networks; namespace ProtonVPN.Vpn.Connection { @@ -39,7 +38,6 @@ internal class NetworkAdapterStatusWrapper : ISingleVpnConnection { private readonly ILogger _logger; private readonly IIssueReporter _issueReporter; - private readonly INetworkAdapterManager _networkAdapterManager; private readonly INetworkInterfaceLoader _networkInterfaceLoader; private readonly WintunAdapter _wintunAdapter; private readonly TapAdapter _tapAdapter; @@ -53,7 +51,6 @@ internal class NetworkAdapterStatusWrapper : ISingleVpnConnection public NetworkAdapterStatusWrapper( ILogger logger, IIssueReporter issueReporter, - INetworkAdapterManager networkAdapterManager, INetworkInterfaceLoader networkInterfaceLoader, WintunAdapter wintunAdapter, TapAdapter tapAdapter, @@ -61,7 +58,6 @@ public NetworkAdapterStatusWrapper( { _logger = logger; _issueReporter = issueReporter; - _networkAdapterManager = networkAdapterManager; _wintunAdapter = wintunAdapter; _tapAdapter = tapAdapter; _networkInterfaceLoader = networkInterfaceLoader; @@ -141,7 +137,6 @@ private bool IsOpenVpnNetworkAdapterAvailable(OpenVpnAdapter? openVpnAdapter) private void HandleOpenVpnAdapterUnavailable() { - EnableOpenVpnAdapters(); if (IsOpenVpnNetworkAdapterAvailable(_config.OpenVpnAdapter)) { _logger.Info("OpenVPN network adapter successfully enabled " + @@ -158,13 +153,6 @@ private void HandleOpenVpnAdapterUnavailable() } } - private void EnableOpenVpnAdapters() - { - _logger.Warn($"OpenVPN network adapter not found (Protocol '{_endpoint.VpnProtocol}', " + - $"Adapter '{_config.OpenVpnAdapter}'). Attempting to enable them if disabled."); - _networkAdapterManager.EnableOpenVpnAdapters(); - } - private void HandleNoTunError() { _logger.Warn("OpenVPN TUN network adapter not found. Checking if TAP is available."); @@ -223,11 +211,7 @@ public void RequestNetShieldStats() private void Origin_StateChanged(object sender, EventArgs e) { - if (e.Data.Status == VpnStatus.Connected && e.Data.VpnProtocol.IsWireGuard()) - { - HandleConnectedWithWireGuard(); - } - else if (e.Data.Status == VpnStatus.Disconnected && e.Data.VpnProtocol.IsOpenVpn()) + if (e.Data.Status == VpnStatus.Disconnected && e.Data.VpnProtocol.IsOpenVpn()) { _wintunAdapter.Close(); } @@ -241,12 +225,6 @@ private void Origin_StateChanged(object sender, EventArgs e) StateChanged?.Invoke(this, e); } - private void HandleConnectedWithWireGuard() - { - _logger.Info("Connected with WireGuard. Disabling duplicated WireGuard adapters."); - _networkAdapterManager.DisableDuplicatedWireGuardAdapters(); - } - private void HandleVpnError(VpnState vpnState) { switch (vpnState.VpnProtocol) @@ -270,15 +248,13 @@ private void HandleVpnError(VpnState vpnState) private void HandleWireGuardError(VpnState vpnState) { _logger.Warn($"Connection error '{vpnState.Error}' while using " + - $"protocol '{vpnState.VpnProtocol}'. Disabling duplicated WireGuard adapters."); - _networkAdapterManager.DisableDuplicatedWireGuardAdapters(); + $"protocol '{vpnState.VpnProtocol}'."); } private void HandleOpenVpnError(VpnState vpnState) { _logger.Warn($"Connection error '{vpnState.Error}' while using " + - $"protocol '{vpnState.VpnProtocol}'. Enabling disabled OpenVPN adapters."); - _networkAdapterManager.EnableOpenVpnAdapters(); + $"protocol '{vpnState.VpnProtocol}'."); } } } \ No newline at end of file diff --git a/src/ProtonVPN.Vpn/Networks/Adapters/INetworkAdapter.cs b/src/ProtonVPN.Vpn/Networks/Adapters/INetworkAdapter.cs deleted file mode 100644 index 520bb557..00000000 --- a/src/ProtonVPN.Vpn/Networks/Adapters/INetworkAdapter.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2023 Proton AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ - -namespace ProtonVPN.Vpn.Networks.Adapters -{ - public interface INetworkAdapter - { - string NetConnectionId { get; } - string Name { get; } - string Description { get; } - string ProductName { get; } - NetConnectionStatus? NetConnectionStatus { get; } - - void Enable(); - void Disable(); - string GenerateLoggingDescription(); - } -} \ No newline at end of file diff --git a/src/ProtonVPN.Vpn/Networks/Adapters/INetworkAdaptersLoader.cs b/src/ProtonVPN.Vpn/Networks/Adapters/INetworkAdaptersLoader.cs deleted file mode 100644 index eb4da5ce..00000000 --- a/src/ProtonVPN.Vpn/Networks/Adapters/INetworkAdaptersLoader.cs +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2023 Proton AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ - -using System.Collections.Generic; - -namespace ProtonVPN.Vpn.Networks.Adapters -{ - public interface INetworkAdaptersLoader - { - IList GetAll(); - } -} \ No newline at end of file diff --git a/src/ProtonVPN.Vpn/Networks/Adapters/NetConnectionStatus.cs b/src/ProtonVPN.Vpn/Networks/Adapters/NetConnectionStatus.cs deleted file mode 100644 index 79249910..00000000 --- a/src/ProtonVPN.Vpn/Networks/Adapters/NetConnectionStatus.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2023 Proton AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ - -namespace ProtonVPN.Vpn.Networks.Adapters -{ - public enum NetConnectionStatus - { - Disconnected = 0, - Connecting = 1, - Connected = 2, - Disconnecting = 3, - HardwareNotPresent = 4, - HardwareDisabled = 5, - HardwareMalfunction = 6, - MediaDisconnected = 7, - Authenticating = 8, - AuthenticationSucceeded = 9, - AuthenticationFailed = 10, - InvalidAddress = 11, - CredentialsRequired = 12 - } -} \ No newline at end of file diff --git a/src/ProtonVPN.Vpn/Networks/Adapters/NetworkAdapter.cs b/src/ProtonVPN.Vpn/Networks/Adapters/NetworkAdapter.cs deleted file mode 100644 index 78f4a585..00000000 --- a/src/ProtonVPN.Vpn/Networks/Adapters/NetworkAdapter.cs +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2023 Proton AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ - -using System.Management; - -namespace ProtonVPN.Vpn.Networks.Adapters -{ - public class NetworkAdapter : INetworkAdapter - { - public const string NET_CONNECTION_ID_KEY = "NetConnectionID"; - public const string NAME_KEY = "Name"; - public const string DESCRIPTION_KEY = "Description"; - public const string PRODUCT_NAME_KEY = "ProductName"; - public const string NET_CONNECTION_STATUS_KEY = "NetConnectionStatus"; - - public string NetConnectionId { get; private set; } - public string Name { get; private set; } - public string Description { get; private set; } - public string ProductName { get; private set; } - public NetConnectionStatus? NetConnectionStatus { get; private set; } - - private readonly ManagementObject _managementObject; - - public NetworkAdapter(ManagementObject managementObject) - { - _managementObject = managementObject; - MapProperties(); - } - - private void MapProperties() - { - NetConnectionId = GetNetworkAdapterPropertyValueOrDefault(_managementObject, NET_CONNECTION_ID_KEY); - Name = GetNetworkAdapterPropertyValueOrDefault(_managementObject, NAME_KEY); - Description = GetNetworkAdapterPropertyValueOrDefault(_managementObject, DESCRIPTION_KEY); - ProductName = GetNetworkAdapterPropertyValueOrDefault(_managementObject, PRODUCT_NAME_KEY); - NetConnectionStatus = GetNetConnectionStatusOrNull(_managementObject); - } - - private T GetNetworkAdapterPropertyValueOrDefault(ManagementObject networkAdapter, string propertyKey) - { - T value; - try - { - value = (T)networkAdapter[propertyKey]; - } - catch - { - value = default(T); - } - - return value; - } - - private NetConnectionStatus? GetNetConnectionStatusOrNull(ManagementObject networkAdapter) - { - ushort netConnectionStatusInt = GetNetworkAdapterPropertyValueOrDefault( - networkAdapter, NET_CONNECTION_STATUS_KEY); - NetConnectionStatus? netConnectionStatus; - try - { - netConnectionStatus = (NetConnectionStatus)netConnectionStatusInt; - } - catch - { - netConnectionStatus = null; - } - - return netConnectionStatus; - } - - public void Enable() - { - _managementObject.InvokeMethod("Enable", null); - } - - public void Disable() - { - _managementObject.InvokeMethod("Disable", null); - } - - public string GenerateLoggingDescription() - { - return - $"{NET_CONNECTION_ID_KEY} '{NetConnectionId}', " + - $"{NAME_KEY} '{Name}', " + - $"{DESCRIPTION_KEY} '{Description}', " + - $"{PRODUCT_NAME_KEY} '{ProductName}', " + - $"{NET_CONNECTION_STATUS_KEY} '{NetConnectionStatus}'"; - } - } -} \ No newline at end of file diff --git a/src/ProtonVPN.Vpn/Networks/Adapters/NetworkAdaptersLoader.cs b/src/ProtonVPN.Vpn/Networks/Adapters/NetworkAdaptersLoader.cs deleted file mode 100644 index bb5f1793..00000000 --- a/src/ProtonVPN.Vpn/Networks/Adapters/NetworkAdaptersLoader.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 Proton AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ - -using System.Collections.Generic; -using System.Management; - -namespace ProtonVPN.Vpn.Networks.Adapters -{ - public class NetworkAdaptersLoader : INetworkAdaptersLoader - { - public IList GetAll() - { - SelectQuery query = new("Win32_NetworkAdapter"); - ManagementObjectSearcher search = new(query); - IList networkAdapters = new List(); - foreach (ManagementObject result in search.Get()) - { - networkAdapters.Add(new NetworkAdapter(result)); - } - - return networkAdapters; - } - } -} \ No newline at end of file diff --git a/src/ProtonVPN.Vpn/Networks/INetworkAdapterManager.cs b/src/ProtonVPN.Vpn/Networks/INetworkAdapterManager.cs deleted file mode 100644 index f2ea0c49..00000000 --- a/src/ProtonVPN.Vpn/Networks/INetworkAdapterManager.cs +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2023 Proton AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ - -namespace ProtonVPN.Vpn.Networks -{ - public interface INetworkAdapterManager - { - int DisableDuplicatedWireGuardAdapters(); - int EnableOpenVpnAdapters(); - } -} \ No newline at end of file diff --git a/src/ProtonVPN.Vpn/Networks/NetworkAdapterManager.cs b/src/ProtonVPN.Vpn/Networks/NetworkAdapterManager.cs deleted file mode 100644 index 70e2b162..00000000 --- a/src/ProtonVPN.Vpn/Networks/NetworkAdapterManager.cs +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (c) 2023 Proton AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using ProtonVPN.Common.Extensions; -using ProtonVPN.Logging.Contracts; -using ProtonVPN.Logging.Contracts.Events.NetworkLogs; -using ProtonVPN.Vpn.Networks.Adapters; - -namespace ProtonVPN.Vpn.Networks -{ - public class NetworkAdapterManager : INetworkAdapterManager - { - private const string WIREGUARD_NETWORK_ADAPTER_NAME = "ProtonVPN"; - private const string WIREGUARD_TUN_NAME = "WireGuard Tunnel"; - private const string PROTON_VPN_TAP_NAME = "TAP-ProtonVPN"; - private const string PROTON_VPN_TUN_NAME = "ProtonVPN Tunnel"; - private const string PROTON_VPN_TUN_NET_CONNECTION_ID = "ProtonVPN TUN"; - - private readonly ILogger _logger; - private readonly INetworkAdaptersLoader _networkAdaptersLoader; - - public NetworkAdapterManager(ILogger logger, INetworkAdaptersLoader networkAdaptersLoader) - { - _logger = logger; - _networkAdaptersLoader = networkAdaptersLoader; - } - - public int DisableDuplicatedWireGuardAdapters() - { - _logger.Info("Checking for duplicate WireGuard adapters."); - int disabledAdapters = 0; - try - { - IList duplicatedWireGuardAdapters = GetDuplicatedWireGuardAdapters(); - if (duplicatedWireGuardAdapters.Any()) - { - _logger.Warn($"Found {duplicatedWireGuardAdapters.Count} " + - "duplicated WireGuard network adapter(s) to be disabled."); - disabledAdapters = DisableAdapters(duplicatedWireGuardAdapters); - _logger.Info($"Disabled {disabledAdapters} duplicated WireGuard network adapter(s)."); - } - } - catch (Exception e) - { - _logger.Error("An error occurred when disabling duplicated WireGuard network adapters.", e); - disabledAdapters = 0; - } - - return disabledAdapters; - } - - private IList GetDuplicatedWireGuardAdapters() - { - IList networkAdapters = _networkAdaptersLoader.GetAll(); - IList duplicatedWireGuardAdapters = new List(); - foreach (INetworkAdapter networkAdapter in networkAdapters) - { - if (IsDuplicatedWireGuardAdapter(networkAdapter)) - { - duplicatedWireGuardAdapters.Add(networkAdapter); - } - } - - return duplicatedWireGuardAdapters; - } - - private bool IsDuplicatedWireGuardAdapter(INetworkAdapter networkAdapter) - { - return IsAdapterNetConnectionIdDuplicated(networkAdapter) && - IsWireGuardAdapter(networkAdapter); - } - - private bool IsAdapterNetConnectionIdDuplicated(INetworkAdapter networkAdapter) - { - return networkAdapter.NetConnectionId.IsNotNullAndContains(WIREGUARD_NETWORK_ADAPTER_NAME) && - networkAdapter.NetConnectionId != WIREGUARD_NETWORK_ADAPTER_NAME; - } - - private bool IsWireGuardAdapter(INetworkAdapter networkAdapter) - { - return IsAtLeastOnePropertyContainingKey(WIREGUARD_TUN_NAME, - networkAdapter.Name, networkAdapter.Description, networkAdapter.ProductName); - } - - private bool IsAtLeastOnePropertyContainingKey(string key, params string[] properties) - { - return properties?.FirstOrDefault(p => p != null && p.Contains(key)) != null; - } - - private int DisableAdapters(IList networkAdapters) - { - int disabledAdapters = 0; - foreach (INetworkAdapter adapter in networkAdapters) - { - bool wasAdapterDisabled = DisableAdapter(adapter); - if (wasAdapterDisabled) - { - disabledAdapters++; - } - } - - return disabledAdapters; - } - - private bool DisableAdapter(INetworkAdapter networkAdapter) - { - string adapterDescription = networkAdapter.GenerateLoggingDescription(); - bool wasAdapterDisabled = false; - if (networkAdapter.NetConnectionStatus == null || - networkAdapter.NetConnectionStatus != NetConnectionStatus.Disconnected) - { - try - { - networkAdapter.Disable(); - _logger.Warn($"Disabled network adapter. {adapterDescription}"); - wasAdapterDisabled = true; - } - catch (Exception e) - { - _logger.Error($"Failed to disable network adapter. {adapterDescription}", e); - } - } - else - { - _logger.Info($"The network adapter is already Disconnected. {adapterDescription}"); - } - - return wasAdapterDisabled; - } - - public int EnableOpenVpnAdapters() - { - _logger.Info("Checking for OpenVPN adapters."); - int enabledAdapters = 0; - try - { - IList openVpnAdapters = GetOpenVpnAdapters(); - if (openVpnAdapters.Any()) - { - _logger.Info($"Found {openVpnAdapters.Count} OpenVPN network adapter(s). " + - $"Attempting to enable the disconnected ones."); - enabledAdapters = EnableAdapters(openVpnAdapters); - _logger.Info($"Enabled {enabledAdapters} OpenVPN network adapter(s)."); - } - } - catch (Exception e) - { - _logger.Error("An error occurred when enabling OpenVPN network adapters.", e); - enabledAdapters = 0; - } - - return enabledAdapters; - } - - private IList GetOpenVpnAdapters() - { - IList networkAdapters = _networkAdaptersLoader.GetAll(); - IList openVpnAdapters = new List(); - foreach (INetworkAdapter networkAdapter in networkAdapters) - { - if (IsOpenVpnAdapter(networkAdapter)) - { - openVpnAdapters.Add(networkAdapter); - } - } - - return openVpnAdapters; - } - - private bool IsOpenVpnAdapter(INetworkAdapter networkAdapter) - { - return IsOpenVpnTapAdapter(networkAdapter) || IsOpenVpnTunAdapter(networkAdapter); - } - - private bool IsOpenVpnTapAdapter(INetworkAdapter networkAdapter) - { - return IsAtLeastOnePropertyContainingKey(PROTON_VPN_TAP_NAME, - networkAdapter.Name, networkAdapter.Description, networkAdapter.ProductName); - } - - private bool IsOpenVpnTunAdapter(INetworkAdapter networkAdapter) - { - return networkAdapter.NetConnectionId.IsNotNullAndContains(PROTON_VPN_TUN_NET_CONNECTION_ID) || - networkAdapter.Name.IsNotNullAndContains(PROTON_VPN_TUN_NAME); - } - - private int EnableAdapters(IList networkAdapters) - { - int enabledAdapters = 0; - foreach (INetworkAdapter networkAdapter in networkAdapters) - { - bool wasAdapterEnabled = EnableAdapter(networkAdapter); - if (wasAdapterEnabled) - { - enabledAdapters++; - } - } - - return enabledAdapters; - } - - private bool EnableAdapter(INetworkAdapter networkAdapter) - { - string adapterDescription = networkAdapter.GenerateLoggingDescription(); - bool wasAdapterEnabled = false; - if (networkAdapter.NetConnectionStatus == null || - networkAdapter.NetConnectionStatus != NetConnectionStatus.Connected) - { - try - { - networkAdapter.Enable(); - _logger.Warn($"Enabled network adapter. {adapterDescription}"); - wasAdapterEnabled = true; - } - catch (Exception e) - { - _logger.Error($"Failed to enable network adapter. {adapterDescription}", e); - } - } - else - { - _logger.Info($"The network adapter is not disconnected. " + - $"Its status is '{networkAdapter.NetConnectionStatus}'. {adapterDescription}"); - } - - return wasAdapterEnabled; - } - } -} From 8376dbfc0d5979967297a41854f8f5553c8770a0 Mon Sep 17 00:00:00 2001 From: Eduardo Abreu Date: Mon, 25 Nov 2024 12:53:27 +0000 Subject: [PATCH 11/12] Fix duplicated IP pinging [VPNWIN-2478] --- .../Servers/LogicalServerResponse.cs | 2 +- .../Connection/ReconnectingWrapper.cs | 8 ++++---- .../Connection/VpnEndpointCandidates.cs | 15 ++++++++++----- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Api/ProtonVPN.Api.Contracts/Servers/LogicalServerResponse.cs b/src/Api/ProtonVPN.Api.Contracts/Servers/LogicalServerResponse.cs index 92f6c0fa..0f158cf8 100644 --- a/src/Api/ProtonVPN.Api.Contracts/Servers/LogicalServerResponse.cs +++ b/src/Api/ProtonVPN.Api.Contracts/Servers/LogicalServerResponse.cs @@ -41,7 +41,7 @@ public class LogicalServerResponse public string HostCountry { get; set; } public string GatewayName { get; set; } public List Servers { get; set; } - + public static LogicalServerResponse Empty => new() { Id = string.Empty, diff --git a/src/ProtonVPN.Vpn/Connection/ReconnectingWrapper.cs b/src/ProtonVPN.Vpn/Connection/ReconnectingWrapper.cs index e86161f5..995c37b9 100644 --- a/src/ProtonVPN.Vpn/Connection/ReconnectingWrapper.cs +++ b/src/ProtonVPN.Vpn/Connection/ReconnectingWrapper.cs @@ -148,7 +148,7 @@ private void Origin_StateChanged(object sender, EventArgs e) { _logger.Info("Trying the next server. " + $"Status: '{_state.Status}', Error: '{_state.Error}'."); - ConnectToNextEndpoint(); + ConnectToNextEndpoint(skipCurrentIp: _state.Error == VpnError.PingTimeoutError); } OnStateChanged(FilterVpnState(_state)); @@ -245,7 +245,7 @@ private void HandleEndpointResponse(bool isResponding, CancellationToken cancell if (isResponding) { _logger.Info("At least one server has responded to a ping. Attempting connections."); - ConnectToNextEndpoint(); + ConnectToNextEndpoint(skipCurrentIp: false); } else { @@ -255,9 +255,9 @@ private void HandleEndpointResponse(bool isResponding, CancellationToken cancell } } - private void ConnectToNextEndpoint() + private void ConnectToNextEndpoint(bool skipCurrentIp) { - _endpoint = _candidates.NextHost(_config); + _endpoint = skipCurrentIp ? _candidates.NextIp(_config) : _candidates.NextHost(_config); bool isEndpointAvailableToConnect = _endpoint?.Server != null && !_endpoint.Server.IsEmpty(); if (isEndpointAvailableToConnect) { diff --git a/src/ProtonVPN.Vpn/Connection/VpnEndpointCandidates.cs b/src/ProtonVPN.Vpn/Connection/VpnEndpointCandidates.cs index 0720d90a..f4e79afb 100644 --- a/src/ProtonVPN.Vpn/Connection/VpnEndpointCandidates.cs +++ b/src/ProtonVPN.Vpn/Connection/VpnEndpointCandidates.cs @@ -62,7 +62,15 @@ public VpnEndpoint NextHost(VpnConfig config) _skippedHosts[config.VpnProtocol].Add(Current.Server); } - VpnHost server = _all.FirstOrDefault(h => _skippedHosts[config.VpnProtocol].All(skippedHost => h != skippedHost)); + return NextEndpoint(config); + } + + private VpnEndpoint NextEndpoint(VpnConfig config) + { + VpnHost server = _all.FirstOrDefault(h => + _skippedHosts[config.VpnProtocol].All(skippedHost => h != skippedHost) && + _skippedIps[config.VpnProtocol].All(skippedIp => h.Ip != skippedIp)); + Current = CreateVpnEndpoint(server, config.VpnProtocol); return Current; @@ -75,10 +83,7 @@ public VpnEndpoint NextIp(VpnConfig config) _skippedIps[config.VpnProtocol].Add(Current.Server.Ip); } - VpnHost server = _all.FirstOrDefault(h => _skippedIps[config.VpnProtocol].All(skippedIp => h.Ip != skippedIp)); - Current = CreateVpnEndpoint(server, config.VpnProtocol); - - return Current; + return NextEndpoint(config); } private static VpnEndpoint CreateVpnEndpoint(VpnHost server, VpnProtocol protocol) From d444da44f9da225ab9b04cbf3ad9add2e952bdac Mon Sep 17 00:00:00 2001 From: Mindaugas Veblauskas Date: Mon, 25 Nov 2024 12:54:51 +0000 Subject: [PATCH 12/12] Increase app version to 3.5.0 --- src/GlobalAssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs index e3bbc25c..96474453 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/src/GlobalAssemblyInfo.cs @@ -14,8 +14,8 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("3.4.3.0")] -[assembly: AssemblyFileVersion("3.4.3.0")] +[assembly: AssemblyVersion("3.5.0.0")] +[assembly: AssemblyFileVersion("3.5.0.0")] [assembly: ComVisible(false)] [assembly: AssemblyInformationalVersion("$AssemblyVersion")] [assembly: SupportedOSPlatform("windows")] \ No newline at end of file