Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01e9274f7b | ||
|
|
daff23bfb3 | ||
|
|
aa6d26e062 | ||
|
|
b2859d5a06 | ||
|
|
ad8b0657cb | ||
|
|
c930a4e1be | ||
|
|
22834e9477 | ||
|
|
62c2679da2 | ||
|
|
5e9ae9fa1f | ||
|
|
0f19bcfebd | ||
|
|
83fc91d3c6 | ||
|
|
4adeec8223 | ||
|
|
64bfbaa45d |
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -56,7 +56,6 @@ body:
|
|||||||
- IVPN
|
- IVPN
|
||||||
- Mullvad
|
- Mullvad
|
||||||
- NordVPN
|
- NordVPN
|
||||||
- OVPN
|
|
||||||
- Privado
|
- Privado
|
||||||
- Private Internet Access
|
- Private Internet Access
|
||||||
- PrivateVPN
|
- PrivateVPN
|
||||||
|
|||||||
2
.github/labels.yml
vendored
2
.github/labels.yml
vendored
@@ -62,8 +62,6 @@
|
|||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ NordVPN"
|
- name: "☁️ NordVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ OVPN"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ Perfect Privacy"
|
- name: "☁️ Perfect Privacy"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ PIA"
|
- name: "☁️ PIA"
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ issues:
|
|||||||
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
||||||
linters:
|
linters:
|
||||||
- ireturn
|
- ireturn
|
||||||
|
- source: "^\\/\\/ https\\:\\/\\/.+$"
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# # ProtonVPN only:
|
# # ProtonVPN only:
|
||||||
SECURE_CORE_ONLY= \
|
SECURE_CORE_ONLY= \
|
||||||
TOR_ONLY= \
|
TOR_ONLY= \
|
||||||
# # Surfshark and ovpn only:
|
# # Surfshark only:
|
||||||
MULTIHOP_ONLY= \
|
MULTIHOP_ONLY= \
|
||||||
# # VPN Secure only:
|
# # VPN Secure only:
|
||||||
PREMIUM_ONLY= \
|
PREMIUM_ONLY= \
|
||||||
@@ -204,6 +204,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
UPDATER_PERIOD=0 \
|
UPDATER_PERIOD=0 \
|
||||||
UPDATER_MIN_RATIO=0.8 \
|
UPDATER_MIN_RATIO=0.8 \
|
||||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||||
|
UPDATER_PROTONVPN_EMAIL= \
|
||||||
|
UPDATER_PROTONVPN_PASSWORD= \
|
||||||
# Public IP
|
# Public IP
|
||||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||||
PUBLICIP_ENABLED=on \
|
PUBLICIP_ENABLED=on \
|
||||||
|
|||||||
@@ -57,10 +57,10 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
||||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Ovpn**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||||
- Supports OpenVPN for all providers listed
|
- Supports OpenVPN for all providers listed
|
||||||
- Supports Wireguard both kernelspace and userspace
|
- Supports Wireguard both kernelspace and userspace
|
||||||
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Ovpn**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
||||||
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
||||||
|
|||||||
@@ -420,7 +420,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
|
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||||
|
openvpnFileExtractor, allSettings.Updater)
|
||||||
|
|
||||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||||
|
|||||||
14
go.mod
14
go.mod
@@ -3,6 +3,7 @@ module github.com/qdm12/gluetun
|
|||||||
go 1.23
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ProtonMail/go-srp v0.0.7
|
||||||
github.com/breml/rootcerts v0.2.19
|
github.com/breml/rootcerts v0.2.19
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
@@ -22,16 +23,20 @@ require (
|
|||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
golang.org/x/net v0.31.0
|
golang.org/x/net v0.31.0
|
||||||
golang.org/x/sys v0.27.0
|
golang.org/x/sys v0.30.0
|
||||||
golang.org/x/text v0.20.0
|
golang.org/x/text v0.22.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.0 // indirect
|
||||||
|
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
@@ -42,6 +47,7 @@ require (
|
|||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/miekg/dns v1.1.62 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
@@ -50,9 +56,9 @@ require (
|
|||||||
github.com/qdm12/goservices v0.1.0 // indirect
|
github.com/qdm12/goservices v0.1.0 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
golang.org/x/crypto v0.29.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
golang.org/x/sync v0.9.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/tools v0.26.0 // indirect
|
golang.org/x/tools v0.26.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/protobuf v1.35.1 // indirect
|
google.golang.org/protobuf v1.35.1 // indirect
|
||||||
|
|||||||
56
go.sum
56
go.sum
@@ -1,9 +1,23 @@
|
|||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/breml/rootcerts v0.2.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U=
|
github.com/breml/rootcerts v0.2.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U=
|
||||||
github.com/breml/rootcerts v0.2.19/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
github.com/breml/rootcerts v0.2.19/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||||
|
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||||
|
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||||
|
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
@@ -43,6 +57,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
@@ -84,46 +100,70 @@ github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZla
|
|||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ func newNoopLogger() *noopLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *noopLogger) Info(string) {}
|
func (l *noopLogger) Info(string) {}
|
||||||
|
func (l *noopLogger) Warn(string) {}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
|||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
|
|
||||||
providers := provider.NewProviders(storage, time.Now, warner, client,
|
providers := provider.NewProviders(storage, time.Now, warner, client,
|
||||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
||||||
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
||||||
connection, err := providerConf.GetConnection(
|
connection, err := providerConf.GetConnection(
|
||||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
||||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||||
|
ErrUsernameMissing = errors.New("username is required for this provider")
|
||||||
|
ErrPasswordMissing = errors.New("password is required for this provider")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdaterLogger interface {
|
type UpdaterLogger interface {
|
||||||
@@ -35,7 +38,7 @@ type UpdaterLogger interface {
|
|||||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||||
options := settings.Updater{}
|
options := settings.Updater{}
|
||||||
var endUserMode, maintainerMode, updateAll bool
|
var endUserMode, maintainerMode, updateAll bool
|
||||||
var csvProviders, ipToken string
|
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
||||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||||
@@ -47,6 +50,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
||||||
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
||||||
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
||||||
|
flagSet.StringVar(&protonUsername, "proton-username", "",
|
||||||
|
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
|
||||||
|
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
|
||||||
|
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -64,6 +71,16 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
options.Providers = strings.Split(csvProviders, ",")
|
options.Providers = strings.Split(csvProviders, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||||
|
if protonEmail == "" && protonUsername != "" {
|
||||||
|
protonEmail = protonUsername + "@protonmail.com"
|
||||||
|
logger.Warn("use -proton-email instead of -proton-username in the future. " +
|
||||||
|
"This assumes the email is " + protonEmail + " and may not work.")
|
||||||
|
}
|
||||||
|
options.ProtonEmail = &protonEmail
|
||||||
|
options.ProtonPassword = &protonPassword
|
||||||
|
}
|
||||||
|
|
||||||
options.SetDefaults(options.Providers[0])
|
options.SetDefaults(options.Providers[0])
|
||||||
|
|
||||||
err := options.Validate()
|
err := options.Validate()
|
||||||
@@ -94,7 +111,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
|
|
||||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||||
|
|
||||||
updater := updater.New(httpClient, storage, providers, logger)
|
updater := updater.New(httpClient, storage, providers, logger)
|
||||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ var (
|
|||||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||||
|
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
|
||||||
|
ErrUpdaterProtonEmailMissing = errors.New("proton email is missing")
|
||||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||||
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
// no restriction on port
|
// no restriction on port
|
||||||
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
||||||
providers.Ovpn, providers.Privatevpn, providers.Torguard:
|
providers.Privatevpn, providers.Torguard:
|
||||||
// no custom port allowed
|
// no custom port allowed
|
||||||
case providers.Expressvpn, providers.Fastestvpn,
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
|
|||||||
providers.Ivpn,
|
providers.Ivpn,
|
||||||
providers.Mullvad,
|
providers.Mullvad,
|
||||||
providers.Nordvpn,
|
providers.Nordvpn,
|
||||||
providers.Ovpn,
|
|
||||||
providers.Protonvpn,
|
providers.Protonvpn,
|
||||||
providers.Surfshark,
|
providers.Surfshark,
|
||||||
providers.Windscribe,
|
providers.Windscribe,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -31,6 +32,10 @@ type Updater struct {
|
|||||||
// Providers is the list of VPN service providers
|
// Providers is the list of VPN service providers
|
||||||
// to update server information for.
|
// to update server information for.
|
||||||
Providers []string
|
Providers []string
|
||||||
|
// ProtonEmail is the email to authenticate with the Proton API.
|
||||||
|
ProtonEmail *string
|
||||||
|
// ProtonPassword is the password to authenticate with the Proton API.
|
||||||
|
ProtonPassword *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Updater) Validate() (err error) {
|
func (u Updater) Validate() (err error) {
|
||||||
@@ -51,6 +56,18 @@ func (u Updater) Validate() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if provider == providers.Protonvpn {
|
||||||
|
authenticatedAPI := *u.ProtonEmail != "" || *u.ProtonPassword != ""
|
||||||
|
if authenticatedAPI {
|
||||||
|
switch {
|
||||||
|
case *u.ProtonEmail == "":
|
||||||
|
return fmt.Errorf("%w", ErrUpdaterProtonEmailMissing)
|
||||||
|
case *u.ProtonPassword == "":
|
||||||
|
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -58,10 +75,12 @@ func (u Updater) Validate() (err error) {
|
|||||||
|
|
||||||
func (u *Updater) copy() (copied Updater) {
|
func (u *Updater) copy() (copied Updater) {
|
||||||
return Updater{
|
return Updater{
|
||||||
Period: gosettings.CopyPointer(u.Period),
|
Period: gosettings.CopyPointer(u.Period),
|
||||||
DNSAddress: u.DNSAddress,
|
DNSAddress: u.DNSAddress,
|
||||||
MinRatio: u.MinRatio,
|
MinRatio: u.MinRatio,
|
||||||
Providers: gosettings.CopySlice(u.Providers),
|
Providers: gosettings.CopySlice(u.Providers),
|
||||||
|
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
|
||||||
|
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +92,8 @@ func (u *Updater) overrideWith(other Updater) {
|
|||||||
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
||||||
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||||
|
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
|
||||||
|
u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||||
@@ -87,6 +108,10 @@ func (u *Updater) SetDefaults(vpnProvider string) {
|
|||||||
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
||||||
u.Providers = []string{vpnProvider}
|
u.Providers = []string{vpnProvider}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set these to empty strings to avoid nil pointer panics
|
||||||
|
u.ProtonEmail = gosettings.DefaultPointer(u.ProtonEmail, "")
|
||||||
|
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Updater) String() string {
|
func (u Updater) String() string {
|
||||||
@@ -103,6 +128,10 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||||
|
if slices.Contains(u.Providers, providers.Protonvpn) {
|
||||||
|
node.Appendf("Proton API email: %s", *u.ProtonEmail)
|
||||||
|
node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword))
|
||||||
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@@ -125,6 +154,16 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
|||||||
|
|
||||||
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
||||||
|
|
||||||
|
u.ProtonEmail = r.Get("UPDATER_PROTONVPN_EMAIL")
|
||||||
|
if u.ProtonEmail == nil {
|
||||||
|
protonUsername := r.String("UPDATER_PROTONVPN_USERNAME", reader.IsRetro("UPDATER_PROTONVPN_EMAIL"))
|
||||||
|
if protonUsername != "" {
|
||||||
|
protonEmail := protonUsername + "@protonmail.com"
|
||||||
|
u.ProtonEmail = &protonEmail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
|
|||||||
providers.Ivpn,
|
providers.Ivpn,
|
||||||
providers.Mullvad,
|
providers.Mullvad,
|
||||||
providers.Nordvpn,
|
providers.Nordvpn,
|
||||||
providers.Ovpn,
|
|
||||||
providers.Protonvpn,
|
providers.Protonvpn,
|
||||||
providers.Surfshark,
|
providers.Surfshark,
|
||||||
providers.Windscribe,
|
providers.Windscribe,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
@@ -22,7 +21,7 @@ type WireguardSelection struct {
|
|||||||
// in the internal state.
|
// in the internal state.
|
||||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||||
// EndpointPort is a the server port to use for the VPN server.
|
// EndpointPort is a the server port to use for the VPN server.
|
||||||
// It is optional for VPN providers IVPN, Mullvad, Ovpn, Surfshark
|
// It is optional for VPN providers IVPN, Mullvad, Surfshark
|
||||||
// and Windscribe, and compulsory for the others.
|
// and Windscribe, and compulsory for the others.
|
||||||
// When optional, it can be set to 0 to indicate not use
|
// When optional, it can be set to 0 to indicate not use
|
||||||
// a custom endpoint port. It cannot be nil in the internal
|
// a custom endpoint port. It cannot be nil in the internal
|
||||||
@@ -40,9 +39,8 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate EndpointIP
|
// Validate EndpointIP
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case providers.Airvpn, providers.Fastestvpn, providers.Ivpn,
|
case providers.Airvpn, providers.Fastestvpn, providers.Ivpn,
|
||||||
providers.Mullvad, providers.Nordvpn, providers.Ovpn,
|
providers.Mullvad, providers.Nordvpn, providers.Protonvpn,
|
||||||
providers.Protonvpn, providers.Surfshark,
|
providers.Surfshark, providers.Windscribe:
|
||||||
providers.Windscribe:
|
|
||||||
// endpoint IP addresses are baked in
|
// endpoint IP addresses are baked in
|
||||||
case providers.Custom:
|
case providers.Custom:
|
||||||
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
|
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
|
||||||
@@ -64,16 +62,12 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
if *w.EndpointPort != 0 {
|
if *w.EndpointPort != 0 {
|
||||||
return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
|
return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
|
||||||
}
|
}
|
||||||
case providers.Airvpn, providers.Ivpn, providers.Mullvad,
|
case providers.Airvpn, providers.Ivpn, providers.Mullvad, providers.Windscribe:
|
||||||
providers.Ovpn, providers.Windscribe:
|
|
||||||
// EndpointPort is optional and can be 0
|
// EndpointPort is optional and can be 0
|
||||||
if *w.EndpointPort == 0 {
|
if *w.EndpointPort == 0 {
|
||||||
break // no custom endpoint port set
|
break // no custom endpoint port set
|
||||||
}
|
}
|
||||||
if helpers.IsOneOf(vpnProvider,
|
if vpnProvider == providers.Mullvad {
|
||||||
providers.Mullvad,
|
|
||||||
providers.Ovpn,
|
|
||||||
) {
|
|
||||||
break // no restriction on custom endpoint port value
|
break // no restriction on custom endpoint port value
|
||||||
}
|
}
|
||||||
var allowed []uint16
|
var allowed []uint16
|
||||||
@@ -98,7 +92,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate PublicKey
|
// Validate PublicKey
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case providers.Fastestvpn, providers.Ivpn, providers.Mullvad,
|
case providers.Fastestvpn, providers.Ivpn, providers.Mullvad,
|
||||||
providers.Ovpn, providers.Surfshark, providers.Windscribe:
|
providers.Surfshark, providers.Windscribe:
|
||||||
// public keys are baked in
|
// public keys are baked in
|
||||||
case providers.Custom:
|
case providers.Custom:
|
||||||
if w.PublicKey == "" {
|
if w.PublicKey == "" {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const (
|
|||||||
Ivpn = "ivpn"
|
Ivpn = "ivpn"
|
||||||
Mullvad = "mullvad"
|
Mullvad = "mullvad"
|
||||||
Nordvpn = "nordvpn"
|
Nordvpn = "nordvpn"
|
||||||
Ovpn = "ovpn"
|
|
||||||
Perfectprivacy = "perfect privacy"
|
Perfectprivacy = "perfect privacy"
|
||||||
Privado = "privado"
|
Privado = "privado"
|
||||||
PrivateInternetAccess = "private internet access"
|
PrivateInternetAccess = "private internet access"
|
||||||
@@ -45,7 +44,6 @@ func All() []string {
|
|||||||
Ivpn,
|
Ivpn,
|
||||||
Mullvad,
|
Mullvad,
|
||||||
Nordvpn,
|
Nordvpn,
|
||||||
Ovpn,
|
|
||||||
Perfectprivacy,
|
Perfectprivacy,
|
||||||
Privado,
|
Privado,
|
||||||
PrivateInternetAccess,
|
PrivateInternetAccess,
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
l.backoffTime = defaultBackoffTime
|
l.backoffTime = defaultBackoffTime
|
||||||
l.logger.Info("ready")
|
l.logger.Info("ready")
|
||||||
l.signalOrSetStatus(constants.Running)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +53,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.logAndWait(ctx, err)
|
l.logAndWait(ctx, err)
|
||||||
settings = l.GetSettings()
|
settings = l.GetSettings()
|
||||||
}
|
}
|
||||||
|
l.signalOrSetStatus(constants.Running)
|
||||||
|
|
||||||
settings = l.GetSettings()
|
settings = l.GetSettings()
|
||||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||||
@@ -74,15 +74,19 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
l.stopServer()
|
if !*l.GetSettings().KeepNameserver {
|
||||||
// TODO revert OS and Go nameserver when exiting
|
l.stopServer()
|
||||||
|
// TODO revert OS and Go nameserver when exiting
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
case <-l.stop:
|
case <-l.stop:
|
||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
l.logger.Info("stopping")
|
l.logger.Info("stopping")
|
||||||
const fallback = false
|
if !*l.GetSettings().KeepNameserver {
|
||||||
l.useUnencryptedDNS(fallback)
|
const fallback = false
|
||||||
l.stopServer()
|
l.useUnencryptedDNS(fallback)
|
||||||
|
l.stopServer()
|
||||||
|
}
|
||||||
l.stopped <- struct{}{}
|
l.stopped <- struct{}{}
|
||||||
case <-l.start:
|
case <-l.start:
|
||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ type Server struct {
|
|||||||
PortForward bool `json:"port_forward,omitempty"`
|
PortForward bool `json:"port_forward,omitempty"`
|
||||||
Keep bool `json:"keep,omitempty"`
|
Keep bool `json:"keep,omitempty"`
|
||||||
IPs []netip.Addr `json:"ips,omitempty"`
|
IPs []netip.Addr `json:"ips,omitempty"`
|
||||||
PortsTCP []uint16 `json:"ports_tcp,omitempty"`
|
|
||||||
PortsUDP []uint16 `json:"ports_udp,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,10 +60,10 @@ func (s *Service) cleanup() (err error) {
|
|||||||
s.ports = nil
|
s.ports = nil
|
||||||
|
|
||||||
filepath := s.settings.Filepath
|
filepath := s.settings.Filepath
|
||||||
s.logger.Info("removing port file " + filepath)
|
s.logger.Info("clearing port file " + filepath)
|
||||||
err = os.Remove(filepath)
|
err = s.writePortForwardedFile(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("removing port file: %w", err)
|
return fmt.Errorf("clearing port file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ var (
|
|||||||
ErrNotEnoughServers = errors.New("not enough servers found")
|
ErrNotEnoughServers = errors.New("not enough servers found")
|
||||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||||
ErrIPFetcherUnsupported = errors.New("IP fetcher not supported")
|
ErrIPFetcherUnsupported = errors.New("IP fetcher not supported")
|
||||||
|
ErrCredentialsMissing = errors.New("credentials missing")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Fetcher interface {
|
type Fetcher interface {
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ type Provider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
parallelResolver common.ParallelResolver,
|
updaterWarner common.Warner, parallelResolver common.ParallelResolver,
|
||||||
) *Provider {
|
) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
Fetcher: updater.New(parallelResolver),
|
Fetcher: updater.New(parallelResolver, updaterWarner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
|||||||
|
|
||||||
possibleHosts := possibleServers.hostsSlice()
|
possibleHosts := possibleServers.hostsSlice()
|
||||||
resolveSettings := parallelResolverSettings(possibleHosts)
|
resolveSettings := parallelResolverSettings(possibleHosts)
|
||||||
hostToIPs, _, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||||
|
for _, warning := range warnings {
|
||||||
|
u.warner.Warn(warning)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import (
|
|||||||
|
|
||||||
type Updater struct {
|
type Updater struct {
|
||||||
parallelResolver common.ParallelResolver
|
parallelResolver common.ParallelResolver
|
||||||
|
warner common.Warner
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(parallelResolver common.ParallelResolver) *Updater {
|
func New(parallelResolver common.ParallelResolver, warner common.Warner) *Updater {
|
||||||
return &Updater{
|
return &Updater{
|
||||||
parallelResolver: parallelResolver,
|
parallelResolver: parallelResolver,
|
||||||
|
warner: warner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package ovpn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
|
|
||||||
connection models.Connection, err error,
|
|
||||||
) {
|
|
||||||
defaults := utils.NewConnectionDefaults(443, 1194, 9929) //nolint:mnd
|
|
||||||
return utils.GetConnection(p.Name(),
|
|
||||||
p.storage, selection, defaults, ipv6Supported, p.randSource)
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package ovpn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Provider_GetConnection(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const provider = providers.Ovpn
|
|
||||||
|
|
||||||
errTest := errors.New("test error")
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
filteredServers []models.Server
|
|
||||||
storageErr error
|
|
||||||
selection settings.ServerSelection
|
|
||||||
ipv6Supported bool
|
|
||||||
connection models.Connection
|
|
||||||
errWrapped error
|
|
||||||
errMessage string
|
|
||||||
}{
|
|
||||||
"error": {
|
|
||||||
storageErr: errTest,
|
|
||||||
errWrapped: errTest,
|
|
||||||
errMessage: "filtering servers: test error",
|
|
||||||
},
|
|
||||||
"default_openvpn_tcp_port": {
|
|
||||||
filteredServers: []models.Server{
|
|
||||||
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
|
|
||||||
},
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
OpenVPN: settings.OpenVPNSelection{
|
|
||||||
Protocol: constants.TCP,
|
|
||||||
},
|
|
||||||
}.WithDefaults(provider),
|
|
||||||
connection: models.Connection{
|
|
||||||
Type: vpn.OpenVPN,
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
|
||||||
Port: 443,
|
|
||||||
Protocol: constants.TCP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"default_openvpn_udp_port": {
|
|
||||||
filteredServers: []models.Server{
|
|
||||||
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
|
|
||||||
},
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
OpenVPN: settings.OpenVPNSelection{
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
},
|
|
||||||
}.WithDefaults(provider),
|
|
||||||
connection: models.Connection{
|
|
||||||
Type: vpn.OpenVPN,
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
|
||||||
Port: 1194,
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"default_wireguard_port": {
|
|
||||||
filteredServers: []models.Server{
|
|
||||||
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, WgPubKey: "x"},
|
|
||||||
},
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
}.WithDefaults(provider),
|
|
||||||
connection: models.Connection{
|
|
||||||
Type: vpn.Wireguard,
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
|
||||||
Port: 9929,
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
PubKey: "x",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"default_multihop_port": {
|
|
||||||
filteredServers: []models.Server{
|
|
||||||
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, WgPubKey: "x", PortsUDP: []uint16{30044}},
|
|
||||||
},
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
}.WithDefaults(provider),
|
|
||||||
connection: models.Connection{
|
|
||||||
Type: vpn.Wireguard,
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
|
||||||
Port: 30044,
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
PubKey: "x",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
|
|
||||||
storage := common.NewMockStorage(ctrl)
|
|
||||||
storage.EXPECT().FilterServers(provider, testCase.selection).
|
|
||||||
Return(testCase.filteredServers, testCase.storageErr)
|
|
||||||
randSource := rand.NewSource(0)
|
|
||||||
|
|
||||||
client := (*http.Client)(nil)
|
|
||||||
provider := New(storage, randSource, client)
|
|
||||||
|
|
||||||
connection, err := provider.GetConnection(testCase.selection, testCase.ipv6Supported)
|
|
||||||
|
|
||||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
|
||||||
if testCase.errWrapped != nil {
|
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.connection, connection)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package ovpn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Provider) OpenVPNConfig(connection models.Connection,
|
|
||||||
settings settings.OpenVPN, ipv6Supported bool,
|
|
||||||
) (lines []string) {
|
|
||||||
providerSettings := utils.OpenVPNProviderSettings{
|
|
||||||
AuthUserPass: true,
|
|
||||||
RemoteCertTLS: true,
|
|
||||||
Ciphers: []string{
|
|
||||||
openvpn.AES256gcm,
|
|
||||||
openvpn.AES256cbc,
|
|
||||||
openvpn.AES128gcm,
|
|
||||||
openvpn.Chacha20Poly1305,
|
|
||||||
},
|
|
||||||
CAs: []string{
|
|
||||||
"MIIEfTCCA2WgAwIBAgIJAK2aIWqpLj1/MA0GCSqGSIb3DQEBBQUAMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTAeFw0xNDA4MTcxODIxMjlaFw0zNDA4MTIxODIxMjlaMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMR+aP4GTuZwurZuOA2NYzMfqKyZi/TJcLEPlGTB/b4CWA9bTd8f0pHPrDAZsXIEayxxB58BIFNDNiybnbO15JN/QwlsqmA+aZX6mCSkScs/rRwasM6LDo8iGx+KmYEqAgzziONGbCMnlO+OaarXte7LhZ9X6Z/bryu4xq/i1v3raak13kXsrogtu4iDzxqJE/QhbNOi0yhCdlm5RYQjmlKGdPB9pNTgcakVI4HcngRYMzBlrGin0YkvWCdpx5FrDNeld7BSWrJMNYyvd+buaid0Fu1T9/P/Srj/8AiabKoaDyiGFbZdTnGfK+04lWRvwAmvazpqbUt5Omw634jJDuMCAwEAAaOB7TCB6jAdBgNVHQ4EFgQUEvJcHHcTiDtu7bAyZw+xaqg+xdIwgboGA1UdIwSBsjCBr4AUEvJcHHcTiDtu7bAyZw+xaqg+xdKhgYukgYgwgYUxCzAJBgNVBAYTAlNFMRIwEAYDVQQIEwlTdG9ja2hvbG0xEjAQBgNVBAcTCVN0b2NraG9sbTEcMBoGA1UECxMTRmlybWEgRGF2aWQgV2liZXJnaDETMBEGA1UEAxMKb3Zwbi5zZSBjYTEbMBkGCSqGSIb3DQEJARYMaW5mb0BvdnBuLnNlggkArZohaqkuPX8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAJmID6OyBJbV7ayPPgquojF+FICuDdOfGVKP828cyISxcbVA04VpD0QLYVb0k9pFUx0NbgX2SvRTiFhP7LcyS1HV9s+XLCb2WItPPsrdRTwtqU2n3TlCEzWA3WOcOCtT6JSkv1eelmx1JnP0gYJrDvDvRYBFctwWhtE0bineSQkZwN6980zkknADLAiHpeZSu/AMx7CGTwA6SmoFvpNBmHXDcfe/9ZqbbYfUfyPNe+0JbMrcv1elKi+6wlEkHFaEBphiZwGEbOX1CjUMcQFgW/cIp3n50Eiyx6ktuqimhyb59P4Nw8gqH452tTtE4MM/brA5y0Q0WFBRBojfZIbGWWQ==", //nolint:lll
|
|
||||||
},
|
|
||||||
TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll
|
|
||||||
KeyDirection: "1",
|
|
||||||
ExtraLines: []string{
|
|
||||||
"replay-window 256",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(connection.Hostname, "singapore.ovpn.com") {
|
|
||||||
providerSettings.TLSCrypt = providerSettings.TLSAuth
|
|
||||||
providerSettings.TLSAuth = ""
|
|
||||||
providerSettings.KeyDirection = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package ovpn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/ovpn/updater"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Provider struct {
|
|
||||||
storage common.Storage
|
|
||||||
randSource rand.Source
|
|
||||||
common.Fetcher
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
|
||||||
client *http.Client,
|
|
||||||
) *Provider {
|
|
||||||
return &Provider{
|
|
||||||
storage: storage,
|
|
||||||
randSource: randSource,
|
|
||||||
Fetcher: updater.New(client),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) Name() string {
|
|
||||||
return providers.Ovpn
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type apiData struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
DataCenters []apiDataCenter `json:"datacenters"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiDataCenter struct {
|
|
||||||
City string `json:"city"`
|
|
||||||
CountryName string `json:"country_name"`
|
|
||||||
Servers []apiServer `json:"servers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiServer struct {
|
|
||||||
IP netip.Addr `json:"ip"`
|
|
||||||
Ptr string `json:"ptr"` // hostname
|
|
||||||
Online bool `json:"online"`
|
|
||||||
PublicKey string `json:"public_key"`
|
|
||||||
WireguardPorts []uint16 `json:"wireguard_ports"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
|
||||||
data apiData, err error,
|
|
||||||
) {
|
|
||||||
const url = "https://www.ovpn.com/v2/api/client/entry"
|
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK {
|
|
||||||
_ = response.Body.Close()
|
|
||||||
return data, fmt.Errorf("%w: %d %s", common.ErrHTTPStatusCodeNotOK,
|
|
||||||
response.StatusCode, response.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(response.Body)
|
|
||||||
err = decoder.Decode(&data)
|
|
||||||
if err != nil {
|
|
||||||
_ = response.Body.Close()
|
|
||||||
return data, fmt.Errorf("decoding response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = response.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return data, fmt.Errorf("closing response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrCityNotSet = errors.New("city is not set")
|
|
||||||
ErrCountryNameNotSet = errors.New("country name is not set")
|
|
||||||
ErrServersNotSet = errors.New("servers array is not set")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *apiDataCenter) validate() (err error) {
|
|
||||||
conditionalErrors := []conditionalError{
|
|
||||||
{err: ErrCityNotSet, condition: a.City == ""},
|
|
||||||
{err: ErrCountryNameNotSet, condition: a.CountryName == ""},
|
|
||||||
{err: ErrServersNotSet, condition: len(a.Servers) == 0},
|
|
||||||
}
|
|
||||||
err = collectErrors(conditionalErrors)
|
|
||||||
if err != nil {
|
|
||||||
var dataCenterSetFields []string
|
|
||||||
if a.CountryName != "" {
|
|
||||||
dataCenterSetFields = append(dataCenterSetFields, a.CountryName)
|
|
||||||
}
|
|
||||||
if a.City != "" {
|
|
||||||
dataCenterSetFields = append(dataCenterSetFields, a.City)
|
|
||||||
}
|
|
||||||
if len(dataCenterSetFields) == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("data center %s: %w",
|
|
||||||
strings.Join(dataCenterSetFields, ", "), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, server := range a.Servers {
|
|
||||||
err = server.validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("datacenter %s, %s: server %d of %d: %w",
|
|
||||||
a.CountryName, a.City, i+1, len(a.Servers), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrIPFieldNotValid = errors.New("ip address is not set")
|
|
||||||
ErrHostnameFieldNotSet = errors.New("hostname field is not set")
|
|
||||||
ErrPublicKeyFieldNotSet = errors.New("public key field is not set")
|
|
||||||
ErrWireguardPortsNotSet = errors.New("wireguard ports array is not set")
|
|
||||||
ErrWireguardPortNotDefault = errors.New("wireguard port is not the default 9929")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *apiServer) validate() (err error) {
|
|
||||||
const defaultWireguardPort = 9929
|
|
||||||
conditionalErrors := []conditionalError{
|
|
||||||
{err: ErrIPFieldNotValid, condition: !a.IP.IsValid()},
|
|
||||||
{err: ErrHostnameFieldNotSet, condition: a.Ptr == ""},
|
|
||||||
{err: ErrPublicKeyFieldNotSet, condition: a.PublicKey == ""},
|
|
||||||
{err: ErrWireguardPortsNotSet, condition: len(a.WireguardPorts) == 0},
|
|
||||||
{
|
|
||||||
err: ErrWireguardPortNotDefault,
|
|
||||||
condition: len(a.WireguardPorts) != 1 || a.WireguardPorts[0] != defaultWireguardPort,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err = collectErrors(conditionalErrors)
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return nil
|
|
||||||
case a.Ptr != "":
|
|
||||||
return fmt.Errorf("server %s: %w", a.Ptr, err)
|
|
||||||
case a.IP.IsValid():
|
|
||||||
return fmt.Errorf("server %s: %w", a.IP.String(), err)
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type conditionalError struct {
|
|
||||||
err error
|
|
||||||
condition bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type joinedError struct {
|
|
||||||
errs []error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *joinedError) Unwrap() []error {
|
|
||||||
return e.errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *joinedError) Error() string {
|
|
||||||
errStrings := make([]string, len(e.errs))
|
|
||||||
for i, err := range e.errs {
|
|
||||||
errStrings[i] = err.Error()
|
|
||||||
}
|
|
||||||
return strings.Join(errStrings, "; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectErrors(conditionalErrors []conditionalError) (err error) {
|
|
||||||
errs := make([]error, 0, len(conditionalErrors))
|
|
||||||
for _, conditionalError := range conditionalErrors {
|
|
||||||
if !conditionalError.condition {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
errs = append(errs, conditionalError.err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &joinedError{
|
|
||||||
errs: errs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_fetchAPI(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
responseStatus int
|
|
||||||
responseBody io.ReadCloser
|
|
||||||
data apiData
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
"http response status not ok": {
|
|
||||||
responseStatus: http.StatusNoContent,
|
|
||||||
err: errors.New("HTTP status code not OK: 204 No Content"),
|
|
||||||
},
|
|
||||||
"nil body": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
err: errors.New("decoding response body: EOF"),
|
|
||||||
},
|
|
||||||
"no server": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: io.NopCloser(strings.NewReader(`{}`)),
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: io.NopCloser(strings.NewReader(`{
|
|
||||||
"success": true,
|
|
||||||
"datacenters": [
|
|
||||||
{
|
|
||||||
"slug": "vienna",
|
|
||||||
"city": "Vienna",
|
|
||||||
"country": "AT",
|
|
||||||
"country_name": "Austria",
|
|
||||||
"pools": [
|
|
||||||
"pool-1.prd.at.vienna.ovpn.com"
|
|
||||||
],
|
|
||||||
"ping_address": "37.120.212.227",
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"ip": "37.120.212.227",
|
|
||||||
"ptr": "vpn44.prd.vienna.ovpn.com",
|
|
||||||
"name": "VPN44 - Vienna",
|
|
||||||
"online": true,
|
|
||||||
"load": 8,
|
|
||||||
"public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
"public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
"wireguard_ports": [
|
|
||||||
9929
|
|
||||||
],
|
|
||||||
"multihop_openvpn_port": 20044,
|
|
||||||
"multihop_wireguard_port": 30044
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`)),
|
|
||||||
data: apiData{
|
|
||||||
Success: true,
|
|
||||||
DataCenters: []apiDataCenter{
|
|
||||||
{CountryName: "Austria", City: "Vienna", Servers: []apiServer{
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("37.120.212.227"),
|
|
||||||
Ptr: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
Online: true,
|
|
||||||
PublicKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
WireguardPorts: []uint16{9929},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
assert.Equal(t, http.MethodGet, r.Method)
|
|
||||||
assert.Equal(t, r.URL.String(), "https://www.ovpn.com/v2/api/client/entry")
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: testCase.responseStatus,
|
|
||||||
Status: http.StatusText(testCase.responseStatus),
|
|
||||||
Body: testCase.responseBody,
|
|
||||||
}, nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := fetchAPI(ctx, client)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.data, data)
|
|
||||||
if testCase.err != nil {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
|
||||||
|
|
||||||
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
||||||
return f(r)
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrResponseSuccessFalse = errors.New("response success field is false")
|
|
||||||
|
|
||||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
|
||||||
servers []models.Server, err error,
|
|
||||||
) {
|
|
||||||
data, err := fetchAPI(ctx, u.client)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fetching API: %w", err)
|
|
||||||
} else if !data.Success {
|
|
||||||
return nil, fmt.Errorf("%w", ErrResponseSuccessFalse)
|
|
||||||
}
|
|
||||||
|
|
||||||
for dataCenterIndex, dataCenter := range data.DataCenters {
|
|
||||||
err = dataCenter.validate()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("validating data center %d of %d: %w",
|
|
||||||
dataCenterIndex+1, len(data.DataCenters), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, apiServer := range dataCenter.Servers {
|
|
||||||
if !apiServer.Online {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
baseServer := models.Server{
|
|
||||||
Country: dataCenter.CountryName,
|
|
||||||
City: dataCenter.City,
|
|
||||||
Hostname: apiServer.Ptr,
|
|
||||||
IPs: []netip.Addr{apiServer.IP},
|
|
||||||
}
|
|
||||||
openVPNServer := baseServer
|
|
||||||
openVPNServer.VPN = vpn.OpenVPN
|
|
||||||
openVPNServer.TCP = true
|
|
||||||
openVPNServer.UDP = true
|
|
||||||
servers = append(servers, openVPNServer)
|
|
||||||
|
|
||||||
wireguardServer := baseServer
|
|
||||||
wireguardServer.VPN = vpn.Wireguard
|
|
||||||
wireguardServer.WgPubKey = apiServer.PublicKey
|
|
||||||
servers = append(servers, wireguardServer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(servers) < minServers {
|
|
||||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
|
||||||
common.ErrNotEnoughServers, len(servers), minServers)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(models.SortableServers(servers))
|
|
||||||
|
|
||||||
return servers, nil
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Updater_FetchServers(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
// Inputs
|
|
||||||
minServers int
|
|
||||||
|
|
||||||
// From API
|
|
||||||
responseStatus int
|
|
||||||
responseBody string
|
|
||||||
|
|
||||||
// Output
|
|
||||||
servers []models.Server
|
|
||||||
errWrapped error
|
|
||||||
errMessage string
|
|
||||||
}{
|
|
||||||
"http_response_error": {
|
|
||||||
responseStatus: http.StatusNoContent,
|
|
||||||
errWrapped: common.ErrHTTPStatusCodeNotOK,
|
|
||||||
errMessage: "fetching API: HTTP status code not OK: 204 No Content",
|
|
||||||
},
|
|
||||||
"success_field_false": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: `{"success": false}`,
|
|
||||||
errWrapped: ErrResponseSuccessFalse,
|
|
||||||
errMessage: "response success field is false",
|
|
||||||
},
|
|
||||||
"validation_failed": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: `{
|
|
||||||
"success": true,
|
|
||||||
"datacenters": [
|
|
||||||
{
|
|
||||||
"city": "Vienna",
|
|
||||||
"servers": [
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
errWrapped: ErrCountryNameNotSet,
|
|
||||||
errMessage: "validating data center 1 of 1: data center Vienna: country name is not set",
|
|
||||||
},
|
|
||||||
"not_enough_servers": {
|
|
||||||
minServers: 3,
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: `{
|
|
||||||
"success": true,
|
|
||||||
"datacenters": [
|
|
||||||
{
|
|
||||||
"city": "Vienna",
|
|
||||||
"country_name": "Austria",
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"ip": "37.120.212.227",
|
|
||||||
"ptr": "vpn44.prd.vienna.ovpn.com",
|
|
||||||
"online": true,
|
|
||||||
"public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
"wireguard_ports": [9929],
|
|
||||||
"multihop_openvpn_port": 20044,
|
|
||||||
"multihop_wireguard_port": 30044
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
errWrapped: common.ErrNotEnoughServers,
|
|
||||||
errMessage: "not enough servers found: 2 and expected at least 3",
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
minServers: 2,
|
|
||||||
responseBody: `{
|
|
||||||
"success": true,
|
|
||||||
"datacenters": [
|
|
||||||
{
|
|
||||||
"slug": "vienna",
|
|
||||||
"city": "Vienna",
|
|
||||||
"country": "AT",
|
|
||||||
"country_name": "Austria",
|
|
||||||
"pools": [
|
|
||||||
"pool-1.prd.at.vienna.ovpn.com"
|
|
||||||
],
|
|
||||||
"ping_address": "37.120.212.227",
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"ip": "37.120.212.227",
|
|
||||||
"ptr": "vpn44.prd.vienna.ovpn.com",
|
|
||||||
"name": "VPN44 - Vienna",
|
|
||||||
"online": true,
|
|
||||||
"load": 8,
|
|
||||||
"public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
"public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
"wireguard_ports": [
|
|
||||||
9929
|
|
||||||
],
|
|
||||||
"multihop_openvpn_port": 20044,
|
|
||||||
"multihop_wireguard_port": 30044
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ip": "37.120.212.228",
|
|
||||||
"ptr": "vpn45.prd.vienna.ovpn.com",
|
|
||||||
"online": false,
|
|
||||||
"public_key": "r93LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
"wireguard_ports": [9929],
|
|
||||||
"multihop_openvpn_port": 20045,
|
|
||||||
"multihop_wireguard_port": 30045
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
servers: []models.Server{
|
|
||||||
{
|
|
||||||
Country: "Austria",
|
|
||||||
City: "Vienna",
|
|
||||||
Hostname: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")},
|
|
||||||
VPN: vpn.OpenVPN,
|
|
||||||
UDP: true,
|
|
||||||
TCP: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Country: "Austria",
|
|
||||||
City: "Vienna",
|
|
||||||
Hostname: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")},
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
WgPubKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
assert.Equal(t, http.MethodGet, r.Method)
|
|
||||||
assert.Equal(t, r.URL.String(), "https://www.ovpn.com/v2/api/client/entry")
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: testCase.responseStatus,
|
|
||||||
Status: http.StatusText(testCase.responseStatus),
|
|
||||||
Body: io.NopCloser(strings.NewReader(testCase.responseBody)),
|
|
||||||
}, nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
updater := &Updater{
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
|
|
||||||
servers, err := updater.FetchServers(ctx, testCase.minServers)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.servers, servers)
|
|
||||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
|
||||||
if testCase.errWrapped != nil {
|
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Updater struct {
|
|
||||||
client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(client *http.Client) *Updater {
|
|
||||||
return &Updater{
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,11 +18,12 @@ type Provider struct {
|
|||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
client *http.Client, updaterWarner common.Warner,
|
client *http.Client, updaterWarner common.Warner,
|
||||||
|
email, password string,
|
||||||
) *Provider {
|
) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
Fetcher: updater.New(client, updaterWarner),
|
Fetcher: updater.New(client, updaterWarner, email, password),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,562 @@
|
|||||||
package updater
|
package updater
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
crand "crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
srp "github.com/ProtonMail/go-srp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
// apiClient is a minimal Proton v4 API client which can handle all the
|
||||||
|
// oddities of Proton's authentication flow they want to keep hidden
|
||||||
|
// from the public.
|
||||||
|
type apiClient struct {
|
||||||
|
apiURLBase string
|
||||||
|
httpClient *http.Client
|
||||||
|
appVersion string
|
||||||
|
userAgent string
|
||||||
|
generator *rand.ChaCha8
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAPIClient returns an [apiClient] with sane defaults matching Proton's
|
||||||
|
// insane expectations.
|
||||||
|
func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClient, err error) {
|
||||||
|
var seed [32]byte
|
||||||
|
_, _ = crand.Read(seed[:])
|
||||||
|
generator := rand.NewChaCha8(seed)
|
||||||
|
|
||||||
|
// Pick a random user agent from this list. Because I'm not going to tell
|
||||||
|
// Proton shit on where all these funny requests are coming from, given their
|
||||||
|
// unhelpfulness in figuring out their authentication flow.
|
||||||
|
userAgents := [...]string{
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||||
|
}
|
||||||
|
userAgent := userAgents[generator.Uint64()%uint64(len(userAgents))]
|
||||||
|
|
||||||
|
appVersion, err := getMostRecentStableTag(ctx, httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting most recent version for proton app: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apiClient{
|
||||||
|
apiURLBase: "https://account.proton.me/api",
|
||||||
|
httpClient: httpClient,
|
||||||
|
appVersion: appVersion,
|
||||||
|
userAgent: userAgent,
|
||||||
|
generator: generator,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrCodeNotSuccess = errors.New("response code is not success")
|
||||||
|
|
||||||
|
// setHeaders sets the minimal necessary headers for Proton API requests
|
||||||
|
// to succeed without being blocked by their "security" measures.
|
||||||
|
// See for example [getMostRecentStableTag] on how the app version must
|
||||||
|
// be set to a recent version or they block your request. "SeCuRiTy"...
|
||||||
|
func (c *apiClient) setHeaders(request *http.Request, cookie cookie) {
|
||||||
|
request.Header.Set("Cookie", cookie.String())
|
||||||
|
request.Header.Set("User-Agent", c.userAgent)
|
||||||
|
request.Header.Set("x-pm-appversion", c.appVersion)
|
||||||
|
request.Header.Set("x-pm-locale", "en_US")
|
||||||
|
request.Header.Set("x-pm-uid", cookie.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate performs the full Proton authentication flow
|
||||||
|
// to obtain an authenticated cookie (uid, token and session ID).
|
||||||
|
func (c *apiClient) authenticate(ctx context.Context, email, password string,
|
||||||
|
) (authCookie cookie, err error) {
|
||||||
|
sessionID, err := c.getSessionID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("getting session ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenType, accessToken, refreshToken, uid, err := c.getUnauthSession(ctx, sessionID)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("getting unauthenticated session data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieToken, err := c.cookieToken(ctx, sessionID, tokenType, accessToken, refreshToken, uid)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("getting cookie token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unauthCookie := cookie{
|
||||||
|
uid: uid,
|
||||||
|
token: cookieToken,
|
||||||
|
sessionID: sessionID,
|
||||||
|
}
|
||||||
|
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64,
|
||||||
|
srpSessionHex, version, err := c.authInfo(ctx, email, unauthCookie)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("getting auth information: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare SRP proof generator using Proton's official SRP parameters and hashing.
|
||||||
|
srpAuth, err := srp.NewAuth(version, username, []byte(password),
|
||||||
|
saltBase64, modulusPGPClearSigned, serverEphemeralBase64)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("initializing SRP auth: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate SRP proofs (A, M1) with the usual 2048-bit modulus.
|
||||||
|
const modulusBits = 2048
|
||||||
|
proofs, err := srpAuth.GenerateProofs(modulusBits)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("generating SRP proofs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authCookie, err = c.auth(ctx, unauthCookie, email, srpSessionHex, proofs)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("authentifying: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authCookie, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrSessionIDNotFound = errors.New("session ID not found in cookies")
|
||||||
|
|
||||||
|
func (c *apiClient) getSessionID(ctx context.Context) (sessionID string, err error) {
|
||||||
|
const url = "https://account.proton.me/vpn"
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("closing response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cookie := range response.Cookies() {
|
||||||
|
if cookie.Name == "Session-Id" {
|
||||||
|
return cookie.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("%w", ErrSessionIDNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrDataFieldMissing = errors.New("data field missing in response")
|
||||||
|
|
||||||
|
func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
|
||||||
|
tokenType, accessToken, refreshToken, uid string, err error,
|
||||||
|
) {
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/auth/v4/sessions", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
unauthCookie := cookie{
|
||||||
|
sessionID: sessionID,
|
||||||
|
}
|
||||||
|
c.setHeaders(request, unauthCookie)
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", fmt.Errorf("reading response body: %w", err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
return "", "", "", "", buildError(response.StatusCode, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Code uint `json:"Code"` // 1000 on success
|
||||||
|
AccessToken string `json:"AccessToken"` // 32-chars lowercase and digits
|
||||||
|
RefreshToken string `json:"RefreshToken"` // 32-chars lowercase and digits
|
||||||
|
TokenType string `json:"TokenType"` // "Bearer"
|
||||||
|
Scopes []string `json:"Scopes"` // should be [] for our usage
|
||||||
|
UID string `json:"UID"` // 32-chars lowercase and digits
|
||||||
|
LocalID uint `json:"LocalID"` // 0 in my case
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(responseBody, &data)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", fmt.Errorf("decoding response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCode = 1000
|
||||||
|
switch {
|
||||||
|
case data.Code != successCode:
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: expected %d got %d",
|
||||||
|
ErrCodeNotSuccess, successCode, data.Code)
|
||||||
|
case data.AccessToken == "":
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: access token is empty", ErrDataFieldMissing)
|
||||||
|
case data.RefreshToken == "":
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: refresh token is empty", ErrDataFieldMissing)
|
||||||
|
case data.TokenType == "":
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: token type is empty", ErrDataFieldMissing)
|
||||||
|
case data.UID == "":
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: UID is empty", ErrDataFieldMissing)
|
||||||
|
}
|
||||||
|
// Ignore Scopes and LocalID fields, we don't use them.
|
||||||
|
|
||||||
|
return data.TokenType, data.AccessToken, data.RefreshToken, data.UID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUIDMismatch = errors.New("UID in response does not match request UID")
|
||||||
|
|
||||||
|
func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, accessToken,
|
||||||
|
refreshToken, uid string,
|
||||||
|
) (cookieToken string, err error) {
|
||||||
|
type requestBodySchema struct {
|
||||||
|
GrantType string `json:"GrantType"` // "refresh_token"
|
||||||
|
Persistent uint `json:"Persistent"` // 0
|
||||||
|
RedirectURI string `json:"RedirectURI"` // "https://protonmail.com"
|
||||||
|
RefreshToken string `json:"RefreshToken"` // 32-chars lowercase and digits
|
||||||
|
ResponseType string `json:"ResponseType"` // "token"
|
||||||
|
State string `json:"State"` // 24-chars letters and digits
|
||||||
|
UID string `json:"UID"` // 32-chars lowercase and digits
|
||||||
|
}
|
||||||
|
requestBody := requestBodySchema{
|
||||||
|
GrantType: "refresh_token",
|
||||||
|
Persistent: 0,
|
||||||
|
RedirectURI: "https://protonmail.com",
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
ResponseType: "token",
|
||||||
|
State: generateLettersDigits(c.generator, 24), //nolint:mnd
|
||||||
|
UID: uid,
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
if err := encoder.Encode(requestBody); err != nil {
|
||||||
|
return "", fmt.Errorf("encoding request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth/cookies", buffer)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
unauthCookie := cookie{
|
||||||
|
uid: uid,
|
||||||
|
sessionID: sessionID,
|
||||||
|
}
|
||||||
|
c.setHeaders(request, unauthCookie)
|
||||||
|
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading response body: %w", err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
return "", buildError(response.StatusCode, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies struct {
|
||||||
|
Code uint `json:"Code"` // 1000 on success
|
||||||
|
UID string `json:"UID"` // should match request UID
|
||||||
|
LocalID uint `json:"LocalID"` // 0
|
||||||
|
RefreshCounter uint `json:"RefreshCounter"` // 1
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &cookies)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decoding response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCode = 1000
|
||||||
|
switch {
|
||||||
|
case cookies.Code != successCode:
|
||||||
|
return "", fmt.Errorf("%w: expected %d got %d",
|
||||||
|
ErrCodeNotSuccess, successCode, cookies.Code)
|
||||||
|
case cookies.UID != requestBody.UID:
|
||||||
|
return "", fmt.Errorf("%w: expected %s got %s",
|
||||||
|
ErrUIDMismatch, requestBody.UID, cookies.UID)
|
||||||
|
}
|
||||||
|
// Ignore LocalID and RefreshCounter fields, we don't use them.
|
||||||
|
|
||||||
|
for _, cookie := range response.Cookies() {
|
||||||
|
if cookie.Name == "AUTH-"+uid {
|
||||||
|
return cookie.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("%w", ErrAuthCookieNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUsernameDoesNotExist = errors.New("username does not exist")
|
||||||
|
|
||||||
|
// authInfo fetches SRP parameters for the account.
|
||||||
|
func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie cookie) (
|
||||||
|
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64, srpSessionHex string,
|
||||||
|
version int, err error,
|
||||||
|
) {
|
||||||
|
type requestBodySchema struct {
|
||||||
|
Intent string `json:"Intent"` // "Proton"
|
||||||
|
Username string `json:"Username"`
|
||||||
|
}
|
||||||
|
requestBody := requestBodySchema{
|
||||||
|
Intent: "Proton",
|
||||||
|
Username: email,
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
if err := encoder.Encode(requestBody); err != nil {
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("encoding request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth/info", buffer)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
c.setHeaders(request, unauthCookie)
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", 0, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("reading response body: %w", err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
return "", "", "", "", "", 0, buildError(response.StatusCode, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
var info struct {
|
||||||
|
Code uint `json:"Code"` // 1000 on success
|
||||||
|
Modulus string `json:"Modulus"` // PGP clearsigned modulus string
|
||||||
|
ServerEphemeral string `json:"ServerEphemeral"` // base64
|
||||||
|
Version *uint `json:"Version,omitempty"` // 4 as of 2025-10-26
|
||||||
|
Salt string `json:"Salt"` // base64
|
||||||
|
SRPSession string `json:"SRPSession"` // hexadecimal
|
||||||
|
Username string `json:"Username"` // user without @domain.com. Mine has its first letter capitalized.
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &info)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("decoding response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCode = 1000
|
||||||
|
switch {
|
||||||
|
case info.Code != successCode:
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: expected %d got %d",
|
||||||
|
ErrCodeNotSuccess, successCode, info.Code)
|
||||||
|
case info.Modulus == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: modulus is empty", ErrDataFieldMissing)
|
||||||
|
case info.ServerEphemeral == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: server ephemeral is empty", ErrDataFieldMissing)
|
||||||
|
case info.Salt == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w (salt data field is empty)", ErrUsernameDoesNotExist)
|
||||||
|
case info.SRPSession == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: SRP session is empty", ErrDataFieldMissing)
|
||||||
|
case info.Username == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: username is empty", ErrDataFieldMissing)
|
||||||
|
case info.Version == nil:
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: version is missing", ErrDataFieldMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
version = int(*info.Version) //nolint:gosec
|
||||||
|
return info.Username, info.Modulus, info.ServerEphemeral, info.Salt,
|
||||||
|
info.SRPSession, version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type cookie struct {
|
||||||
|
uid string
|
||||||
|
token string
|
||||||
|
sessionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cookie) String() string {
|
||||||
|
s := ""
|
||||||
|
if c.token != "" {
|
||||||
|
s += fmt.Sprintf("AUTH-%s=%s; ", c.uid, c.token)
|
||||||
|
}
|
||||||
|
if c.sessionID != "" {
|
||||||
|
s += fmt.Sprintf("Session-Id=%s; ", c.sessionID)
|
||||||
|
}
|
||||||
|
if c.token != "" {
|
||||||
|
s += "Tag=default; iaas=W10; Domain=proton.me; Feature=VPNDashboard:A"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrServerProofNotValid indicates the M2 from the server didn't match the expected proof.
|
||||||
|
ErrServerProofNotValid = errors.New("server proof from server is not valid")
|
||||||
|
ErrVPNScopeNotFound = errors.New("VPN scope not found in scopes")
|
||||||
|
ErrTwoFANotSupported = errors.New("two factor authentication not supported in this client")
|
||||||
|
ErrAuthCookieNotFound = errors.New("auth cookie not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// auth performs the SRP proof submission (and optionally TOTP) to obtain tokens.
|
||||||
|
func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
|
||||||
|
username, srpSession string, proofs *srp.Proofs,
|
||||||
|
) (authCookie cookie, err error) {
|
||||||
|
clientEphemeral := base64.StdEncoding.EncodeToString(proofs.ClientEphemeral)
|
||||||
|
clientProof := base64.StdEncoding.EncodeToString(proofs.ClientProof)
|
||||||
|
|
||||||
|
type requestBodySchema struct {
|
||||||
|
ClientEphemeral string `json:"ClientEphemeral"` // base64(A)
|
||||||
|
ClientProof string `json:"ClientProof"` // base64(M1)
|
||||||
|
Payload map[string]string `json:"Payload,omitempty"` // not sure
|
||||||
|
SRPSession string `json:"SRPSession"` // hexadecimal
|
||||||
|
Username string `json:"Username"` // user@protonmail.com
|
||||||
|
}
|
||||||
|
requestBody := requestBodySchema{
|
||||||
|
ClientEphemeral: clientEphemeral,
|
||||||
|
ClientProof: clientProof,
|
||||||
|
SRPSession: srpSession,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
if err := encoder.Encode(requestBody); err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("encoding request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth", buffer)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
c.setHeaders(request, unauthCookie)
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("reading response body: %w", err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
return cookie{}, buildError(response.StatusCode, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
type twoFAStatus uint
|
||||||
|
//nolint:unused
|
||||||
|
const (
|
||||||
|
twoFADisabled twoFAStatus = iota
|
||||||
|
twoFAHasTOTP
|
||||||
|
twoFAHasFIDO2
|
||||||
|
twoFAHasFIDO2AndTOTP
|
||||||
|
)
|
||||||
|
type twoFAInfo struct {
|
||||||
|
Enabled twoFAStatus `json:"Enabled"`
|
||||||
|
FIDO2 struct {
|
||||||
|
AuthenticationOptions any `json:"AuthenticationOptions"`
|
||||||
|
RegisteredKeys []any `json:"RegisteredKeys"`
|
||||||
|
} `json:"FIDO2"`
|
||||||
|
TOTP uint `json:"TOTP"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var auth struct {
|
||||||
|
Code uint `json:"Code"` // 1000 on success
|
||||||
|
LocalID uint `json:"LocalID"` // 7 in my case
|
||||||
|
Scopes []string `json:"Scopes"` // this should contain "vpn". Same as `Scope` field value.
|
||||||
|
UID string `json:"UID"` // same as `Uid` field value
|
||||||
|
UserID string `json:"UserID"` // base64
|
||||||
|
EventID string `json:"EventID"` // base64
|
||||||
|
PasswordMode uint `json:"PasswordMode"` // 1 in my case
|
||||||
|
ServerProof string `json:"ServerProof"` // base64(M2)
|
||||||
|
TwoFactor uint `json:"TwoFactor"` // 0 if 2FA not required
|
||||||
|
TwoFA twoFAInfo `json:"2FA"`
|
||||||
|
TemporaryPassword uint `json:"TemporaryPassword"` // 0 in my case
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(responseBody, &auth)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("decoding response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m2, err := base64.StdEncoding.DecodeString(auth.ServerProof)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("decoding server proof: %w", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(m2, proofs.ExpectedServerProof) {
|
||||||
|
return cookie{}, fmt.Errorf("%w: expected %x got %x",
|
||||||
|
ErrServerProofNotValid, proofs.ExpectedServerProof, m2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCode = 1000
|
||||||
|
switch {
|
||||||
|
case auth.Code != successCode:
|
||||||
|
return cookie{}, fmt.Errorf("%w: expected %d got %d",
|
||||||
|
ErrCodeNotSuccess, successCode, auth.Code)
|
||||||
|
case auth.UID != unauthCookie.uid:
|
||||||
|
return cookie{}, fmt.Errorf("%w: expected %s got %s",
|
||||||
|
ErrUIDMismatch, unauthCookie.uid, auth.UID)
|
||||||
|
case auth.TwoFactor != 0:
|
||||||
|
return cookie{}, fmt.Errorf("%w", ErrTwoFANotSupported)
|
||||||
|
case !slices.Contains(auth.Scopes, "vpn"):
|
||||||
|
return cookie{}, fmt.Errorf("%w: in %v", ErrVPNScopeNotFound, auth.Scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, setCookieHeader := range response.Header.Values("Set-Cookie") {
|
||||||
|
parts := strings.Split(setCookieHeader, ";")
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.HasPrefix(part, "AUTH-"+unauthCookie.uid+"=") {
|
||||||
|
authCookie = unauthCookie
|
||||||
|
authCookie.token = strings.TrimPrefix(part, "AUTH-"+unauthCookie.uid+"=")
|
||||||
|
return authCookie, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie{}, fmt.Errorf("%w: in HTTP headers %s",
|
||||||
|
ErrAuthCookieNotFound, httpHeadersToString(response.Header))
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateLettersDigits mimicing Proton's own random string generator:
|
||||||
|
// https://github.com/ProtonMail/WebClients/blob/e4d7e4ab9babe15b79a131960185f9f8275512cd/packages/utils/generateLettersDigits.ts
|
||||||
|
func generateLettersDigits(rng *rand.ChaCha8, length uint) string {
|
||||||
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
return generateFromCharset(rng, length, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFromCharset(rng *rand.ChaCha8, length uint, charset string) string {
|
||||||
|
result := make([]byte, length)
|
||||||
|
randomBytes := make([]byte, length)
|
||||||
|
_, _ = rng.Read(randomBytes)
|
||||||
|
for i := range length {
|
||||||
|
result[i] = charset[int(randomBytes[i])%len(charset)]
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpHeadersToString(headers http.Header) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
first := true
|
||||||
|
for key, values := range headers {
|
||||||
|
for _, value := range values {
|
||||||
|
if !first {
|
||||||
|
builder.WriteString(", ")
|
||||||
|
}
|
||||||
|
builder.WriteString(fmt.Sprintf("%s: %s", key, value))
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
type apiData struct {
|
type apiData struct {
|
||||||
LogicalServers []logicalServer `json:"LogicalServers"`
|
LogicalServers []logicalServer `json:"LogicalServers"`
|
||||||
@@ -33,25 +580,25 @@ type physicalServer struct {
|
|||||||
X25519PublicKey string `json:"X25519PublicKey"`
|
X25519PublicKey string `json:"X25519PublicKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
||||||
data apiData, err error,
|
data apiData, err error,
|
||||||
) {
|
) {
|
||||||
const url = "https://api.protonmail.ch/vpn/logicals"
|
const url = "https://account.proton.me/api/vpn/logicals"
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
c.setHeaders(request, cookie)
|
||||||
|
|
||||||
response, err := client.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK {
|
if response.StatusCode != http.StatusOK {
|
||||||
return data, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
b, _ := io.ReadAll(response.Body)
|
||||||
response.StatusCode, response.Status)
|
return data, buildError(response.StatusCode, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
decoder := json.NewDecoder(response.Body)
|
decoder := json.NewDecoder(response.Body)
|
||||||
@@ -59,9 +606,31 @@ func fetchAPI(ctx context.Context, client *http.Client) (
|
|||||||
return data, fmt.Errorf("decoding response body: %w", err)
|
return data, fmt.Errorf("decoding response body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := response.Body.Close(); err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||||
|
|
||||||
|
func buildError(httpCode int, body []byte) error {
|
||||||
|
prettyCode := http.StatusText(httpCode)
|
||||||
|
var protonError struct {
|
||||||
|
Code *int `json:"Code,omitempty"`
|
||||||
|
Error *string `json:"Error,omitempty"`
|
||||||
|
Details map[string]string `json:"Details"`
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err := decoder.Decode(&protonError)
|
||||||
|
if err != nil || protonError.Error == nil || protonError.Code == nil {
|
||||||
|
return fmt.Errorf("%w: %s: %s",
|
||||||
|
ErrHTTPStatusCodeNotOK, prettyCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
details := make([]string, 0, len(protonError.Details))
|
||||||
|
for key, value := range protonError.Details {
|
||||||
|
details = append(details, fmt.Sprintf("%s: %s", key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%w: %s: %s (code %d with details: %s)",
|
||||||
|
ErrHTTPStatusCodeNotOK, prettyCode, *protonError.Error, *protonError.Code, strings.Join(details, ", "))
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,9 +13,26 @@ import (
|
|||||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||||
servers []models.Server, err error,
|
servers []models.Server, err error,
|
||||||
) {
|
) {
|
||||||
data, err := fetchAPI(ctx, u.client)
|
switch {
|
||||||
|
case u.email == "":
|
||||||
|
return nil, fmt.Errorf("%w: email is empty", common.ErrCredentialsMissing)
|
||||||
|
case u.password == "":
|
||||||
|
return nil, fmt.Errorf("%w: password is empty", common.ErrCredentialsMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiClient, err := newAPIClient(ctx, u.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("creating API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := apiClient.authenticate(ctx, u.email, u.password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("authentifying with Proton: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := apiClient.fetchServers(ctx, cookie)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching logical servers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
countryCodes := constants.CountryCodes()
|
countryCodes := constants.CountryCodes()
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Updater struct {
|
type Updater struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
warner common.Warner
|
email string
|
||||||
|
password string
|
||||||
|
warner common.Warner
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(client *http.Client, warner common.Warner) *Updater {
|
func New(client *http.Client, warner common.Warner, email, password string) *Updater {
|
||||||
return &Updater{
|
return &Updater{
|
||||||
client: client,
|
client: client,
|
||||||
warner: warner,
|
email: email,
|
||||||
|
password: password,
|
||||||
|
warner: warner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
internal/provider/protonvpn/updater/version.go
Normal file
64
internal/provider/protonvpn/updater/version.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getMostRecentStableTag finds the most recent proton-account stable tag version,
|
||||||
|
// in order to use it in the x-pm-appversion http request header. Because if we do
|
||||||
|
// fall behind on versioning, Proton doesn't like it because they like to create
|
||||||
|
// complications where there is no need for it. Hence this function.
|
||||||
|
func getMostRecentStableTag(ctx context.Context, client *http.Client) (version string, err error) {
|
||||||
|
page := 1
|
||||||
|
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
url := "https://api.github.com/repos/ProtonMail/WebClients/tags?per_page=30&page=" + fmt.Sprint(page)
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("%w: %s: %s", ErrHTTPStatusCodeNotOK, response.Status, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &tags)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decoding JSON response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
if !regexVersion.MatchString(tag.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
version := "web-account@" + strings.TrimPrefix(tag.Name, "proton-account@")
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("%w (queried %d pages)", context.Canceled, page)
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/provider/ivpn"
|
"github.com/qdm12/gluetun/internal/provider/ivpn"
|
||||||
"github.com/qdm12/gluetun/internal/provider/mullvad"
|
"github.com/qdm12/gluetun/internal/provider/mullvad"
|
||||||
"github.com/qdm12/gluetun/internal/provider/nordvpn"
|
"github.com/qdm12/gluetun/internal/provider/nordvpn"
|
||||||
"github.com/qdm12/gluetun/internal/provider/ovpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/perfectprivacy"
|
"github.com/qdm12/gluetun/internal/provider/perfectprivacy"
|
||||||
"github.com/qdm12/gluetun/internal/provider/privado"
|
"github.com/qdm12/gluetun/internal/provider/privado"
|
||||||
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess"
|
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess"
|
||||||
@@ -55,7 +54,7 @@ type Extractor interface {
|
|||||||
func NewProviders(storage Storage, timeNow func() time.Time,
|
func NewProviders(storage Storage, timeNow func() time.Time,
|
||||||
updaterWarner common.Warner, client *http.Client, unzipper common.Unzipper,
|
updaterWarner common.Warner, client *http.Client, unzipper common.Unzipper,
|
||||||
parallelResolver common.ParallelResolver, ipFetcher common.IPFetcher,
|
parallelResolver common.ParallelResolver, ipFetcher common.IPFetcher,
|
||||||
extractor custom.Extractor,
|
extractor custom.Extractor, credentials settings.Updater,
|
||||||
) *Providers {
|
) *Providers {
|
||||||
randSource := rand.NewSource(timeNow().UnixNano())
|
randSource := rand.NewSource(timeNow().UnixNano())
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
|||||||
providerNameToProvider := map[string]Provider{
|
providerNameToProvider := map[string]Provider{
|
||||||
providers.Airvpn: airvpn.New(storage, randSource, client),
|
providers.Airvpn: airvpn.New(storage, randSource, client),
|
||||||
providers.Custom: custom.New(extractor),
|
providers.Custom: custom.New(extractor),
|
||||||
providers.Cyberghost: cyberghost.New(storage, randSource, parallelResolver),
|
providers.Cyberghost: cyberghost.New(storage, randSource, updaterWarner, parallelResolver),
|
||||||
providers.Expressvpn: expressvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Expressvpn: expressvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
providers.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
@@ -72,12 +71,11 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
|||||||
providers.Ivpn: ivpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
providers.Ivpn: ivpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.Mullvad: mullvad.New(storage, randSource, client),
|
providers.Mullvad: mullvad.New(storage, randSource, client),
|
||||||
providers.Nordvpn: nordvpn.New(storage, randSource, client, updaterWarner),
|
providers.Nordvpn: nordvpn.New(storage, randSource, client, updaterWarner),
|
||||||
providers.Ovpn: ovpn.New(storage, randSource, client),
|
|
||||||
providers.Perfectprivacy: perfectprivacy.New(storage, randSource, unzipper, updaterWarner),
|
providers.Perfectprivacy: perfectprivacy.New(storage, randSource, unzipper, updaterWarner),
|
||||||
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
|
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
|
||||||
providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner),
|
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword),
|
||||||
providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ func GetConnection(provider string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol := getProtocol(selection)
|
protocol := getProtocol(selection)
|
||||||
|
port := getPort(selection, defaults.OpenVPNTCPPort,
|
||||||
|
defaults.OpenVPNUDPPort, defaults.WireguardPort)
|
||||||
|
|
||||||
connections := make([]models.Connection, 0, len(servers))
|
connections := make([]models.Connection, 0, len(servers))
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
@@ -59,9 +61,6 @@ func GetConnection(provider string,
|
|||||||
hostname = server.OvpnX509
|
hostname = server.OvpnX509
|
||||||
}
|
}
|
||||||
|
|
||||||
port := getPort(selection, server, defaults.OpenVPNTCPPort,
|
|
||||||
defaults.OpenVPNUDPPort, defaults.WireguardPort)
|
|
||||||
|
|
||||||
connection := models.Connection{
|
connection := models.Connection{
|
||||||
Type: selection.VPN,
|
Type: selection.VPN,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
|
|||||||
@@ -6,44 +6,29 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPort(selection settings.ServerSelection, server models.Server,
|
func getPort(selection settings.ServerSelection,
|
||||||
defaultOpenVPNTCP, defaultOpenVPNUDP, defaultWireguard uint16,
|
defaultOpenVPNTCP, defaultOpenVPNUDP, defaultWireguard uint16,
|
||||||
) (port uint16) {
|
) (port uint16) {
|
||||||
switch selection.VPN {
|
switch selection.VPN {
|
||||||
case vpn.Wireguard:
|
case vpn.Wireguard:
|
||||||
customPort := *selection.Wireguard.EndpointPort
|
customPort := *selection.Wireguard.EndpointPort
|
||||||
if customPort > 0 {
|
if customPort > 0 {
|
||||||
// Note: servers filtering ensures the custom port is within the
|
|
||||||
// server ports defined if any is set.
|
|
||||||
return customPort
|
return customPort
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(server.PortsUDP) > 0 {
|
|
||||||
defaultWireguard = server.PortsUDP[0]
|
|
||||||
}
|
|
||||||
checkDefined("Wireguard", defaultWireguard)
|
checkDefined("Wireguard", defaultWireguard)
|
||||||
return defaultWireguard
|
return defaultWireguard
|
||||||
default: // OpenVPN
|
default: // OpenVPN
|
||||||
customPort := *selection.OpenVPN.CustomPort
|
customPort := *selection.OpenVPN.CustomPort
|
||||||
if customPort > 0 {
|
if customPort > 0 {
|
||||||
// Note: servers filtering ensures the custom port is within the
|
|
||||||
// server ports defined if any is set.
|
|
||||||
return customPort
|
return customPort
|
||||||
}
|
}
|
||||||
if selection.OpenVPN.Protocol == constants.TCP {
|
if selection.OpenVPN.Protocol == constants.TCP {
|
||||||
if len(server.PortsTCP) > 0 {
|
|
||||||
defaultOpenVPNTCP = server.PortsTCP[0]
|
|
||||||
}
|
|
||||||
checkDefined("OpenVPN TCP", defaultOpenVPNTCP)
|
checkDefined("OpenVPN TCP", defaultOpenVPNTCP)
|
||||||
return defaultOpenVPNTCP
|
return defaultOpenVPNTCP
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(server.PortsUDP) > 0 {
|
|
||||||
defaultOpenVPNUDP = server.PortsUDP[0]
|
|
||||||
}
|
|
||||||
checkDefined("OpenVPN UDP", defaultOpenVPNUDP)
|
checkDefined("OpenVPN UDP", defaultOpenVPNUDP)
|
||||||
return defaultOpenVPNUDP
|
return defaultOpenVPNUDP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +23,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
selection settings.ServerSelection
|
selection settings.ServerSelection
|
||||||
server models.Server
|
|
||||||
defaultOpenVPNTCP uint16
|
defaultOpenVPNTCP uint16
|
||||||
defaultOpenVPNUDP uint16
|
defaultOpenVPNUDP uint16
|
||||||
defaultWireguard uint16
|
defaultWireguard uint16
|
||||||
@@ -51,20 +49,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
defaultWireguard: defaultWireguard,
|
defaultWireguard: defaultWireguard,
|
||||||
port: defaultOpenVPNUDP,
|
port: defaultOpenVPNUDP,
|
||||||
},
|
},
|
||||||
"OpenVPN_server_port_udp": {
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.OpenVPN,
|
|
||||||
OpenVPN: settings.OpenVPNSelection{
|
|
||||||
CustomPort: uint16Ptr(0),
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: models.Server{
|
|
||||||
PortsUDP: []uint16{1234},
|
|
||||||
},
|
|
||||||
defaultOpenVPNUDP: defaultOpenVPNUDP,
|
|
||||||
port: 1234,
|
|
||||||
},
|
|
||||||
"OpenVPN UDP no default port defined": {
|
"OpenVPN UDP no default port defined": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.OpenVPN,
|
VPN: vpn.OpenVPN,
|
||||||
@@ -105,20 +89,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
},
|
},
|
||||||
port: 1234,
|
port: 1234,
|
||||||
},
|
},
|
||||||
"OpenVPN_server_port_tcp": {
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.OpenVPN,
|
|
||||||
OpenVPN: settings.OpenVPNSelection{
|
|
||||||
CustomPort: uint16Ptr(0),
|
|
||||||
Protocol: constants.TCP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: models.Server{
|
|
||||||
PortsTCP: []uint16{1234},
|
|
||||||
},
|
|
||||||
defaultOpenVPNTCP: defaultOpenVPNTCP,
|
|
||||||
port: 1234,
|
|
||||||
},
|
|
||||||
"Wireguard": {
|
"Wireguard": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.Wireguard,
|
VPN: vpn.Wireguard,
|
||||||
@@ -136,19 +106,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
defaultWireguard: defaultWireguard,
|
defaultWireguard: defaultWireguard,
|
||||||
port: 1234,
|
port: 1234,
|
||||||
},
|
},
|
||||||
"Wireguard_server_port": {
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
Wireguard: settings.WireguardSelection{
|
|
||||||
EndpointPort: uint16Ptr(0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: models.Server{
|
|
||||||
PortsUDP: []uint16{1234},
|
|
||||||
},
|
|
||||||
defaultWireguard: defaultWireguard,
|
|
||||||
port: 1234,
|
|
||||||
},
|
|
||||||
"Wireguard no default port defined": {
|
"Wireguard no default port defined": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.Wireguard,
|
VPN: vpn.Wireguard,
|
||||||
@@ -164,7 +121,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
if testCase.panics != "" {
|
if testCase.panics != "" {
|
||||||
assert.PanicsWithValue(t, testCase.panics, func() {
|
assert.PanicsWithValue(t, testCase.panics, func() {
|
||||||
_ = getPort(testCase.selection,
|
_ = getPort(testCase.selection,
|
||||||
testCase.server,
|
|
||||||
testCase.defaultOpenVPNTCP,
|
testCase.defaultOpenVPNTCP,
|
||||||
testCase.defaultOpenVPNUDP,
|
testCase.defaultOpenVPNUDP,
|
||||||
testCase.defaultWireguard)
|
testCase.defaultWireguard)
|
||||||
@@ -173,7 +129,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
port := getPort(testCase.selection,
|
port := getPort(testCase.selection,
|
||||||
testCase.server,
|
|
||||||
testCase.defaultOpenVPNTCP,
|
testCase.defaultOpenVPNTCP,
|
||||||
testCase.defaultOpenVPNUDP,
|
testCase.defaultOpenVPNUDP,
|
||||||
testCase.defaultWireguard)
|
testCase.defaultWireguard)
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
|||||||
Ciphers: []string{openvpn.AES256cbc},
|
Ciphers: []string{openvpn.AES256cbc},
|
||||||
Auth: openvpn.SHA512,
|
Auth: openvpn.SHA512,
|
||||||
CAs: []string{
|
CAs: []string{
|
||||||
"MIID7jCCA1CgAwIBAgIQQTT3w3N+5i8OMfe565xaSjAKBggqhkjOPQQDBDCBojELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xGjAYBgNVBAsMEUtlZXBTb2xpZCBSb290IENBMRowGAYDVQQDDBFLZWVwU29saWQgUm9vdCBDQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5Aa2VlcHNvbGlkLmNvbTAeFw0yMDA0MDExNjI3MTRaFw0yNTAzMzExNjI3MTRaMIGgMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEVMBMGA1UECwwMS2VlcFNvbGlkIENBMR0wGwYDVQQDDBRPcGVuVlBOIFNlcnZlciBTdWJDQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5Aa2VlcHNvbGlkLmNvbTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAR9nmoZUraRSSPUhYwIQBLSx+phJdIlqU7F7Hszh95ivnWYkwuizKLaUYy6lSISDohlUtQl9URBlRrGroVctOGlOAdpL2ARTljw5gmUcaavc5cvLiAV7fPJ7BFUgVxInmaVcaMlDwGgKLxmjU2Fw85VLROHbWQjYc93x/BTSFcYO/np4o4IBIzCCAR8wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUrUCjH8xe37lJihyzpqjWwxxNOiswgeIGA1UdIwSB2jCB14AU/LRRnTRaEbxct895Pk9DoymNQIqhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb22CFEssZFYAz8WhYnIDxLeDgKTLD8p2MAsGA1UdDwQEAwIBBjAKBggqhkjOPQQDBAOBiwAwgYcCQgGuK8UNnpE8k8hAamnT9gxCSs5APqrgmdLe6BxYSz7AptpF2/MPzLFsXgj4YxC6vJP8Rs8e3Hw9VJ7DF0aYgu8DvQJBeyFWjRnk8kmu2zEU+wF9fkvN9AJ7v0xF0iEaFVsdPKv6sJQP1sAL+AIepJQ7TYvh9Q9G/WaRCfItCtcOAEz3SKA=", //nolint:lll
|
"MIIECjCCA2ygAwIBAgIRAJ/aLZu0PCO7LlOTcPQE9UwwCgYIKoZIzj0EAwQwgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wHhcNMjUwMzMxMTQ0OTU4WhcNMzAwNjEzMTQ0OTU4WjCBqTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xHTAbBgNVBAsMFEtlZXBTb2xpZCBPcGVuVlBOIENBMR0wGwYDVQQDDBRLZWVwU29saWQgT3BlblZQTiBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAEHfJRyn9MZ7HQctQULIxVUNFFw+tWetokml5PvIsS1i3mM4NQnj0HHL5zCCQRKUmSiiWtGvbGlsHEWX/hz+NiVoQGjMqBD2ykdLimiFrceonIofEBZW8to6jTjG3wmJkRykDqsuLyBLUKGc2F5dR3YFGgwyDoRz0NaAYI+qgqWfE+cVaOCASwwggEoMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFB4IhTj1gStDx+fNq+ubBcr+lEbwMIHrBgNVHSMEgeMwgeCAFOEcFx6OcN8T1R8lTdCLhFlYuk5joYGxpIGuMIGrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEeMBwGA1UECwwVS2VlcFNvbGlkIFZQTiBSb290IENBMR4wHAYDVQQDDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExIzAhBgkqhkiG9w0BCQEWFGFkbWluc0BrZWVwc29saWQuY29tghRnfb8jJuxu5dJzLm5ZdurkedrxzjALBgNVHQ8EBAMCAQYwCgYIKoZIzj0EAwQDgYsAMIGHAkIBg8Cdu474VlljCoP8WEr6xErKL6Bygy5+SO1Ey0Uu3B7q8R22F0EWvrOmqmyNZ3oRyqhpUGaEBqB2aqDGT7u7wGsCQUP3nyMlDbXqCF05byMbhQrBsCz1nyqDNnfzM2uGmT09XwWXGCYTIGdynyJJLzdOlpf3T19ZLvqLSf6Kvq45u6si", //nolint:lll
|
||||||
"MIID9zCCA1igAwIBAgIUSyxkVgDPxaFicgPEt4OApMsPynYwCgYIKoZIzj0EAwQwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb20wIBcNMTkxMjMxMTY1NzMyWhgPMjA1NzA1MTUxNjU3MzJaMIGiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEaMBgGA1UECwwRS2VlcFNvbGlkIFJvb3QgQ0ExGjAYBgNVBAMMEUtlZXBTb2xpZCBSb290IENBMSIwIAYJKoZIhvcNAQkBFhNhZG1pbkBrZWVwc29saWQuY29tMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBlmcBvPVcV7mVgiUVkO9Dh8b4Hdw/eyU8OLWJ0qyRiqI1q0ad1cxgi6asy33xwilrMkRxhArDSfB87zpUpUboTEMBSf9n+dCoGRncGfW9G+8IvhzPY3Z3nzVHBGhoKlN1jsCuKzzpjGawqTAeCkJNBPQNd75Dp6Tgl198bAowD+iPX3WjggEjMIIBHzAdBgNVHQ4EFgQU/LRRnTRaEbxct895Pk9DoymNQIowgeIGA1UdIwSB2jCB14AU/LRRnTRaEbxct895Pk9DoymNQIqhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb22CFEssZFYAz8WhYnIDxLeDgKTLD8p2MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMAoGCCqGSM49BAMEA4GMADCBiAJCAbDIYRjZYaDwMf8Gq4udKVS4aWtZt73lVulCVmr951tQ9J2Dzh4OEQZvU5+M688o2N/fVxNQoxwm/NsiJxpc/prQAkIBiRbrcEGvalu9h6UqE6yAXe0JZcF5xn/BIe5XygglOput4kvZKLKtIqPe2bwBmL/dqq6XDL7s5QaTWPo5MtpzGjA=", //nolint:lll
|
"MIIEEDCCA3GgAwIBAgIUZ32/IybsbuXScy5uWXbq5Hna8c4wCgYIKoZIzj0EAwQwgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wHhcNMjUwMzMxMTQ0NTUzWhcNMzUwODI2MTQ0NTUzWjCBqzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xHjAcBgNVBAsMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEeMBwGA1UEAwwVS2VlcFNvbGlkIFZQTiBSb290IENBMSMwIQYJKoZIhvcNAQkBFhRhZG1pbnNAa2VlcHNvbGlkLmNvbTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAN77xqCz3wrFDnRMtggwScgvO6wPFZYECTUu5WW0JaowgmuIgo+BiQQyTeUzJEICulc1Hg7EaUEV+z8jsSrB+4/EAWazn/ufWOx/51fa5FCv4YooCbgLPb1CzYDuTc7MUR5PLQ88o3W01wCCgT8RoNH8uChyPBLUBh2f4rUfpzl20Bqdo4IBLDCCASgwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQU4RwXHo5w3xPVHyVN0IuEWVi6TmMwgesGA1UdIwSB4zCB4IAU4RwXHo5w3xPVHyVN0IuEWVi6TmOhgbGkga4wgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb22CFGd9vyMm7G7l0nMubll26uR52vHOMAsGA1UdDwQEAwIBBjAKBggqhkjOPQQDBAOBjAAwgYgCQgCZtqE+wXwH0ixjWafX3SClp8O3bYeyB/7jbzf8MprXRYBVQ8JjvugjaZTvX82Uy++LaN3oHqK+NUhJUdfZx/eIuQJCAad7HpsKyTYuUUkgAgWXJma4MstxyO9PVRNYozi1oc45Z8deSvwy404n3u1kY5QXLZQaaMY7m2pF+ECs4WkKCh5s", //nolint:lll
|
||||||
},
|
},
|
||||||
ExtraLines: []string{
|
ExtraLines: []string{
|
||||||
"route-metric 1",
|
"route-metric 1",
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !*l.settings.Enabled {
|
||||||
|
singleRunResult <- nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
result, err := l.fetcher.FetchInfo(singleRunCtx, netip.Addr{})
|
result, err := l.fetcher.FetchInfo(singleRunCtx, netip.Addr{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("fetching information: %w", err)
|
err = fmt.Errorf("fetching information: %w", err)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m *logMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
m.childHandler.ServeHTTP(statefulWriter, r)
|
m.childHandler.ServeHTTP(statefulWriter, r)
|
||||||
duration := m.timeNow().Sub(tStart)
|
duration := m.timeNow().Sub(tStart)
|
||||||
m.logger.Info(strconv.Itoa(statefulWriter.statusCode) + " " +
|
m.logger.Info(strconv.Itoa(statefulWriter.statusCode) + " " +
|
||||||
r.Method + " " + r.RequestURI +
|
r.Method + " " + r.URL.String() +
|
||||||
" wrote " + strconv.Itoa(statefulWriter.length) + "B to " +
|
" wrote " + strconv.Itoa(statefulWriter.length) + "B to " +
|
||||||
r.RemoteAddr + " in " + duration.String())
|
r.RemoteAddr + " in " + duration.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
@@ -122,10 +121,6 @@ func filterServer(server models.Server,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if filterByPorts(selection, server.PortsTCP) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO filter port forward server for PIA
|
// TODO filter port forward server for PIA
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -169,21 +164,3 @@ func filterByProtocol(selection settings.ServerSelection,
|
|||||||
return (wantTCP && !serverTCP) || (wantUDP && !serverUDP)
|
return (wantTCP && !serverTCP) || (wantUDP && !serverUDP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterByPorts(selection settings.ServerSelection,
|
|
||||||
serverPorts []uint16,
|
|
||||||
) (filtered bool) {
|
|
||||||
if len(serverPorts) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
customPort := *selection.OpenVPN.CustomPort
|
|
||||||
if selection.VPN == vpn.Wireguard {
|
|
||||||
customPort = *selection.Wireguard.EndpointPort
|
|
||||||
}
|
|
||||||
if customPort == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return !slices.Contains(serverPorts, customPort)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func commaJoin(slice []string) string {
|
func commaJoin(slice []string) string {
|
||||||
@@ -17,7 +16,7 @@ func commaJoin(slice []string) string {
|
|||||||
|
|
||||||
var ErrNoServerFound = errors.New("no server found")
|
var ErrNoServerFound = errors.New("no server found")
|
||||||
|
|
||||||
func noServerFoundError(selection settings.ServerSelection) (err error) { //nolint:gocyclo
|
func noServerFoundError(selection settings.ServerSelection) (err error) {
|
||||||
var messageParts []string
|
var messageParts []string
|
||||||
|
|
||||||
messageParts = append(messageParts, "VPN "+selection.VPN)
|
messageParts = append(messageParts, "VPN "+selection.VPN)
|
||||||
@@ -154,15 +153,6 @@ func noServerFoundError(selection settings.ServerSelection) (err error) { //noli
|
|||||||
"target ip address "+selection.TargetIP.String())
|
"target ip address "+selection.TargetIP.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
customPort := *selection.OpenVPN.CustomPort
|
|
||||||
if selection.VPN == vpn.Wireguard {
|
|
||||||
customPort = *selection.Wireguard.EndpointPort
|
|
||||||
}
|
|
||||||
if customPort > 0 {
|
|
||||||
messageParts = append(messageParts,
|
|
||||||
fmt.Sprintf("%s endpoint port %d", selection.VPN, customPort))
|
|
||||||
}
|
|
||||||
|
|
||||||
message := "for " + strings.Join(messageParts, "; ")
|
message := "for " + strings.Join(messageParts, "; ")
|
||||||
|
|
||||||
return fmt.Errorf("%w: %s", ErrNoServerFound, message)
|
return fmt.Errorf("%w: %s", ErrNoServerFound, message)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Infoer
|
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Logger
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Infoer)
|
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Logger)
|
||||||
|
|
||||||
// Package storage is a generated GoMock package.
|
// Package storage is a generated GoMock package.
|
||||||
package storage
|
package storage
|
||||||
@@ -10,37 +10,49 @@ import (
|
|||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockInfoer is a mock of Infoer interface.
|
// MockLogger is a mock of Logger interface.
|
||||||
type MockInfoer struct {
|
type MockLogger struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
recorder *MockInfoerMockRecorder
|
recorder *MockLoggerMockRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockInfoerMockRecorder is the mock recorder for MockInfoer.
|
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||||
type MockInfoerMockRecorder struct {
|
type MockLoggerMockRecorder struct {
|
||||||
mock *MockInfoer
|
mock *MockLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockInfoer creates a new mock instance.
|
// NewMockLogger creates a new mock instance.
|
||||||
func NewMockInfoer(ctrl *gomock.Controller) *MockInfoer {
|
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||||
mock := &MockInfoer{ctrl: ctrl}
|
mock := &MockLogger{ctrl: ctrl}
|
||||||
mock.recorder = &MockInfoerMockRecorder{mock}
|
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||||
return mock
|
return mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
func (m *MockInfoer) EXPECT() *MockInfoerMockRecorder {
|
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info mocks base method.
|
// Info mocks base method.
|
||||||
func (m *MockInfoer) Info(arg0 string) {
|
func (m *MockLogger) Info(arg0 string) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
m.ctrl.Call(m, "Info", arg0)
|
m.ctrl.Call(m, "Info", arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info indicates an expected call of Info.
|
// Info indicates an expected call of Info.
|
||||||
func (mr *MockInfoerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockInfoer)(nil).Info), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn mocks base method.
|
||||||
|
func (m *MockLogger) Warn(arg0 string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "Warn", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn indicates an expected call of Warn.
|
||||||
|
func (mr *MockLoggerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), arg0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
logger := NewMockInfoer(ctrl)
|
logger := NewMockLogger(ctrl)
|
||||||
var previousLogCall *gomock.Call
|
var previousLogCall *gomock.Call
|
||||||
for _, logged := range testCase.logged {
|
for _, logged := range testCase.logged {
|
||||||
call := logger.EXPECT().Info(logged)
|
call := logger.EXPECT().Info(logged)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,18 +13,20 @@ type Storage struct {
|
|||||||
// the embedded JSON file on every call to the
|
// the embedded JSON file on every call to the
|
||||||
// SyncServers method.
|
// SyncServers method.
|
||||||
hardcodedServers models.AllServers
|
hardcodedServers models.AllServers
|
||||||
logger Infoer
|
logger Logger
|
||||||
filepath string
|
filepath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Infoer interface {
|
type Logger interface {
|
||||||
Info(s string)
|
Info(s string)
|
||||||
|
Warn(s string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new storage and reads the servers from the
|
// New creates a new storage and reads the servers from the
|
||||||
// embedded servers file and the file on disk.
|
// embedded servers file and the file on disk.
|
||||||
// Passing an empty filepath disables writing servers to a file.
|
// Passing an empty filepath disables the reading and writing of
|
||||||
func New(logger Infoer, filepath string) (storage *Storage, err error) {
|
// servers.
|
||||||
|
func New(logger Logger, filepath string) (storage *Storage, err error) {
|
||||||
// A unit test prevents any error from being returned
|
// A unit test prevents any error from being returned
|
||||||
// and ensures all providers are part of the servers returned.
|
// and ensures all providers are part of the servers returned.
|
||||||
hardcodedServers, _ := parseHardcodedServers()
|
hardcodedServers, _ := parseHardcodedServers()
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (s *Storage) syncServers() (err error) {
|
|||||||
|
|
||||||
err = s.flushToFile(s.filepath)
|
err = s.flushToFile(s.filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writing servers to file: %w", err)
|
s.logger.Warn("failed writing servers to file: " + err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func (u *Updater) updateProvider(ctx context.Context, provider Provider,
|
|||||||
u.logger.Warn("note: if running the update manually, you can use the flag " +
|
u.logger.Warn("note: if running the update manually, you can use the flag " +
|
||||||
"-minratio to allow the update to succeed with less servers found")
|
"-minratio to allow the update to succeed with less servers found")
|
||||||
}
|
}
|
||||||
return fmt.Errorf("getting servers: %w", err)
|
return fmt.Errorf("getting %s servers: %w", providerName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package updater
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
@@ -48,22 +50,22 @@ func (u *Updater) UpdateServers(ctx context.Context, providers []string,
|
|||||||
// TODO support servers offering only TCP or only UDP
|
// TODO support servers offering only TCP or only UDP
|
||||||
// for NordVPN and PureVPN
|
// for NordVPN and PureVPN
|
||||||
err := u.updateProvider(ctx, fetcher, minRatio)
|
err := u.updateProvider(ctx, fetcher, minRatio)
|
||||||
if err == nil {
|
switch {
|
||||||
|
case err == nil:
|
||||||
continue
|
continue
|
||||||
}
|
case errors.Is(err, common.ErrCredentialsMissing):
|
||||||
|
u.logger.Warn(err.Error() + " - skipping update for " + providerName)
|
||||||
// return the only error for the single provider.
|
continue
|
||||||
if len(providers) == 1 {
|
case len(providers) == 1:
|
||||||
|
// return the only error for the single provider.
|
||||||
return err
|
return err
|
||||||
|
case ctx.Err() != nil:
|
||||||
|
// stop updating other providers if context is done
|
||||||
|
return ctx.Err()
|
||||||
|
default: // error encountered updating one of multiple providers
|
||||||
|
// Log the error and continue updating the next provider.
|
||||||
|
u.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop updating the next providers if context is canceled.
|
|
||||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
||||||
return ctxErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the error and continue updating the next provider.
|
|
||||||
u.logger.Error(err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -32,9 +32,14 @@ func (w *Wireguard) addRoutes(link netlink.Link, destinations []netip.Prefix,
|
|||||||
func (w *Wireguard) addRoute(link netlink.Link, dst netip.Prefix,
|
func (w *Wireguard) addRoute(link netlink.Link, dst netip.Prefix,
|
||||||
firewallMark uint32,
|
firewallMark uint32,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
|
family := netlink.FamilyV4
|
||||||
|
if dst.Addr().Is6() {
|
||||||
|
family = netlink.FamilyV6
|
||||||
|
}
|
||||||
route := netlink.Route{
|
route := netlink.Route{
|
||||||
LinkIndex: link.Index,
|
LinkIndex: link.Index,
|
||||||
Dst: dst,
|
Dst: dst,
|
||||||
|
Family: family,
|
||||||
Table: int(firewallMark),
|
Table: int(firewallMark),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func Test_Wireguard_addRoute(t *testing.T) {
|
|||||||
expectedRoute: netlink.Route{
|
expectedRoute: netlink.Route{
|
||||||
LinkIndex: linkIndex,
|
LinkIndex: linkIndex,
|
||||||
Dst: ipPrefix,
|
Dst: ipPrefix,
|
||||||
|
Family: netlink.FamilyV4,
|
||||||
Table: firewallMark,
|
Table: firewallMark,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -49,6 +50,7 @@ func Test_Wireguard_addRoute(t *testing.T) {
|
|||||||
expectedRoute: netlink.Route{
|
expectedRoute: netlink.Route{
|
||||||
LinkIndex: linkIndex,
|
LinkIndex: linkIndex,
|
||||||
Dst: ipPrefix,
|
Dst: ipPrefix,
|
||||||
|
Family: netlink.FamilyV4,
|
||||||
Table: firewallMark,
|
Table: firewallMark,
|
||||||
},
|
},
|
||||||
routeAddErr: errDummy,
|
routeAddErr: errDummy,
|
||||||
|
|||||||
Reference in New Issue
Block a user