Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7450424849 | ||
|
|
16bc27bf9f | ||
|
|
01e9274f7b | ||
|
|
daff23bfb3 | ||
|
|
aa6d26e062 | ||
|
|
b2859d5a06 | ||
|
|
ad8b0657cb | ||
|
|
c930a4e1be | ||
|
|
22834e9477 | ||
|
|
62c2679da2 | ||
|
|
5e9ae9fa1f | ||
|
|
0f19bcfebd | ||
|
|
83fc91d3c6 | ||
|
|
4adeec8223 | ||
|
|
64bfbaa45d |
@@ -20,6 +20,9 @@ issues:
|
||||
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
||||
linters:
|
||||
- ireturn
|
||||
- source: "^\\/\\/ https\\:\\/\\/.+$"
|
||||
linters:
|
||||
- lll
|
||||
|
||||
linters:
|
||||
enable:
|
||||
|
||||
@@ -204,6 +204,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
UPDATER_PERIOD=0 \
|
||||
UPDATER_MIN_RATIO=0.8 \
|
||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||
UPDATER_PROTONVPN_EMAIL= \
|
||||
UPDATER_PROTONVPN_PASSWORD= \
|
||||
# Public IP
|
||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||
PUBLICIP_ENABLED=on \
|
||||
|
||||
@@ -420,7 +420,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||
openvpnFileExtractor := extract.New()
|
||||
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"))
|
||||
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
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-srp v0.0.7
|
||||
github.com/breml/rootcerts v0.2.19
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
@@ -22,16 +23,20 @@ require (
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/sys v0.27.0
|
||||
golang.org/x/text v0.20.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/text v0.22.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
)
|
||||
|
||||
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/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/google/go-cmp v0.6.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/miekg/dns v1.1.62 // 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/prometheus/client_golang v1.20.5 // 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/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // 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/sync v0.9.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // 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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
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/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/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
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.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
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/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
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-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-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/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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-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-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-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.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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
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-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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
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/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-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.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/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
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) Warn(string) {}
|
||||
|
||||
@@ -56,6 +56,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allSettings.SetDefaults()
|
||||
|
||||
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
||||
if err != nil {
|
||||
@@ -75,7 +76,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
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)
|
||||
connection, err := providerConf.GetConnection(
|
||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -24,6 +25,8 @@ import (
|
||||
var (
|
||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be 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 {
|
||||
@@ -35,7 +38,7 @@ type UpdaterLogger interface {
|
||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||
options := settings.Updater{}
|
||||
var endUserMode, maintainerMode, updateAll bool
|
||||
var csvProviders, ipToken string
|
||||
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||
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.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(&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 {
|
||||
return err
|
||||
}
|
||||
@@ -64,6 +71,16 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
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])
|
||||
|
||||
err := options.Validate()
|
||||
@@ -94,7 +111,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||
|
||||
updater := updater.New(httpClient, storage, providers, logger)
|
||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||
|
||||
@@ -36,6 +36,8 @@ var (
|
||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||
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")
|
||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||
|
||||
@@ -2,6 +2,7 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
@@ -24,6 +25,12 @@ type OpenVPNSelection struct {
|
||||
// and can be udp or tcp. It cannot be the empty string
|
||||
// in the internal state.
|
||||
Protocol string `json:"protocol"`
|
||||
// EndpointIP is the server endpoint IP address.
|
||||
// If set, it overrides any IP address from the picked
|
||||
// built-in server connection. To indicate it should
|
||||
// not be used, it should be set to [netip.IPv4Unspecified].
|
||||
// It can never be the zero value in the internal state.
|
||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||
// CustomPort is the OpenVPN server endpoint port.
|
||||
// It can be set to 0 to indicate no custom port should
|
||||
// be used. It cannot be nil in the internal state.
|
||||
@@ -142,6 +149,7 @@ func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
|
||||
return OpenVPNSelection{
|
||||
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
||||
Protocol: o.Protocol,
|
||||
EndpointIP: o.EndpointIP,
|
||||
CustomPort: gosettings.CopyPointer(o.CustomPort),
|
||||
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
||||
}
|
||||
@@ -151,12 +159,14 @@ func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
|
||||
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
||||
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
|
||||
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
|
||||
o.EndpointIP = gosettings.OverrideWithValidator(o.EndpointIP, other.EndpointIP)
|
||||
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
||||
}
|
||||
|
||||
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
||||
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
||||
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
|
||||
o.EndpointIP = gosettings.DefaultValidator(o.EndpointIP, netip.IPv4Unspecified())
|
||||
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
|
||||
|
||||
var defaultEncPreset string
|
||||
@@ -174,6 +184,10 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("OpenVPN server selection settings:")
|
||||
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
|
||||
|
||||
if !o.EndpointIP.IsUnspecified() {
|
||||
node.Appendf("Endpoint IP address: %s", o.EndpointIP)
|
||||
}
|
||||
|
||||
if *o.CustomPort != 0 {
|
||||
node.Appendf("Custom port: %d", *o.CustomPort)
|
||||
}
|
||||
@@ -194,6 +208,12 @@ func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
|
||||
|
||||
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
||||
|
||||
o.EndpointIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
|
||||
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
|
||||
if err != nil {
|
||||
|
||||
@@ -3,7 +3,6 @@ package settings
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
@@ -22,12 +21,6 @@ type ServerSelection struct { //nolint:maligned
|
||||
// or 'wireguard'. It cannot be the empty string
|
||||
// in the internal state.
|
||||
VPN string `json:"vpn"`
|
||||
// TargetIP is the server endpoint IP address to use.
|
||||
// It will override any IP address from the picked
|
||||
// built-in server. It cannot be the empty value in the internal
|
||||
// state, and can be set to the unspecified address to indicate
|
||||
// there is not target IP address to use.
|
||||
TargetIP netip.Addr `json:"target_ip"`
|
||||
// Countries is the list of countries to filter VPN servers with.
|
||||
Countries []string `json:"countries"`
|
||||
// Categories is the list of categories to filter VPN servers with.
|
||||
@@ -299,7 +292,6 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
|
||||
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||
return ServerSelection{
|
||||
VPN: ss.VPN,
|
||||
TargetIP: ss.TargetIP,
|
||||
Countries: gosettings.CopySlice(ss.Countries),
|
||||
Categories: gosettings.CopySlice(ss.Categories),
|
||||
Regions: gosettings.CopySlice(ss.Regions),
|
||||
@@ -323,7 +315,6 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||
|
||||
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
|
||||
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
|
||||
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
|
||||
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
|
||||
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
|
||||
@@ -346,7 +337,6 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||
|
||||
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
|
||||
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
|
||||
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
|
||||
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
|
||||
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
||||
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
||||
@@ -371,9 +361,6 @@ func (ss ServerSelection) String() string {
|
||||
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Server selection settings:")
|
||||
node.Appendf("VPN type: %s", ss.VPN)
|
||||
if !ss.TargetIP.IsUnspecified() {
|
||||
node.Appendf("Target IP address: %s", ss.TargetIP)
|
||||
}
|
||||
|
||||
if len(ss.Countries) > 0 {
|
||||
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
||||
@@ -464,12 +451,6 @@ func (ss *ServerSelection) read(r *reader.Reader,
|
||||
) (err error) {
|
||||
ss.VPN = vpnType
|
||||
|
||||
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
countriesRetroKeys := []string{"COUNTRY"}
|
||||
if vpnProvider == providers.Cyberghost {
|
||||
countriesRetroKeys = append(countriesRetroKeys, "REGION")
|
||||
|
||||
@@ -2,6 +2,7 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -31,6 +32,10 @@ type Updater struct {
|
||||
// Providers is the list of VPN service providers
|
||||
// to update server information for.
|
||||
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) {
|
||||
@@ -51,6 +56,18 @@ func (u Updater) Validate() (err error) {
|
||||
if err != nil {
|
||||
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
|
||||
@@ -58,10 +75,12 @@ func (u Updater) Validate() (err error) {
|
||||
|
||||
func (u *Updater) copy() (copied Updater) {
|
||||
return Updater{
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
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.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||
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) {
|
||||
@@ -87,6 +108,10 @@ func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
||||
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 {
|
||||
@@ -103,6 +128,10 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||
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
|
||||
}
|
||||
@@ -125,6 +154,16 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
|
||||
type WireguardSelection struct {
|
||||
// EndpointIP is the server endpoint IP address.
|
||||
// It is only used with VPN providers generating Wireguard
|
||||
// configurations specific to each server and user.
|
||||
// To indicate it should not be used, it should be set
|
||||
// to netip.IPv4Unspecified(). It can never be the zero value
|
||||
// in the internal state.
|
||||
// It is notably required with the custom provider.
|
||||
// Otherwise it overrides any IP address from the picked
|
||||
// built-in server connection. To indicate it should
|
||||
// not be used, it should be set to [netip.IPv4Unspecified].
|
||||
// It can never be the zero value in the internal state.
|
||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||
// EndpointPort is a the server port to use for the VPN server.
|
||||
// It is optional for VPN providers IVPN, Mullvad, Surfshark
|
||||
|
||||
@@ -37,7 +37,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
if err == nil {
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.logger.Info("ready")
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -54,6 +53,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
l.logAndWait(ctx, err)
|
||||
settings = l.GetSettings()
|
||||
}
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
|
||||
settings = l.GetSettings()
|
||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||
@@ -74,15 +74,19 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
l.stopServer()
|
||||
// TODO revert OS and Go nameserver when exiting
|
||||
if !*l.GetSettings().KeepNameserver {
|
||||
l.stopServer()
|
||||
// TODO revert OS and Go nameserver when exiting
|
||||
}
|
||||
return true
|
||||
case <-l.stop:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("stopping")
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.stopServer()
|
||||
if !*l.GetSettings().KeepNameserver {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.stopServer()
|
||||
}
|
||||
l.stopped <- struct{}{}
|
||||
case <-l.start:
|
||||
l.userTrigger = true
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -61,10 +60,10 @@ func (s *Service) cleanup() (err error) {
|
||||
s.ports = nil
|
||||
|
||||
filepath := s.settings.Filepath
|
||||
s.logger.Info("removing port file " + filepath)
|
||||
err = os.Remove(filepath)
|
||||
s.logger.Info("clearing port file " + filepath)
|
||||
err = s.writePortForwardedFile(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing port file: %w", err)
|
||||
return fmt.Errorf("clearing port file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -13,6 +13,7 @@ var (
|
||||
ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
ErrIPFetcherUnsupported = errors.New("IP fetcher not supported")
|
||||
ErrCredentialsMissing = errors.New("credentials missing")
|
||||
)
|
||||
|
||||
type Fetcher interface {
|
||||
|
||||
@@ -15,12 +15,12 @@ type Provider struct {
|
||||
}
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
parallelResolver common.ParallelResolver,
|
||||
updaterWarner common.Warner, parallelResolver common.ParallelResolver,
|
||||
) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
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()
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
|
||||
type Updater struct {
|
||||
parallelResolver common.ParallelResolver
|
||||
warner common.Warner
|
||||
}
|
||||
|
||||
func New(parallelResolver common.ParallelResolver) *Updater {
|
||||
func New(parallelResolver common.ParallelResolver, warner common.Warner) *Updater {
|
||||
return &Updater{
|
||||
parallelResolver: parallelResolver,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
//nolint:lll
|
||||
func hardcodedServers() (servers []models.Server) {
|
||||
return []models.Server{
|
||||
{Country: "Albania", Hostname: "albania-ca-version-2.expressnetw.com"},
|
||||
@@ -12,69 +11,83 @@ func hardcodedServers() (servers []models.Server) {
|
||||
{Country: "Andorra", Hostname: "andorra-ca-version-2.expressnetw.com"},
|
||||
{Country: "Argentina", Hostname: "argentina-ca-version-2.expressnetw.com"},
|
||||
{Country: "Armenia", Hostname: "armenia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Australia", City: "Adelaide", Hostname: "australia-adelaide--ca-version-2.expressnetw.com"},
|
||||
{Country: "Australia", City: "Brisbane", Hostname: "australia-brisbane-ca-version-2.expressnetw.com"},
|
||||
{Country: "Australia", City: "Melbourne", Hostname: "australia-melbourne-ca-version-2.expressnetw.com"},
|
||||
{Country: "Australia", City: "Perth", Hostname: "australia-perth-ca-version-2.expressnetw.com"},
|
||||
{Country: "Australia", City: "Sydney", Hostname: "australia-sydney-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Australia", City: "Sydney", Hostname: "australia-sydney-ca-version-2.expressnetw.com"},
|
||||
{Country: "Australia", City: "Woolloomooloo", Hostname: "australia-woolloomooloo-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Austria", Hostname: "austria-ca-version-2.expressnetw.com"},
|
||||
{Country: "Azerbaijan", Hostname: "azerbaijan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bahamas", Hostname: "bahamas-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bangladesh", Hostname: "bangladesh-ca-version-2.expressnetw.com"},
|
||||
{Country: "Belarus", Hostname: "belarus-ca-version-2.expressnetw.com"},
|
||||
{Country: "Belgium", Hostname: "belgium-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bermuda", Hostname: "bermuda-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bhutan", Hostname: "bhutan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bosnia And Herzegovina", City: "Bosnia And Herzegovina", Hostname: "bosniaandherzegovina-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bolivia", Hostname: "bolivia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bosnia and Herzegovina", Hostname: "bosniaandherzegovina-ca-version-2.expressnetw.com"},
|
||||
{Country: "Brazil", Hostname: "brazil-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Brazil", Hostname: "brazil-ca-version-2.expressnetw.com"},
|
||||
{Country: "Brunei", Hostname: "brunei-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bulgaria", Hostname: "bulgaria-ca-version-2.expressnetw.com"},
|
||||
{Country: "Cambodia", Hostname: "cambodia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Canada", City: "Montreal", Hostname: "canada-montreal-ca-version-2.expressnetw.com"},
|
||||
{Country: "Canada", City: "Montreal", Hostname: "canada-montreal-ca-version-2.expressnetw.com"},
|
||||
{Country: "Canada", City: "Toronto", Hostname: "canada-toronto-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Canada", City: "Toronto", Hostname: "canada-toronto-ca-version-2.expressnetw.com"},
|
||||
{Country: "Canada", City: "Vancouver", Hostname: "canada-vancouver-ca-version-2.expressnetw.com"},
|
||||
{Country: "Cayman Islands", Hostname: "caymanislands-ca-version-2.expressnetw.com"},
|
||||
{Country: "Chile", Hostname: "chile-ca-version-2.expressnetw.com"},
|
||||
{Country: "Colombia", Hostname: "colombia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Costa Rica", City: "Costa Rica", Hostname: "costarica-ca-version-2.expressnetw.com"},
|
||||
{Country: "Costa Rica", Hostname: "costarica-ca-version-2.expressnetw.com"},
|
||||
{Country: "Croatia", Hostname: "croatia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Cuba", Hostname: "cuba-ca-version-2.expressnetw.com"},
|
||||
{Country: "Cyprus", Hostname: "cyprus-ca-version-2.expressnetw.com"},
|
||||
{Country: "Czech Republic", City: "Czech Republic", Hostname: "czechrepublic-ca-version-2.expressnetw.com"},
|
||||
{Country: "Czech Republic", Hostname: "czechrepublic-ca-version-2.expressnetw.com"},
|
||||
{Country: "Denmark", Hostname: "denmark-ca-version-2.expressnetw.com"},
|
||||
{Country: "Dominican Republic", Hostname: "dominicanrepublic-ca-version-2.expressnetw.com"},
|
||||
{Country: "Ecuador", Hostname: "ecuador-ca-version-2.expressnetw.com"},
|
||||
{Country: "Egypt", Hostname: "egypt-ca-version-2.expressnetw.com"},
|
||||
{Country: "Estonia", Hostname: "estonia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Finland", Hostname: "finland-ca-version-2.expressnetw.com"},
|
||||
{Country: "France", City: "Alsace", Hostname: "france-alsace-ca-version-2.expressnetw.com"},
|
||||
{Country: "France", City: "Marseille", Hostname: "france-marseille-ca-version-2.expressnetw.com"},
|
||||
{Country: "France", City: "Paris", Hostname: "france-paris-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "France", City: "Paris", Hostname: "france-paris-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "France", City: "Strasbourg", Hostname: "france-strasbourg-ca-version-2.expressnetw.com"},
|
||||
{Country: "Georgia", Hostname: "georgia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Germany", City: "Frankfurt", Hostname: "germany-darmstadt-ca-version-2.expressnetw.com"},
|
||||
{Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "Germany", City: "Nuremberg", Hostname: "germany-nuremberg-ca-version-2.expressnetw.com"},
|
||||
{Country: "Ghana", Hostname: "ghana-ca-version-2.expressnetw.com"},
|
||||
{Country: "Greece", Hostname: "greece-ca-version-2.expressnetw.com"},
|
||||
{Country: "Guam", Hostname: "guam-ca-version-2.expressnetw.com"},
|
||||
{Country: "Guatemala", Hostname: "guatemala-ca-version-2.expressnetw.com"},
|
||||
{Country: "Hong Kong", City: "Hong Kong", Hostname: "hongkong-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Hong Kong", City: "Hong Kong", Hostname: "hongkong4-ca-version-2.expressnetw.com"},
|
||||
{Country: "Honduras", Hostname: "honduras-ca-version-2.expressnetw.com"},
|
||||
{Country: "Hong Kong", Hostname: "hongkong-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "Hong Kong", Hostname: "hongkong-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Hungary", Hostname: "hungary-ca-version-2.expressnetw.com"},
|
||||
{Country: "Iceland", Hostname: "iceland-ca-version-2.expressnetw.com"},
|
||||
{Country: "India", City: "Chennai", Hostname: "india-chennai-ca-version-2.expressnetw.com"},
|
||||
{Country: "India", City: "Mumbai", Hostname: "india-mumbai-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "India (via Singapore)", Hostname: "india-sg-ca-version-2.expressnetw.com"},
|
||||
{Country: "India (via UK)", Hostname: "india-uk-ca-version-2.expressnetw.com"},
|
||||
{Country: "Indonesia", Hostname: "indonesia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Ireland", Hostname: "ireland-ca-version-2.expressnetw.com"},
|
||||
{Country: "Isle Of Man", City: "Isle Of Man", Hostname: "isleofman-ca-version-2.expressnetw.com"},
|
||||
{Country: "Isle of Man", Hostname: "isleofman-ca-version-2.expressnetw.com"},
|
||||
{Country: "Israel", Hostname: "israel-ca-version-2.expressnetw.com"},
|
||||
{Country: "Italy", City: "Cosenza", Hostname: "italy-cosenza-ca-version-2.expressnetw.com"},
|
||||
{Country: "Italy", City: "Milan", Hostname: "italy-milan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Kawasaki", Hostname: "japan-kawasaki-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Italy", City: "Naples", Hostname: "italy-naples-ca-version-2.expressnetw.com"},
|
||||
{Country: "Jamaica", Hostname: "jamaica-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Osaka", Hostname: "japan-osaka-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Shibuya", Hostname: "japan-shibuya-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Yokohama", Hostname: "japan-yokohama-ca-version-2.expressnetw.com"},
|
||||
{Country: "Jersey", Hostname: "jersey-ca-version-2.expressnetw.com"},
|
||||
{Country: "Kazakhstan", Hostname: "kazakhstan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Kenya", Hostname: "kenya-ca-version-2.expressnetw.com"},
|
||||
{Country: "Kyrgyzstan", Hostname: "kyrgyzstan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Laos", Hostname: "laos-ca-version-2.expressnetw.com"},
|
||||
{Country: "Latvia", Hostname: "latvia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Lebanon", Hostname: "lebanon-ca-version-2.expressnetw.com"},
|
||||
{Country: "Liechtenstein", Hostname: "liechtenstein-ca-version-2.expressnetw.com"},
|
||||
{Country: "Lithuania", Hostname: "lithuania-ca-version-2.expressnetw.com"},
|
||||
{Country: "Luxembourg", Hostname: "luxembourg-ca-version-2.expressnetw.com"},
|
||||
@@ -86,21 +99,22 @@ func hardcodedServers() (servers []models.Server) {
|
||||
{Country: "Monaco", Hostname: "monaco-ca-version-2.expressnetw.com"},
|
||||
{Country: "Mongolia", Hostname: "mongolia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Montenegro", Hostname: "montenegro-ca-version-2.expressnetw.com"},
|
||||
{Country: "Morocco", Hostname: "morocco-ca-version-2.expressnetw.com"},
|
||||
{Country: "Myanmar", Hostname: "myanmar-ca-version-2.expressnetw.com"},
|
||||
{Country: "Nepal", Hostname: "nepal-ca-version-2.expressnetw.com"},
|
||||
{Country: "Netherlands", City: "Amsterdam", Hostname: "netherlands-amsterdam-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Netherlands", City: "Amsterdam", Hostname: "netherlands-amsterdam-ca-version-2.expressnetw.com"},
|
||||
{Country: "Netherlands", City: "Rotterdam", Hostname: "netherlands-rotterdam-ca-version-2.expressnetw.com"},
|
||||
{Country: "Netherlands", City: "The Hague", Hostname: "netherlands-thehague-ca-version-2.expressnetw.com"},
|
||||
{Country: "New Zealand", City: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
|
||||
{Country: "North Macedonia", City: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
|
||||
{Country: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
|
||||
{Country: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"},
|
||||
{Country: "Pakistan", Hostname: "pakistan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Panama", Hostname: "panama-ca-version-2.expressnetw.com"},
|
||||
{Country: "Peru", Hostname: "peru-ca-version-2.expressnetw.com"},
|
||||
{Country: "Philippines Via Singapore", City: "Philippines Via Singapore", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"},
|
||||
{Country: "Philippines (via Singapore)", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"},
|
||||
{Country: "Poland", Hostname: "poland-ca-version-2.expressnetw.com"},
|
||||
{Country: "Portugal", Hostname: "portugal-ca-version-2.expressnetw.com"},
|
||||
{Country: "Puerto Rico", Hostname: "puertorico-ca-version-2.expressnetw.com"},
|
||||
{Country: "Romania", Hostname: "romania-ca-version-2.expressnetw.com"},
|
||||
{Country: "Serbia", Hostname: "serbia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Singapore", City: "CBD", Hostname: "singapore-cbd-ca-version-2.expressnetw.com"},
|
||||
@@ -108,43 +122,58 @@ func hardcodedServers() (servers []models.Server) {
|
||||
{Country: "Singapore", City: "Marina Bay", Hostname: "singapore-marinabay-ca-version-2.expressnetw.com"},
|
||||
{Country: "Slovakia", Hostname: "slovakia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Slovenia", Hostname: "slovenia-ca-version-2.expressnetw.com"},
|
||||
{Country: "South Africa", City: "South Africa", Hostname: "southafrica-ca-version-2.expressnetw.com"},
|
||||
{Country: "South Korea", City: "South Korea", Hostname: "southkorea2-ca-version-2.expressnetw.com"},
|
||||
{Country: "South Africa", Hostname: "southafrica-ca-version-2.expressnetw.com"},
|
||||
{Country: "South Korea", Hostname: "southkorea2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona-ca-version-2.expressnetw.com"},
|
||||
{Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Spain", City: "Madrid", Hostname: "spain-ca-version-2.expressnetw.com"},
|
||||
{Country: "Sri Lanka", City: "Sri Lanka", Hostname: "srilanka-ca-version-2.expressnetw.com"},
|
||||
{Country: "Sri Lanka", Hostname: "srilanka-ca-version-2.expressnetw.com"},
|
||||
{Country: "Sweden", Hostname: "sweden-ca-version-2.expressnetw.com"},
|
||||
{Country: "Sweden", Hostname: "sweden2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Switzerland", Hostname: "switzerland-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Switzerland", Hostname: "switzerland-ca-version-2.expressnetw.com"},
|
||||
{Country: "Taiwan", Hostname: "taiwan-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Taiwan", Hostname: "taiwan-3-ca-version-2.expressnetw.com"},
|
||||
{Country: "Thailand", Hostname: "thailand-ca-version-2.expressnetw.com"},
|
||||
{Country: "Trinidad and Tobago", Hostname: "trinidadandtobago-ca-version-2.expressnetw.com"},
|
||||
{Country: "Turkey", Hostname: "turkey-ca-version-2.expressnetw.com"},
|
||||
{Country: "Ukraine", Hostname: "ukraine-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "Docklands", Hostname: "uk-berkshire-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "London", Hostname: "uk-east-london-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "Docklands", Hostname: "uk-1-docklands-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "East London", Hostname: "uk-east-london-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "London", Hostname: "uk-london-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "Midlands", Hostname: "uk-midlands-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "Tottenham", Hostname: "uk-tottenham-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "Wembley", Hostname: "uk-wembley-ca-version-2.expressnetw.com"},
|
||||
{Country: "Ukraine", Hostname: "ukraine-ca-version-2.expressnetw.com"},
|
||||
{Country: "Uruguay", Hostname: "uruguay-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Albuquerque", Hostname: "usa-albuquerque-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Atlanta", Hostname: "usa-atlanta-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Boston", Hostname: "us-boston-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Chicago", Hostname: "usa-chicago-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Dallas", Hostname: "usa-dallas-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Dallas", Hostname: "usa-dallas-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Denver", Hostname: "usa-denver-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Houston", Hostname: "usa-houston-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Jackson", Hostname: "us-jackson-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Lincoln Park", Hostname: "usa-lincolnpark-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Little Rock", Hostname: "us-littlerock-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-3-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles5-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles5-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Miami", Hostname: "usa-miami-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Miami", Hostname: "usa-miami-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey2-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-3-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New York", Hostname: "us-new-york-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey2-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New Orleans", Hostname: "us-neworleans-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New York", Hostname: "usa-newyork-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Oklahoma City", Hostname: "us-oklahoma-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Phoenix", Hostname: "usa-phoenix-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Salt Lake City", Hostname: "usa-saltlakecity-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "San Francisco", Hostname: "usa-sanfrancisco-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Santa Monica", Hostname: "usa-santa-monica-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Seattle", Hostname: "usa-seattle-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Tampa", Hostname: "usa-tampa-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Washington DC", Hostname: "usa-washingtondc-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Wichita", Hostname: "us-wichita-ca-version-2.expressnetw.com"},
|
||||
{Country: "Uzbekistan", Hostname: "uzbekistan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Venezuela", Hostname: "venezuela-ca-version-2.expressnetw.com"},
|
||||
{Country: "Vietnam", Hostname: "vietnam-ca-version-2.expressnetw.com"},
|
||||
|
||||
@@ -18,11 +18,12 @@ type Provider struct {
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
client *http.Client, updaterWarner common.Warner,
|
||||
email, password string,
|
||||
) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(client, updaterWarner),
|
||||
Fetcher: updater.New(client, updaterWarner, email, password),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,562 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"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 {
|
||||
LogicalServers []logicalServer `json:"LogicalServers"`
|
||||
@@ -33,25 +580,25 @@ type physicalServer struct {
|
||||
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,
|
||||
) {
|
||||
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)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
c.setHeaders(request, cookie)
|
||||
|
||||
response, err := client.Do(request)
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return data, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
||||
response.StatusCode, response.Status)
|
||||
b, _ := io.ReadAll(response.Body)
|
||||
return data, buildError(response.StatusCode, b)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
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) (
|
||||
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 {
|
||||
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()
|
||||
|
||||
@@ -7,13 +7,17 @@ import (
|
||||
)
|
||||
|
||||
type Updater struct {
|
||||
client *http.Client
|
||||
warner common.Warner
|
||||
client *http.Client
|
||||
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{
|
||||
client: client,
|
||||
warner: warner,
|
||||
client: client,
|
||||
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)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ type Extractor interface {
|
||||
func NewProviders(storage Storage, timeNow func() time.Time,
|
||||
updaterWarner common.Warner, client *http.Client, unzipper common.Unzipper,
|
||||
parallelResolver common.ParallelResolver, ipFetcher common.IPFetcher,
|
||||
extractor custom.Extractor,
|
||||
extractor custom.Extractor, credentials settings.Updater,
|
||||
) *Providers {
|
||||
randSource := rand.NewSource(timeNow().UnixNano())
|
||||
|
||||
@@ -62,7 +62,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
||||
providerNameToProvider := map[string]Provider{
|
||||
providers.Airvpn: airvpn.New(storage, randSource, client),
|
||||
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.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||
providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
@@ -75,7 +75,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
||||
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
|
||||
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.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
||||
|
||||
@@ -26,16 +26,25 @@ func pickConnection(connections []models.Connection,
|
||||
return connection, ErrNoConnectionToPickFrom
|
||||
}
|
||||
|
||||
targetIPSet := selection.TargetIP.IsValid() && !selection.TargetIP.IsUnspecified()
|
||||
var targetIP netip.Addr
|
||||
switch selection.VPN {
|
||||
case vpn.OpenVPN:
|
||||
targetIP = selection.OpenVPN.EndpointIP
|
||||
case vpn.Wireguard:
|
||||
targetIP = selection.Wireguard.EndpointIP
|
||||
default:
|
||||
panic("unknown VPN type: " + selection.VPN)
|
||||
}
|
||||
targetIPSet := targetIP.IsValid() && !targetIP.IsUnspecified()
|
||||
|
||||
if targetIPSet && selection.VPN == vpn.Wireguard {
|
||||
// we need the right public key
|
||||
return getTargetIPConnection(connections, selection.TargetIP)
|
||||
return getTargetIPConnection(connections, targetIP)
|
||||
}
|
||||
|
||||
connection = pickRandomConnection(connections, randSource)
|
||||
if targetIPSet {
|
||||
connection.IP = selection.TargetIP
|
||||
connection.IP = targetIP
|
||||
}
|
||||
|
||||
return connection, nil
|
||||
|
||||
@@ -20,8 +20,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
Ciphers: []string{openvpn.AES256cbc},
|
||||
Auth: openvpn.SHA512,
|
||||
CAs: []string{
|
||||
"MIID7jCCA1CgAwIBAgIQQTT3w3N+5i8OMfe565xaSjAKBggqhkjOPQQDBDCBojELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xGjAYBgNVBAsMEUtlZXBTb2xpZCBSb290IENBMRowGAYDVQQDDBFLZWVwU29saWQgUm9vdCBDQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5Aa2VlcHNvbGlkLmNvbTAeFw0yMDA0MDExNjI3MTRaFw0yNTAzMzExNjI3MTRaMIGgMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEVMBMGA1UECwwMS2VlcFNvbGlkIENBMR0wGwYDVQQDDBRPcGVuVlBOIFNlcnZlciBTdWJDQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5Aa2VlcHNvbGlkLmNvbTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAR9nmoZUraRSSPUhYwIQBLSx+phJdIlqU7F7Hszh95ivnWYkwuizKLaUYy6lSISDohlUtQl9URBlRrGroVctOGlOAdpL2ARTljw5gmUcaavc5cvLiAV7fPJ7BFUgVxInmaVcaMlDwGgKLxmjU2Fw85VLROHbWQjYc93x/BTSFcYO/np4o4IBIzCCAR8wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUrUCjH8xe37lJihyzpqjWwxxNOiswgeIGA1UdIwSB2jCB14AU/LRRnTRaEbxct895Pk9DoymNQIqhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb22CFEssZFYAz8WhYnIDxLeDgKTLD8p2MAsGA1UdDwQEAwIBBjAKBggqhkjOPQQDBAOBiwAwgYcCQgGuK8UNnpE8k8hAamnT9gxCSs5APqrgmdLe6BxYSz7AptpF2/MPzLFsXgj4YxC6vJP8Rs8e3Hw9VJ7DF0aYgu8DvQJBeyFWjRnk8kmu2zEU+wF9fkvN9AJ7v0xF0iEaFVsdPKv6sJQP1sAL+AIepJQ7TYvh9Q9G/WaRCfItCtcOAEz3SKA=", //nolint:lll
|
||||
"MIID9zCCA1igAwIBAgIUSyxkVgDPxaFicgPEt4OApMsPynYwCgYIKoZIzj0EAwQwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb20wIBcNMTkxMjMxMTY1NzMyWhgPMjA1NzA1MTUxNjU3MzJaMIGiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEaMBgGA1UECwwRS2VlcFNvbGlkIFJvb3QgQ0ExGjAYBgNVBAMMEUtlZXBTb2xpZCBSb290IENBMSIwIAYJKoZIhvcNAQkBFhNhZG1pbkBrZWVwc29saWQuY29tMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBlmcBvPVcV7mVgiUVkO9Dh8b4Hdw/eyU8OLWJ0qyRiqI1q0ad1cxgi6asy33xwilrMkRxhArDSfB87zpUpUboTEMBSf9n+dCoGRncGfW9G+8IvhzPY3Z3nzVHBGhoKlN1jsCuKzzpjGawqTAeCkJNBPQNd75Dp6Tgl198bAowD+iPX3WjggEjMIIBHzAdBgNVHQ4EFgQU/LRRnTRaEbxct895Pk9DoymNQIowgeIGA1UdIwSB2jCB14AU/LRRnTRaEbxct895Pk9DoymNQIqhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb22CFEssZFYAz8WhYnIDxLeDgKTLD8p2MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMAoGCCqGSM49BAMEA4GMADCBiAJCAbDIYRjZYaDwMf8Gq4udKVS4aWtZt73lVulCVmr951tQ9J2Dzh4OEQZvU5+M688o2N/fVxNQoxwm/NsiJxpc/prQAkIBiRbrcEGvalu9h6UqE6yAXe0JZcF5xn/BIe5XygglOput4kvZKLKtIqPe2bwBmL/dqq6XDL7s5QaTWPo5MtpzGjA=", //nolint:lll
|
||||
"MIIECjCCA2ygAwIBAgIRAJ/aLZu0PCO7LlOTcPQE9UwwCgYIKoZIzj0EAwQwgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wHhcNMjUwMzMxMTQ0OTU4WhcNMzAwNjEzMTQ0OTU4WjCBqTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xHTAbBgNVBAsMFEtlZXBTb2xpZCBPcGVuVlBOIENBMR0wGwYDVQQDDBRLZWVwU29saWQgT3BlblZQTiBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAEHfJRyn9MZ7HQctQULIxVUNFFw+tWetokml5PvIsS1i3mM4NQnj0HHL5zCCQRKUmSiiWtGvbGlsHEWX/hz+NiVoQGjMqBD2ykdLimiFrceonIofEBZW8to6jTjG3wmJkRykDqsuLyBLUKGc2F5dR3YFGgwyDoRz0NaAYI+qgqWfE+cVaOCASwwggEoMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFB4IhTj1gStDx+fNq+ubBcr+lEbwMIHrBgNVHSMEgeMwgeCAFOEcFx6OcN8T1R8lTdCLhFlYuk5joYGxpIGuMIGrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEeMBwGA1UECwwVS2VlcFNvbGlkIFZQTiBSb290IENBMR4wHAYDVQQDDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExIzAhBgkqhkiG9w0BCQEWFGFkbWluc0BrZWVwc29saWQuY29tghRnfb8jJuxu5dJzLm5ZdurkedrxzjALBgNVHQ8EBAMCAQYwCgYIKoZIzj0EAwQDgYsAMIGHAkIBg8Cdu474VlljCoP8WEr6xErKL6Bygy5+SO1Ey0Uu3B7q8R22F0EWvrOmqmyNZ3oRyqhpUGaEBqB2aqDGT7u7wGsCQUP3nyMlDbXqCF05byMbhQrBsCz1nyqDNnfzM2uGmT09XwWXGCYTIGdynyJJLzdOlpf3T19ZLvqLSf6Kvq45u6si", //nolint:lll
|
||||
"MIIEEDCCA3GgAwIBAgIUZ32/IybsbuXScy5uWXbq5Hna8c4wCgYIKoZIzj0EAwQwgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wHhcNMjUwMzMxMTQ0NTUzWhcNMzUwODI2MTQ0NTUzWjCBqzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xHjAcBgNVBAsMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEeMBwGA1UEAwwVS2VlcFNvbGlkIFZQTiBSb290IENBMSMwIQYJKoZIhvcNAQkBFhRhZG1pbnNAa2VlcHNvbGlkLmNvbTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAN77xqCz3wrFDnRMtggwScgvO6wPFZYECTUu5WW0JaowgmuIgo+BiQQyTeUzJEICulc1Hg7EaUEV+z8jsSrB+4/EAWazn/ufWOx/51fa5FCv4YooCbgLPb1CzYDuTc7MUR5PLQ88o3W01wCCgT8RoNH8uChyPBLUBh2f4rUfpzl20Bqdo4IBLDCCASgwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQU4RwXHo5w3xPVHyVN0IuEWVi6TmMwgesGA1UdIwSB4zCB4IAU4RwXHo5w3xPVHyVN0IuEWVi6TmOhgbGkga4wgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb22CFGd9vyMm7G7l0nMubll26uR52vHOMAsGA1UdDwQEAwIBBjAKBggqhkjOPQQDBAOBjAAwgYgCQgCZtqE+wXwH0ixjWafX3SClp8O3bYeyB/7jbzf8MprXRYBVQ8JjvugjaZTvX82Uy++LaN3oHqK+NUhJUdfZx/eIuQJCAad7HpsKyTYuUUkgAgWXJma4MstxyO9PVRNYozi1oc45Z8deSvwy404n3u1kY5QXLZQaaMY7m2pF+ECs4WkKCh5s", //nolint:lll
|
||||
},
|
||||
ExtraLines: []string{
|
||||
"route-metric 1",
|
||||
|
||||
@@ -114,6 +114,11 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
||||
continue
|
||||
}
|
||||
|
||||
if !*l.settings.Enabled {
|
||||
singleRunResult <- nil
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := l.fetcher.FetchInfo(singleRunCtx, netip.Addr{})
|
||||
if err != nil {
|
||||
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)
|
||||
duration := m.timeNow().Sub(tStart)
|
||||
m.logger.Info(strconv.Itoa(statefulWriter.statusCode) + " " +
|
||||
r.Method + " " + r.RequestURI +
|
||||
r.Method + " " + r.URL.String() +
|
||||
" wrote " + strconv.Itoa(statefulWriter.length) + "B to " +
|
||||
r.RemoteAddr + " in " + duration.String())
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
)
|
||||
|
||||
func commaJoin(slice []string) string {
|
||||
@@ -148,9 +149,13 @@ func noServerFoundError(selection settings.ServerSelection) (err error) {
|
||||
messageParts = append(messageParts, "tor only")
|
||||
}
|
||||
|
||||
if selection.TargetIP.IsValid() {
|
||||
targetIP := selection.OpenVPN.EndpointIP
|
||||
if selection.VPN == vpn.Wireguard {
|
||||
targetIP = selection.Wireguard.EndpointIP
|
||||
}
|
||||
if targetIP.IsValid() {
|
||||
messageParts = append(messageParts,
|
||||
"target ip address "+selection.TargetIP.String())
|
||||
"target ip address "+targetIP.String())
|
||||
}
|
||||
|
||||
message := "for " + strings.Join(messageParts, "; ")
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
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.
|
||||
// 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
|
||||
@@ -10,37 +10,49 @@ import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockInfoer is a mock of Infoer interface.
|
||||
type MockInfoer struct {
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInfoerMockRecorder
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockInfoerMockRecorder is the mock recorder for MockInfoer.
|
||||
type MockInfoerMockRecorder struct {
|
||||
mock *MockInfoer
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockInfoer creates a new mock instance.
|
||||
func NewMockInfoer(ctrl *gomock.Controller) *MockInfoer {
|
||||
mock := &MockInfoer{ctrl: ctrl}
|
||||
mock.recorder = &MockInfoerMockRecorder{mock}
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Info mocks base method.
|
||||
func (m *MockInfoer) Info(arg0 string) {
|
||||
func (m *MockLogger) Info(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Info", arg0)
|
||||
}
|
||||
|
||||
// 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()
|
||||
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()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
logger := NewMockInfoer(ctrl)
|
||||
logger := NewMockLogger(ctrl)
|
||||
var previousLogCall *gomock.Call
|
||||
for _, logged := range testCase.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
|
||||
// SyncServers method.
|
||||
hardcodedServers models.AllServers
|
||||
logger Infoer
|
||||
logger Logger
|
||||
filepath string
|
||||
}
|
||||
|
||||
type Infoer interface {
|
||||
type Logger interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
}
|
||||
|
||||
// New creates a new storage and reads the servers from the
|
||||
// embedded servers file and the file on disk.
|
||||
// Passing an empty filepath disables writing servers to a file.
|
||||
func New(logger Infoer, filepath string) (storage *Storage, err error) {
|
||||
// Passing an empty filepath disables the reading and writing of
|
||||
// servers.
|
||||
func New(logger Logger, filepath string) (storage *Storage, err error) {
|
||||
// A unit test prevents any error from being returned
|
||||
// and ensures all providers are part of the servers returned.
|
||||
hardcodedServers, _ := parseHardcodedServers()
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *Storage) syncServers() (err error) {
|
||||
|
||||
err = s.flushToFile(s.filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing servers to file: %w", err)
|
||||
s.logger.Warn("failed writing servers to file: " + err.Error())
|
||||
}
|
||||
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 " +
|
||||
"-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 {
|
||||
|
||||
@@ -2,9 +2,11 @@ package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
"golang.org/x/text/cases"
|
||||
"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
|
||||
// for NordVPN and PureVPN
|
||||
err := u.updateProvider(ctx, fetcher, minRatio)
|
||||
if err == nil {
|
||||
switch {
|
||||
case err == nil:
|
||||
continue
|
||||
}
|
||||
|
||||
// return the only error for the single provider.
|
||||
if len(providers) == 1 {
|
||||
case errors.Is(err, common.ErrCredentialsMissing):
|
||||
u.logger.Warn(err.Error() + " - skipping update for " + providerName)
|
||||
continue
|
||||
case len(providers) == 1:
|
||||
// return the only error for the single provider.
|
||||
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
|
||||
|
||||
@@ -32,9 +32,14 @@ func (w *Wireguard) addRoutes(link netlink.Link, destinations []netip.Prefix,
|
||||
func (w *Wireguard) addRoute(link netlink.Link, dst netip.Prefix,
|
||||
firewallMark uint32,
|
||||
) (err error) {
|
||||
family := netlink.FamilyV4
|
||||
if dst.Addr().Is6() {
|
||||
family = netlink.FamilyV6
|
||||
}
|
||||
route := netlink.Route{
|
||||
LinkIndex: link.Index,
|
||||
Dst: dst,
|
||||
Family: family,
|
||||
Table: int(firewallMark),
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ func Test_Wireguard_addRoute(t *testing.T) {
|
||||
expectedRoute: netlink.Route{
|
||||
LinkIndex: linkIndex,
|
||||
Dst: ipPrefix,
|
||||
Family: netlink.FamilyV4,
|
||||
Table: firewallMark,
|
||||
},
|
||||
},
|
||||
@@ -49,6 +50,7 @@ func Test_Wireguard_addRoute(t *testing.T) {
|
||||
expectedRoute: netlink.Route{
|
||||
LinkIndex: linkIndex,
|
||||
Dst: ipPrefix,
|
||||
Family: netlink.FamilyV4,
|
||||
Table: firewallMark,
|
||||
},
|
||||
routeAddErr: errDummy,
|
||||
|
||||
Reference in New Issue
Block a user