Compare commits
1 Commits
remove-kee
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4dafaecac |
37
.github/ISSUE_TEMPLATE/provider.md
vendored
37
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -6,35 +6,12 @@ labels: ":bulb: New provider"
|
||||
|
||||
---
|
||||
|
||||
Important notes:
|
||||
One of the following is required:
|
||||
|
||||
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
||||
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
||||
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
|
||||
## For Wireguard
|
||||
|
||||
Wireguard can be natively supported ONLY if:
|
||||
|
||||
- the `PrivateKey` field value is the same across all servers for one user account
|
||||
- the `Address` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `PublicKey` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `Endpoint` field value:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available
|
||||
- can be determined using a pattern, for example using country codes in hostnames
|
||||
|
||||
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
|
||||
|
||||
If **all** of these conditions are met, please provide an answer for each of them.
|
||||
|
||||
## For OpenVPN
|
||||
|
||||
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
|
||||
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
|
||||
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
||||
you can only use a custom Openvpn configuration file.
|
||||
[The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.
|
||||
|
||||
3
.github/workflows/configs/mlc-config.json
vendored
3
.github/workflows/configs/mlc-config.json
vendored
@@ -8,7 +8,6 @@
|
||||
"retryOn429": false,
|
||||
"fallbackRetryDelay": "30s",
|
||||
"aliveStatusCodes": [
|
||||
200,
|
||||
429
|
||||
200
|
||||
]
|
||||
}
|
||||
4
.github/workflows/markdown.yml
vendored
4
.github/workflows/markdown.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v21
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v20
|
||||
with:
|
||||
globs: "**.md"
|
||||
config: .markdownlint-cli2.jsonc
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
use-quiet-mode: yes
|
||||
config-file: .github/workflows/configs/mlc-config.json
|
||||
|
||||
- uses: peter-evans/dockerhub-description@v4
|
||||
- uses: peter-evans/dockerhub-description@v5
|
||||
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
|
||||
with:
|
||||
username: qmcgaw
|
||||
|
||||
@@ -56,9 +56,6 @@ linters:
|
||||
- revive
|
||||
path: internal\/provider\/(common|utils)\/.+\.go
|
||||
text: "var-naming: avoid (bad|meaningless) package names"
|
||||
- linters:
|
||||
- lll
|
||||
source: "^// https://.+$"
|
||||
- linters:
|
||||
- err113
|
||||
- mnd
|
||||
|
||||
28
Dockerfile
28
Dockerfile
@@ -164,22 +164,21 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
# Health
|
||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||
HEALTH_ICMP_TARGET_IP=1.1.1.1 \
|
||||
HEALTH_ICMP_TARGET_IP=0.0.0.0 \
|
||||
HEALTH_RESTART_VPN=on \
|
||||
# DNS
|
||||
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
||||
DNS_UPSTREAM_RESOLVERS=cloudflare \
|
||||
DNS_BLOCK_IPS= \
|
||||
DNS_BLOCK_IP_PREFIXES= \
|
||||
DNS_CACHING=on \
|
||||
DNS_UPSTREAM_IPV6=off \
|
||||
# DNS over TLS
|
||||
DOT=on \
|
||||
DOT_PROVIDERS=cloudflare \
|
||||
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
||||
DOT_CACHING=on \
|
||||
DOT_IPV6=off \
|
||||
BLOCK_MALICIOUS=on \
|
||||
BLOCK_SURVEILLANCE=off \
|
||||
BLOCK_ADS=off \
|
||||
DNS_UNBLOCK_HOSTNAMES= \
|
||||
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
||||
UNBLOCK= \
|
||||
DNS_UPDATE_PERIOD=24h \
|
||||
DNS_UPSTREAM_PLAIN_ADDRESSES= \
|
||||
DNS_ADDRESS=127.0.0.1 \
|
||||
DNS_KEEP_NAMESERVER=off \
|
||||
# HTTP proxy
|
||||
HTTPPROXY= \
|
||||
HTTPPROXY_LOG=off \
|
||||
@@ -200,13 +199,10 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
HTTP_CONTROL_SERVER_LOG=on \
|
||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
|
||||
# Server data updater
|
||||
UPDATER_PERIOD=0 \
|
||||
UPDATER_MIN_RATIO=0.8 \
|
||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||
UPDATER_PROTONVPN_USERNAME= \
|
||||
UPDATER_PROTONVPN_PASSWORD= \
|
||||
# Public IP
|
||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||
PUBLICIP_ENABLED=on \
|
||||
@@ -222,8 +218,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
# Extras
|
||||
VERSION_INFORMATION=on \
|
||||
TZ= \
|
||||
PUID=1000 \
|
||||
PGID=1000
|
||||
PUID= \
|
||||
PGID=
|
||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Gluetun VPN client
|
||||
|
||||
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
|
||||
|
||||
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
||||
|
||||

|
||||
|
||||
@@ -427,8 +427,7 @@ 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, allSettings.Updater)
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
|
||||
|
||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||
@@ -467,10 +466,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
||||
otherGroupHandler.Add(shadowsocksHandler)
|
||||
|
||||
controlServerAddress := *allSettings.ControlServer.Address
|
||||
controlServerLogging := *allSettings.ControlServer.Log
|
||||
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
|
||||
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
||||
logger.New(log.SetComponent("http server")),
|
||||
allSettings.ControlServer.AuthFilePath,
|
||||
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||
storage, ipv6Supported)
|
||||
if err != nil {
|
||||
|
||||
33
go.mod
33
go.mod
@@ -3,40 +3,35 @@ module github.com/qdm12/gluetun
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-srp v0.0.7
|
||||
github.com/breml/rootcerts v0.3.3
|
||||
github.com/breml/rootcerts v0.3.2
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/klauspost/compress v1.18.1
|
||||
github.com/klauspost/compress v1.17.11
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f
|
||||
github.com/pelletier/go-toml/v2 v2.2.3
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc8
|
||||
github.com/qdm12/gosettings v0.4.4
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.2.0
|
||||
github.com/qdm12/gotree v0.3.0
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/qdm12/ss-server v0.6.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/text v0.30.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.1 // 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
|
||||
@@ -47,20 +42,18 @@ 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
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||
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.5 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
96
go.sum
96
go.sum
@@ -1,23 +1,9 @@
|
||||
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.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
|
||||
github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/breml/rootcerts v0.3.2 h1:gm11iClhK8wFn/GDdINoDaqPkiaGXyVvSwoXINZN+z4=
|
||||
github.com/breml/rootcerts v0.3.2/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
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.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/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=
|
||||
@@ -30,8 +16,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -55,10 +41,8 @@ github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE9
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/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=
|
||||
@@ -69,10 +53,10 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f h1:6wN5D9wACfmXDsQ366egVt0jXY4nqL/QnIwg4nWhXco=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
|
||||
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
|
||||
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||
@@ -89,8 +73,8 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
@@ -100,72 +84,48 @@ github.com/vishvananda/netns v0.0.5/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.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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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.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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -7,4 +7,3 @@ func newNoopLogger() *noopLogger {
|
||||
}
|
||||
|
||||
func (l *noopLogger) Info(string) {}
|
||||
func (l *noopLogger) Warn(string) {}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, warner, client,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
||||
connection, err := providerConf.GetConnection(
|
||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,8 +24,6 @@ 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 {
|
||||
@@ -38,7 +35,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, protonUsername, protonPassword string
|
||||
var csvProviders, ipToken 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,
|
||||
@@ -50,8 +47,6 @@ 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", "", "Username 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
|
||||
}
|
||||
@@ -69,11 +64,6 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
options.Providers = strings.Split(csvProviders, ",")
|
||||
}
|
||||
|
||||
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||
options.ProtonUsername = &protonUsername
|
||||
options.ProtonPassword = &protonPassword
|
||||
}
|
||||
|
||||
options.SetDefaults(options.Providers[0])
|
||||
|
||||
err := options.Validate()
|
||||
@@ -81,11 +71,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
return fmt.Errorf("options validation failed: %w", err)
|
||||
}
|
||||
|
||||
serversDataPath := constants.ServersData
|
||||
if maintainerMode {
|
||||
serversDataPath = ""
|
||||
}
|
||||
storage, err := storage.New(logger, serversDataPath)
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating servers storage: %w", err)
|
||||
}
|
||||
@@ -108,7 +94,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, options)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
|
||||
updater := updater.New(httpClient, storage, providers, logger)
|
||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||
|
||||
@@ -14,10 +14,6 @@ func readObsolete(r *reader.Reader) (warnings []string) {
|
||||
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
|
||||
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
|
||||
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
|
||||
"DNS_SERVER": "DNS_SERVER is obsolete because the forwarding server is always enabled.",
|
||||
"DOT": "DOT is obsolete because the forwarding server is always enabled.",
|
||||
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because the forwarding server is always used and " +
|
||||
"forwards local names to private DNS resolvers found in /etc/resolv.conf",
|
||||
}
|
||||
sortedKeys := maps.Keys(keyToMessage)
|
||||
slices.Sort(sortedKeys)
|
||||
|
||||
@@ -1,96 +1,42 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
const (
|
||||
DNSUpstreamTypeDot = "dot"
|
||||
DNSUpstreamTypeDoh = "doh"
|
||||
DNSUpstreamTypePlain = "plain"
|
||||
)
|
||||
|
||||
// DNS contains settings to configure DNS.
|
||||
type DNS struct {
|
||||
// UpstreamType can be [dnsUpstreamTypeDot], [dnsUpstreamTypeDoh]
|
||||
// or [dnsUpstreamTypePlain]. It defaults to [dnsUpstreamTypeDot].
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
// UpdatePeriod is the period to update DNS block lists.
|
||||
// It can be set to 0 to disable the update.
|
||||
// It defaults to 24h and cannot be nil in
|
||||
// the internal state.
|
||||
UpdatePeriod *time.Duration
|
||||
// Providers is a list of DNS providers.
|
||||
// It defaults to either ["cloudflare"] or [] if the
|
||||
// UpstreamPlainAddresses field is set.
|
||||
Providers []string `json:"providers"`
|
||||
// Caching is true if the server should cache
|
||||
// DNS responses.
|
||||
Caching *bool `json:"caching"`
|
||||
// IPv6 is true if the server should connect over IPv6.
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
// Blacklist contains settings to configure the filter
|
||||
// block lists.
|
||||
Blacklist DNSBlacklist
|
||||
// UpstreamPlainAddresses are the upstream plaintext DNS resolver
|
||||
// addresses to use by the built-in DNS server forwarder.
|
||||
// Note, if the upstream type is [dnsUpstreamTypePlain] these are merged
|
||||
// together with provider names set in the Providers field.
|
||||
// If this field is set, the Providers field will default to the empty slice.
|
||||
UpstreamPlainAddresses []netip.AddrPort
|
||||
// ServerAddress is the DNS server to use inside
|
||||
// the Go program and for the system.
|
||||
// It defaults to '127.0.0.1' to be used with the
|
||||
// DoT server. It cannot be the zero value in the internal
|
||||
// state.
|
||||
ServerAddress netip.Addr
|
||||
// KeepNameserver is true if the existing DNS server
|
||||
// found in /etc/resolv.conf should be used
|
||||
// Note setting this to true will likely DNS traffic
|
||||
// outside the VPN tunnel since it would go through
|
||||
// the local DNS server of your Docker/Kubernetes
|
||||
// configuration, which is likely not going through the tunnel.
|
||||
// This will also disable the DNS over TLS server and the
|
||||
// `ServerAddress` field will be ignored.
|
||||
// It defaults to false and cannot be nil in the
|
||||
// internal state.
|
||||
KeepNameserver *bool
|
||||
// DOT contains settings to configure the DoT
|
||||
// server.
|
||||
DoT DoT
|
||||
}
|
||||
|
||||
var (
|
||||
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
|
||||
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
|
||||
ErrDNSUpstreamPlainNoIPv6 = errors.New("upstream plain addresses do not contain any IPv6 address")
|
||||
ErrDNSUpstreamPlainNoIPv4 = errors.New("upstream plain addresses do not contain any IPv4 address")
|
||||
)
|
||||
|
||||
func (d DNS) validate() (err error) {
|
||||
if !helpers.IsOneOf(d.UpstreamType, DNSUpstreamTypeDot, DNSUpstreamTypeDoh, DNSUpstreamTypePlain) {
|
||||
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
|
||||
}
|
||||
|
||||
const minUpdatePeriod = 30 * time.Second
|
||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||
}
|
||||
|
||||
providers := provider.NewProviders()
|
||||
for _, providerName := range d.Providers {
|
||||
_, err := providers.Get(providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
if *d.IPv6 && !slices.ContainsFunc(d.UpstreamPlainAddresses, func(addrPort netip.AddrPort) bool {
|
||||
return addrPort.Addr().Is6()
|
||||
}) {
|
||||
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv6, len(d.UpstreamPlainAddresses))
|
||||
} else if !slices.ContainsFunc(d.UpstreamPlainAddresses, func(addrPort netip.AddrPort) bool {
|
||||
return addrPort.Addr().Is4()
|
||||
}) {
|
||||
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv4, len(d.UpstreamPlainAddresses))
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
err = d.DoT.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("validating DoT settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -98,13 +44,9 @@ func (d DNS) validate() (err error) {
|
||||
|
||||
func (d *DNS) Copy() (copied DNS) {
|
||||
return DNS{
|
||||
UpstreamType: d.UpstreamType,
|
||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||
Providers: gosettings.CopySlice(d.Providers),
|
||||
Caching: gosettings.CopyPointer(d.Caching),
|
||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||
Blacklist: d.Blacklist.copy(),
|
||||
UpstreamPlainAddresses: d.UpstreamPlainAddresses,
|
||||
ServerAddress: d.ServerAddress,
|
||||
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||
DoT: d.DoT.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,72 +54,16 @@ func (d *DNS) Copy() (copied DNS) {
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DNS) overrideWith(other DNS) {
|
||||
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
|
||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||
d.Blacklist.overrideWith(other.Blacklist)
|
||||
d.UpstreamPlainAddresses = gosettings.OverrideWithSlice(d.UpstreamPlainAddresses, other.UpstreamPlainAddresses)
|
||||
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||
d.DoT.overrideWith(other.DoT)
|
||||
}
|
||||
|
||||
func (d *DNS) setDefaults() {
|
||||
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, DNSUpstreamTypeDot)
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||
provider.Cloudflare().Name,
|
||||
})
|
||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||
d.Blacklist.setDefaults()
|
||||
d.UpstreamPlainAddresses = gosettings.DefaultSlice(d.UpstreamPlainAddresses, []netip.AddrPort{})
|
||||
}
|
||||
|
||||
func defaultDNSProviders() []string {
|
||||
return []string{
|
||||
provider.Cloudflare().Name,
|
||||
}
|
||||
}
|
||||
|
||||
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
for _, addrPort := range d.UpstreamPlainAddresses {
|
||||
if addrPort.Addr().Is4() {
|
||||
return addrPort.Addr()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ipv4 = findPlainIPv4InProviders(d.Providers)
|
||||
if ipv4.IsValid() {
|
||||
return ipv4
|
||||
}
|
||||
|
||||
// Either:
|
||||
// - all upstream plain addresses are IPv6 and no provider is set
|
||||
// - all providers set do not have a plaintext IPv4 address
|
||||
ipv4 = findPlainIPv4InProviders(defaultDNSProviders())
|
||||
if !ipv4.IsValid() {
|
||||
panic("no plaintext IPv4 address found in default DNS providers")
|
||||
}
|
||||
return ipv4
|
||||
}
|
||||
|
||||
func findPlainIPv4InProviders(providerNames []string) netip.Addr {
|
||||
providers := provider.NewProviders()
|
||||
for _, name := range providerNames {
|
||||
provider, err := providers.Get(name)
|
||||
if err != nil {
|
||||
// Settings should be validated before calling this function,
|
||||
// so an error happening here is a programming error.
|
||||
panic(err)
|
||||
}
|
||||
if len(provider.Plain.IPv4) > 0 {
|
||||
return provider.Plain.IPv4[0].Addr()
|
||||
}
|
||||
}
|
||||
return netip.Addr{}
|
||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
||||
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||
d.DoT.setDefaults()
|
||||
}
|
||||
|
||||
func (d DNS) String() string {
|
||||
@@ -186,100 +72,30 @@ func (d DNS) String() string {
|
||||
|
||||
func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS settings:")
|
||||
|
||||
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
|
||||
|
||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||
if len(d.UpstreamPlainAddresses) > 0 {
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
for _, addr := range d.UpstreamPlainAddresses {
|
||||
upstreamResolvers.Append(addr.String())
|
||||
}
|
||||
} else {
|
||||
node.Appendf("Upstream plain addresses: ignored because upstream type is not plain")
|
||||
}
|
||||
} else {
|
||||
for _, provider := range d.Providers {
|
||||
upstreamResolvers.Append(provider)
|
||||
}
|
||||
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
|
||||
if *d.KeepNameserver {
|
||||
return node
|
||||
}
|
||||
|
||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||
|
||||
update := "disabled"
|
||||
if *d.UpdatePeriod > 0 {
|
||||
update = "every " + d.UpdatePeriod.String()
|
||||
}
|
||||
node.Appendf("Update period: %s", update)
|
||||
|
||||
node.AppendNode(d.Blacklist.toLinesNode())
|
||||
|
||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||
node.AppendNode(d.DoT.toLinesNode())
|
||||
return node
|
||||
}
|
||||
|
||||
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
||||
|
||||
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
|
||||
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
|
||||
|
||||
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
|
||||
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
|
||||
err = d.DoT.read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Blacklist.read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.readUpstreamPlainAddresses(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("DNS over TLS settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNS) readUpstreamPlainAddresses(r *reader.Reader) (err error) {
|
||||
// If DNS_UPSTREAM_PLAIN_ADDRESSES is set, the user must also set DNS_UPSTREAM_TYPE=plain
|
||||
// for these to be used. This is an added safety measure to reduce misunderstandings, and
|
||||
// reduce odd settings overrides.
|
||||
d.UpstreamPlainAddresses, err = r.CSVNetipAddrPorts("DNS_UPSTREAM_PLAIN_ADDRESSES")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Retro-compatibility - remove in v4
|
||||
// If DNS_ADDRESS is set to a non-localhost address, append it to the other
|
||||
// upstream plain addresses, assuming port 53, and force the upstream type to plain AND
|
||||
// clear any user picked providers, to maintain retro-compatibility behavior.
|
||||
serverAddress, err := r.NetipAddr("DNS_ADDRESS",
|
||||
reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"),
|
||||
reader.IsRetro("DNS_UPSTREAM_PLAIN_ADDRESSES"))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !serverAddress.IsValid() {
|
||||
return nil
|
||||
}
|
||||
isLocalhost := serverAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) == 0
|
||||
if isLocalhost {
|
||||
return nil
|
||||
}
|
||||
const defaultPlainPort = 53
|
||||
addrPort := netip.AddrPortFrom(serverAddress, defaultPlainPort)
|
||||
d.UpstreamPlainAddresses = append(d.UpstreamPlainAddresses, addrPort)
|
||||
d.UpstreamType = DNSUpstreamTypePlain
|
||||
d.Providers = []string{}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_defaultDNSProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
names := defaultDNSProviders()
|
||||
|
||||
found := false
|
||||
providers := provider.NewProviders()
|
||||
for _, name := range names {
|
||||
provider, err := providers.Get(name)
|
||||
require.NoError(t, err)
|
||||
if len(provider.Plain.IPv4) > 0 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "no default DNS provider has a plaintext IPv4 address")
|
||||
}
|
||||
@@ -22,9 +22,6 @@ type DNSBlacklist struct {
|
||||
AddBlockedHosts []string
|
||||
AddBlockedIPs []netip.Addr
|
||||
AddBlockedIPPrefixes []netip.Prefix
|
||||
// RebindingProtectionExemptHostnames is a list of hostnames
|
||||
// exempt from DNS rebinding protection.
|
||||
RebindingProtectionExemptHostnames []string
|
||||
}
|
||||
|
||||
func (b *DNSBlacklist) setDefaults() {
|
||||
@@ -36,9 +33,8 @@ func (b *DNSBlacklist) setDefaults() {
|
||||
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
||||
|
||||
var (
|
||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt host is not valid")
|
||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||
)
|
||||
|
||||
func (b DNSBlacklist) validate() (err error) {
|
||||
@@ -54,25 +50,18 @@ func (b DNSBlacklist) validate() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||
if !hostRegex.MatchString(host) {
|
||||
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
||||
return DNSBlacklist{
|
||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
|
||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +73,6 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
||||
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
|
||||
other.RebindingProtectionExemptHostnames)
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
||||
@@ -142,13 +129,6 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.RebindingProtectionExemptHostnames) > 0 {
|
||||
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
|
||||
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||
exemptHostsNode.Append(host)
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -169,47 +149,23 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
|
||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
|
||||
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
|
||||
|
||||
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
|
||||
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO v4 remove this block below
|
||||
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ips = append(ips, privateIPs...)
|
||||
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
|
||||
|
||||
return ips, ipPrefixes, nil
|
||||
}
|
||||
|
||||
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||
|
||||
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
|
||||
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
|
||||
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
||||
if len(privateAddresses) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
170
internal/configuration/settings/dot.go
Normal file
170
internal/configuration/settings/dot.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// DoT contains settings to configure the DoT server.
|
||||
type DoT struct {
|
||||
// Enabled is true if the DoT server should be running
|
||||
// and used. It defaults to true, and cannot be nil
|
||||
// in the internal state.
|
||||
Enabled *bool
|
||||
// UpdatePeriod is the period to update DNS block lists.
|
||||
// It can be set to 0 to disable the update.
|
||||
// It defaults to 24h and cannot be nil in
|
||||
// the internal state.
|
||||
UpdatePeriod *time.Duration
|
||||
// Providers is a list of DNS over TLS providers
|
||||
Providers []string `json:"providers"`
|
||||
// Caching is true if the DoT server should cache
|
||||
// DNS responses.
|
||||
Caching *bool `json:"caching"`
|
||||
// IPv6 is true if the DoT server should connect over IPv6.
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
// Blacklist contains settings to configure the filter
|
||||
// block lists.
|
||||
Blacklist DNSBlacklist
|
||||
}
|
||||
|
||||
var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
|
||||
|
||||
func (d DoT) validate() (err error) {
|
||||
const minUpdatePeriod = 30 * time.Second
|
||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||
}
|
||||
|
||||
providers := provider.NewProviders()
|
||||
for _, providerName := range d.Providers {
|
||||
_, err := providers.Get(providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DoT) copy() (copied DoT) {
|
||||
return DoT{
|
||||
Enabled: gosettings.CopyPointer(d.Enabled),
|
||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||
Providers: gosettings.CopySlice(d.Providers),
|
||||
Caching: gosettings.CopyPointer(d.Caching),
|
||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||
Blacklist: d.Blacklist.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DoT) overrideWith(other DoT) {
|
||||
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||
d.Blacklist.overrideWith(other.Blacklist)
|
||||
}
|
||||
|
||||
func (d *DoT) setDefaults() {
|
||||
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||
provider.Cloudflare().Name,
|
||||
})
|
||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||
d.Blacklist.setDefaults()
|
||||
}
|
||||
|
||||
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||
providers := provider.NewProviders()
|
||||
provider, err := providers.Get(d.Providers[0])
|
||||
if err != nil {
|
||||
// Settings should be validated before calling this function,
|
||||
// so an error happening here is a programming error.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return provider.DoT.IPv4[0].Addr()
|
||||
}
|
||||
|
||||
func (d DoT) String() string {
|
||||
return d.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (d DoT) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS over TLS settings:")
|
||||
|
||||
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
|
||||
if !*d.Enabled {
|
||||
return node
|
||||
}
|
||||
|
||||
update := "disabled"
|
||||
if *d.UpdatePeriod > 0 {
|
||||
update = "every " + d.UpdatePeriod.String()
|
||||
}
|
||||
node.Appendf("Update period: %s", update)
|
||||
|
||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||
for _, provider := range d.Providers {
|
||||
upstreamResolvers.Append(provider)
|
||||
}
|
||||
|
||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||
|
||||
node.AppendNode(d.Blacklist.toLinesNode())
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (d *DoT) read(reader *reader.Reader) (err error) {
|
||||
d.Enabled, err = reader.BoolPtr("DOT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Providers = reader.CSV("DOT_PROVIDERS")
|
||||
|
||||
d.Caching, err = reader.BoolPtr("DOT_CACHING")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Blacklist.read(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -36,8 +36,6 @@ 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")
|
||||
ErrUpdaterProtonUsernameMissing = errors.New("proton username 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")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
@@ -17,12 +18,18 @@ type Health struct {
|
||||
// for the health check server.
|
||||
// It cannot be the empty string in the internal state.
|
||||
ServerAddress string
|
||||
// ReadHeaderTimeout is the HTTP server header read timeout
|
||||
// duration of the HTTP server. It defaults to 100 milliseconds.
|
||||
ReadHeaderTimeout time.Duration
|
||||
// ReadTimeout is the HTTP read timeout duration of the
|
||||
// HTTP server. It defaults to 500 milliseconds.
|
||||
ReadTimeout time.Duration
|
||||
// TargetAddress is the address (host or host:port)
|
||||
// to TCP TLS dial to periodically for the health check.
|
||||
// It cannot be the empty string in the internal state.
|
||||
TargetAddress string
|
||||
// ICMPTargetIP is the IP address to use for ICMP echo requests
|
||||
// in the health checker. It can be set to an unspecified address (0.0.0.0)
|
||||
// in the health checker. It can be set to an unspecified address
|
||||
// such that the VPN server IP is used, which is also the default behavior.
|
||||
ICMPTargetIP netip.Addr
|
||||
// RestartVPN indicates whether to restart the VPN connection
|
||||
@@ -41,10 +48,12 @@ func (h Health) Validate() (err error) {
|
||||
|
||||
func (h *Health) copy() (copied Health) {
|
||||
return Health{
|
||||
ServerAddress: h.ServerAddress,
|
||||
TargetAddress: h.TargetAddress,
|
||||
ICMPTargetIP: h.ICMPTargetIP,
|
||||
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
||||
ServerAddress: h.ServerAddress,
|
||||
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||
ReadTimeout: h.ReadTimeout,
|
||||
TargetAddress: h.TargetAddress,
|
||||
ICMPTargetIP: h.ICMPTargetIP,
|
||||
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +62,8 @@ func (h *Health) copy() (copied Health) {
|
||||
// settings.
|
||||
func (h *Health) OverrideWith(other Health) {
|
||||
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
||||
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
||||
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
||||
h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
|
||||
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
|
||||
@@ -60,6 +71,10 @@ func (h *Health) OverrideWith(other Health) {
|
||||
|
||||
func (h *Health) SetDefaults() {
|
||||
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
||||
const defaultReadHeaderTimeout = 100 * time.Millisecond
|
||||
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||
const defaultReadTimeout = 500 * time.Millisecond
|
||||
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
||||
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
||||
h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
|
||||
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -27,9 +24,6 @@ type ControlServer struct {
|
||||
// It cannot be empty in the internal state and defaults to
|
||||
// /gluetun/auth/config.toml.
|
||||
AuthFilePath string
|
||||
// AuthDefaultRole is a JSON encoded object defining the default role
|
||||
// that applies to all routes without a previously user-defined role assigned to.
|
||||
AuthDefaultRole string
|
||||
}
|
||||
|
||||
func (c ControlServer) validate() (err error) {
|
||||
@@ -50,30 +44,14 @@ func (c ControlServer) validate() (err error) {
|
||||
ErrControlServerPrivilegedPort, port, uid)
|
||||
}
|
||||
|
||||
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
|
||||
jsonDecoder.DisallowUnknownFields()
|
||||
var role auth.Role
|
||||
err = jsonDecoder.Decode(&role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
|
||||
}
|
||||
|
||||
if role.Auth != "" {
|
||||
err = role.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("default authentication role is not valid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ControlServer) copy() (copied ControlServer) {
|
||||
return ControlServer{
|
||||
Address: gosettings.CopyPointer(c.Address),
|
||||
Log: gosettings.CopyPointer(c.Log),
|
||||
AuthFilePath: c.AuthFilePath,
|
||||
AuthDefaultRole: c.AuthDefaultRole,
|
||||
Address: gosettings.CopyPointer(c.Address),
|
||||
Log: gosettings.CopyPointer(c.Log),
|
||||
AuthFilePath: c.AuthFilePath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,21 +62,12 @@ func (c *ControlServer) overrideWith(other ControlServer) {
|
||||
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
||||
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
||||
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
|
||||
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
|
||||
}
|
||||
|
||||
func (c *ControlServer) setDefaults() {
|
||||
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
||||
c.Log = gosettings.DefaultPointer(c.Log, true)
|
||||
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
|
||||
c.AuthDefaultRole = gosettings.DefaultComparable(c.AuthDefaultRole, "{}")
|
||||
if c.AuthDefaultRole != "{}" {
|
||||
var role auth.Role
|
||||
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||
role.Name = "default"
|
||||
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
|
||||
c.AuthDefaultRole = string(roleBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (c ControlServer) String() string {
|
||||
@@ -110,11 +79,6 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("Listening address: %s", *c.Address)
|
||||
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
||||
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
||||
if c.AuthDefaultRole != "{}" {
|
||||
var role auth.Role
|
||||
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||
node.AppendNode(role.ToLinesNode())
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -127,7 +91,6 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
|
||||
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
||||
|
||||
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
|
||||
c.AuthDefaultRole = r.String("HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
@@ -173,11 +174,13 @@ func (s Settings) Warnings() (warnings []string) {
|
||||
"by creating an issue, attaching the new certificate and we will update Gluetun.")
|
||||
}
|
||||
|
||||
for _, upstreamAddress := range s.DNS.UpstreamPlainAddresses {
|
||||
if upstreamAddress.Addr().IsPrivate() {
|
||||
warnings = append(warnings, "DNS upstream address "+upstreamAddress.String()+" is private: "+
|
||||
"DNS traffic might leak out of the VPN tunnel to that address.")
|
||||
}
|
||||
// TODO remove in v4
|
||||
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
||||
" so the DNS over TLS (DoT) server will not be used."+
|
||||
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
|
||||
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||
" corresponding to the first DoT provider chosen is used.")
|
||||
}
|
||||
|
||||
return warnings
|
||||
|
||||
@@ -38,16 +38,19 @@ func Test_Settings_String(t *testing.T) {
|
||||
| ├── Run OpenVPN as: root
|
||||
| └── Verbosity level: 1
|
||||
├── DNS settings:
|
||||
| ├── Upstream resolver type: dot
|
||||
| ├── Upstream resolvers:
|
||||
| | └── Cloudflare
|
||||
| ├── Caching: yes
|
||||
| ├── IPv6: no
|
||||
| ├── Update period: every 24h0m0s
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
| ├── Keep existing nameserver(s): no
|
||||
| ├── DNS server address to use: 127.0.0.1
|
||||
| └── DNS over TLS settings:
|
||||
| ├── Enabled: yes
|
||||
| ├── Update period: every 24h0m0s
|
||||
| ├── Upstream resolvers:
|
||||
| | └── Cloudflare
|
||||
| ├── Caching: yes
|
||||
| ├── IPv6: no
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
├── Firewall settings:
|
||||
| └── Enabled: yes
|
||||
├── Log settings:
|
||||
|
||||
@@ -2,7 +2,6 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,10 +31,6 @@ type Updater struct {
|
||||
// Providers is the list of VPN service providers
|
||||
// to update server information for.
|
||||
Providers []string
|
||||
// ProtonUsername is the username to authenticate with the Proton API.
|
||||
ProtonUsername *string
|
||||
// ProtonPassword is the password to authenticate with the Proton API.
|
||||
ProtonPassword *string
|
||||
}
|
||||
|
||||
func (u Updater) Validate() (err error) {
|
||||
@@ -56,18 +51,6 @@ func (u Updater) Validate() (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
||||
}
|
||||
|
||||
if provider == providers.Protonvpn {
|
||||
authenticatedAPI := *u.ProtonUsername != "" || *u.ProtonPassword != ""
|
||||
if authenticatedAPI {
|
||||
switch {
|
||||
case *u.ProtonUsername == "":
|
||||
return fmt.Errorf("%w", ErrUpdaterProtonUsernameMissing)
|
||||
case *u.ProtonPassword == "":
|
||||
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -75,12 +58,10 @@ 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),
|
||||
ProtonUsername: gosettings.CopyPointer(u.ProtonUsername),
|
||||
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,8 +73,6 @@ 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.ProtonUsername = gosettings.OverrideWithPointer(u.ProtonUsername, other.ProtonUsername)
|
||||
u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword)
|
||||
}
|
||||
|
||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
@@ -108,10 +87,6 @@ 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.ProtonUsername = gosettings.DefaultPointer(u.ProtonUsername, "")
|
||||
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
|
||||
}
|
||||
|
||||
func (u Updater) String() string {
|
||||
@@ -128,10 +103,6 @@ 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 username: %s", *u.ProtonUsername)
|
||||
node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
@@ -154,14 +125,6 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
||||
|
||||
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
||||
|
||||
u.ProtonUsername = r.Get("UPDATER_PROTONVPN_USERNAME")
|
||||
if u.ProtonUsername != nil {
|
||||
// Enforce to use the username not the email address
|
||||
*u.ProtonUsername = strings.TrimSuffix(*u.ProtonUsername, "@protonmail.com")
|
||||
*u.ProtonUsername = strings.TrimSuffix(*u.ProtonUsername, "@proton.me")
|
||||
}
|
||||
u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +155,7 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
|
||||
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
|
||||
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w - note this MUST be an IP address, "+
|
||||
"see https://github.com/qdm12/gluetun/issues/788", err)
|
||||
return err
|
||||
}
|
||||
|
||||
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||
@@ -17,23 +16,22 @@ import (
|
||||
)
|
||||
|
||||
type Loop struct {
|
||||
statusManager *loopstate.State
|
||||
state *state.State
|
||||
server *server.Server
|
||||
filter *mapfilter.Filter
|
||||
localResolvers []netip.Addr
|
||||
resolvConf string
|
||||
client *http.Client
|
||||
logger Logger
|
||||
userTrigger bool
|
||||
start <-chan struct{}
|
||||
running chan<- models.LoopStatus
|
||||
stop <-chan struct{}
|
||||
stopped chan<- struct{}
|
||||
updateTicker <-chan struct{}
|
||||
backoffTime time.Duration
|
||||
timeNow func() time.Time
|
||||
timeSince func(time.Time) time.Duration
|
||||
statusManager *loopstate.State
|
||||
state *state.State
|
||||
server *server.Server
|
||||
filter *mapfilter.Filter
|
||||
resolvConf string
|
||||
client *http.Client
|
||||
logger Logger
|
||||
userTrigger bool
|
||||
start <-chan struct{}
|
||||
running chan<- models.LoopStatus
|
||||
stop <-chan struct{}
|
||||
stopped chan<- struct{}
|
||||
updateTicker <-chan struct{}
|
||||
backoffTime time.Duration
|
||||
timeNow func() time.Time
|
||||
timeSince func(time.Time) time.Duration
|
||||
}
|
||||
|
||||
const defaultBackoffTime = 10 * time.Second
|
||||
@@ -50,9 +48,7 @@ func NewLoop(settings settings.DNS,
|
||||
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
||||
state := state.New(statusManager, settings, updateTicker)
|
||||
|
||||
filter, err := mapfilter.New(mapfilter.Settings{
|
||||
Logger: buildFilterLogger(logger),
|
||||
})
|
||||
filter, err := mapfilter.New(mapfilter.Settings{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating map filter: %w", err)
|
||||
}
|
||||
@@ -104,15 +100,3 @@ func (l *Loop) signalOrSetStatus(status models.LoopStatus) {
|
||||
l.statusManager.SetStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
type filterLogger struct {
|
||||
logger Logger
|
||||
}
|
||||
|
||||
func (l *filterLogger) Log(msg string) {
|
||||
l.logger.Info(msg)
|
||||
}
|
||||
|
||||
func buildFilterLogger(logger Logger) *filterLogger {
|
||||
return &filterLogger{logger: logger}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,15 @@ import (
|
||||
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
settings := l.GetSettings()
|
||||
|
||||
targetIP := settings.GetFirstPlaintextIPv4()
|
||||
// Try with user provided plaintext ip address
|
||||
// if it's not 127.0.0.1 (default for DoT), otherwise
|
||||
// use the first DoT provider ipv4 address found.
|
||||
var targetIP netip.Addr
|
||||
if settings.ServerAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
targetIP = settings.ServerAddress
|
||||
} else {
|
||||
targetIP = settings.DoT.GetFirstPlaintextIPv4()
|
||||
}
|
||||
|
||||
if fallback {
|
||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||
@@ -19,15 +27,14 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
}
|
||||
|
||||
const dialTimeout = 3 * time.Second
|
||||
const defaultDNSPort = 53
|
||||
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
||||
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
|
||||
Timeout: dialTimeout,
|
||||
IP: targetIP,
|
||||
Timeout: dialTimeout,
|
||||
}
|
||||
nameserver.UseDNSInternally(settingsInternalDNS)
|
||||
|
||||
settingsSystemWide := nameserver.SettingsSystemDNS{
|
||||
IPs: []netip.Addr{targetIP},
|
||||
IP: targetIP,
|
||||
ResolvPath: l.resolvConf,
|
||||
}
|
||||
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
||||
|
||||
@@ -4,23 +4,21 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
)
|
||||
|
||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
var err error
|
||||
l.localResolvers, err = nameserver.GetPrivateDNSServers()
|
||||
if err != nil {
|
||||
l.logger.Error("getting private DNS servers: " + err.Error())
|
||||
return
|
||||
if *l.GetSettings().KeepNameserver {
|
||||
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
|
||||
"this will likely leak DNS traffic outside the VPN " +
|
||||
"and go through your container network DNS outside the VPN tunnel!")
|
||||
} else {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
|
||||
select {
|
||||
case <-l.start:
|
||||
case <-ctx.Done():
|
||||
@@ -28,16 +26,18 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
// Upper scope variables for the DNS forwarder server only
|
||||
// Upper scope variables for the DNS over TLS server only
|
||||
// Their values are to be used if DOT=off
|
||||
var runError <-chan error
|
||||
|
||||
for {
|
||||
settings := l.GetSettings()
|
||||
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
||||
var err error
|
||||
runError, err = l.setupServer(ctx)
|
||||
if err == nil {
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.logger.Info("ready")
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -52,11 +52,14 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
l.logAndWait(ctx, err)
|
||||
settings = l.GetSettings()
|
||||
}
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
settings = l.GetSettings()
|
||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
|
||||
l.userTrigger = false
|
||||
|
||||
@@ -98,6 +101,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
func (l *Loop) stopServer() {
|
||||
stopErr := l.server.Stop()
|
||||
if stopErr != nil {
|
||||
l.logger.Error("stopping server: " + stopErr.Error())
|
||||
l.logger.Error("stopping DoT server: " + stopErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,12 @@ package dns
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
"github.com/qdm12/dns/v2/pkg/dot"
|
||||
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
|
||||
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/localdns"
|
||||
"github.com/qdm12/dns/v2/pkg/plain"
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/dns/v2/pkg/server"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
@@ -26,55 +22,33 @@ func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
return l.state.SetSettings(ctx, settings)
|
||||
}
|
||||
|
||||
func buildServerSettings(userSettings settings.DNS,
|
||||
filter *mapfilter.Filter, localResolvers []netip.Addr,
|
||||
logger Logger) (
|
||||
func buildDoTSettings(settings settings.DNS,
|
||||
filter *mapfilter.Filter, logger Logger) (
|
||||
serverSettings server.Settings, err error,
|
||||
) {
|
||||
serverSettings.Logger = logger
|
||||
|
||||
upstreamResolvers := buildProviders(userSettings)
|
||||
|
||||
ipVersion := "ipv4"
|
||||
if *userSettings.IPv6 {
|
||||
ipVersion = "ipv6"
|
||||
var dotSettings dot.Settings
|
||||
providersData := provider.NewProviders()
|
||||
dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.DoT.Providers))
|
||||
for i := range settings.DoT.Providers {
|
||||
var err error
|
||||
dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.DoT.Providers[i])
|
||||
if err != nil {
|
||||
panic(err) // this should already had been checked
|
||||
}
|
||||
}
|
||||
dotSettings.IPVersion = "ipv4"
|
||||
if *settings.DoT.IPv6 {
|
||||
dotSettings.IPVersion = "ipv6"
|
||||
}
|
||||
|
||||
var dialer server.Dialer
|
||||
switch userSettings.UpstreamType {
|
||||
case settings.DNSUpstreamTypeDot:
|
||||
dialerSettings := dot.Settings{
|
||||
UpstreamResolvers: upstreamResolvers,
|
||||
IPVersion: ipVersion,
|
||||
}
|
||||
dialer, err = dot.New(dialerSettings)
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
||||
}
|
||||
case settings.DNSUpstreamTypeDoh:
|
||||
dialerSettings := doh.Settings{
|
||||
UpstreamResolvers: upstreamResolvers,
|
||||
IPVersion: ipVersion,
|
||||
}
|
||||
dialer, err = doh.New(dialerSettings)
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err)
|
||||
}
|
||||
case settings.DNSUpstreamTypePlain:
|
||||
dialerSettings := plain.Settings{
|
||||
UpstreamResolvers: upstreamResolvers,
|
||||
IPVersion: ipVersion,
|
||||
}
|
||||
dialer, err = plain.New(dialerSettings)
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err)
|
||||
}
|
||||
default:
|
||||
panic("unknown upstream type: " + userSettings.UpstreamType)
|
||||
serverSettings.Dialer, err = dot.New(dotSettings)
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
||||
}
|
||||
serverSettings.Dialer = dialer
|
||||
|
||||
if *userSettings.Caching {
|
||||
if *settings.DoT.Caching {
|
||||
lruCache, err := lru.New(lru.Settings{})
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
|
||||
@@ -96,67 +70,5 @@ func buildServerSettings(userSettings settings.DNS,
|
||||
}
|
||||
serverSettings.Middlewares = append(serverSettings.Middlewares, filterMiddleware)
|
||||
|
||||
localResolversAddrPorts := make([]netip.AddrPort, len(localResolvers))
|
||||
const defaultDNSPort = 53
|
||||
for i, addr := range localResolvers {
|
||||
localResolversAddrPorts[i] = netip.AddrPortFrom(addr, defaultDNSPort)
|
||||
}
|
||||
localDNSMiddleware, err := localdns.New(localdns.Settings{
|
||||
Resolvers: localResolversAddrPorts, // auto-detected at container start only
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating local DNS middleware: %w", err)
|
||||
}
|
||||
// Place after cache middleware, since we want to avoid caching for local
|
||||
// hostnames that may change regularly.
|
||||
// Place after filter middleware to avoid conflicts with the rebinding protection.
|
||||
serverSettings.Middlewares = append(serverSettings.Middlewares, localDNSMiddleware)
|
||||
|
||||
return serverSettings, nil
|
||||
}
|
||||
|
||||
func buildProviders(userSettings settings.DNS) []provider.Provider {
|
||||
if userSettings.UpstreamType == settings.DNSUpstreamTypePlain &&
|
||||
len(userSettings.UpstreamPlainAddresses) > 0 {
|
||||
providers := make([]provider.Provider, len(userSettings.UpstreamPlainAddresses))
|
||||
for i, addrPort := range userSettings.UpstreamPlainAddresses {
|
||||
providers[i] = provider.Provider{
|
||||
Name: addrPort.String(),
|
||||
}
|
||||
if addrPort.Addr().Is4() {
|
||||
providers[i].Plain.IPv4 = []netip.AddrPort{addrPort}
|
||||
} else {
|
||||
providers[i].Plain.IPv6 = []netip.AddrPort{addrPort}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
providersData := provider.NewProviders()
|
||||
providers := make([]provider.Provider, 0, len(userSettings.Providers)+len(userSettings.UpstreamPlainAddresses))
|
||||
for _, providerName := range userSettings.Providers {
|
||||
provider, err := providersData.Get(providerName)
|
||||
if err != nil {
|
||||
panic(err) // this should already had been checked
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
if userSettings.UpstreamType != settings.DNSUpstreamTypePlain {
|
||||
return providers
|
||||
}
|
||||
|
||||
for _, addrPort := range userSettings.UpstreamPlainAddresses {
|
||||
newProvider := provider.Provider{
|
||||
Name: addrPort.String(),
|
||||
}
|
||||
if addrPort.Addr().Is4() {
|
||||
newProvider.Plain.IPv4 = []netip.AddrPort{addrPort}
|
||||
} else {
|
||||
newProvider.Plain.IPv6 = []netip.AddrPort{addrPort}
|
||||
}
|
||||
providers = append(providers, newProvider)
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
||||
|
||||
settings := l.GetSettings()
|
||||
|
||||
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
|
||||
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("building server settings: %w", err)
|
||||
return nil, fmt.Errorf("building DoT settings: %w", err)
|
||||
}
|
||||
|
||||
server, err := server.New(serverSettings)
|
||||
server, err := server.New(dotSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating server: %w", err)
|
||||
return nil, fmt.Errorf("creating DoT server: %w", err)
|
||||
}
|
||||
|
||||
runError, err = server.Start(ctx)
|
||||
@@ -37,8 +37,11 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
||||
l.server = server
|
||||
|
||||
// use internal DNS server
|
||||
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{})
|
||||
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
|
||||
IP: settings.ServerAddress,
|
||||
})
|
||||
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
||||
IP: settings.ServerAddress,
|
||||
ResolvPath: l.resolvConf,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -27,7 +27,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
|
||||
// Check for only update period change
|
||||
tempSettings := s.settings.Copy()
|
||||
*tempSettings.UpdatePeriod = *settings.UpdatePeriod
|
||||
*tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod
|
||||
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
||||
|
||||
s.settings = settings
|
||||
@@ -40,6 +40,8 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
|
||||
// Restart
|
||||
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
||||
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||
if *settings.DoT.Enabled {
|
||||
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
timer.Stop()
|
||||
timerIsStopped := true
|
||||
settings := l.GetSettings()
|
||||
if period := *settings.UpdatePeriod; period > 0 {
|
||||
if period := *settings.DoT.UpdatePeriod; period > 0 {
|
||||
timer.Reset(period)
|
||||
timerIsStopped = false
|
||||
}
|
||||
@@ -43,14 +43,14 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
||||
|
||||
settings := l.GetSettings()
|
||||
timer.Reset(*settings.UpdatePeriod)
|
||||
timer.Reset(*settings.DoT.UpdatePeriod)
|
||||
case <-l.updateTicker:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
timerIsStopped = true
|
||||
settings := l.GetSettings()
|
||||
newUpdatePeriod := *settings.UpdatePeriod
|
||||
newUpdatePeriod := *settings.DoT.UpdatePeriod
|
||||
if newUpdatePeriod == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
||||
settings := l.GetSettings()
|
||||
|
||||
l.logger.Info("downloading hostnames and IP block lists")
|
||||
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
|
||||
blacklistSettings := settings.DoT.Blacklist.ToBlockBuilderSettings(l.client)
|
||||
|
||||
blockBuilder, err := blockbuilder.New(blacklistSettings)
|
||||
if err != nil {
|
||||
@@ -37,7 +37,6 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
||||
IPPrefixes: result.BlockedIPPrefixes,
|
||||
}
|
||||
updateSettings.BlockHostnames(result.BlockedHostnames)
|
||||
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
|
||||
err = l.filter.Update(updateSettings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating filter: %w", err)
|
||||
|
||||
@@ -323,12 +323,12 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
|
||||
|
||||
func parseProtocol(s string) (protocol string, err error) {
|
||||
switch s {
|
||||
case "0", "all":
|
||||
case "1", "icmp":
|
||||
case "0":
|
||||
case "1":
|
||||
protocol = "icmp"
|
||||
case "6", "tcp":
|
||||
case "6":
|
||||
protocol = "tcp"
|
||||
case "17", "udp":
|
||||
case "17":
|
||||
protocol = "udp"
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)
|
||||
|
||||
@@ -58,7 +58,6 @@ num pkts bytes target prot opt in out source destinati
|
||||
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
||||
3 0 0 ACCEPT 1 -- tun0 * 0.0.0.0/0 0.0.0.0/0
|
||||
4 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
|
||||
5 0 0 ACCEPT all -- tun0 * 1.2.3.4 0.0.0.0/0
|
||||
`,
|
||||
table: chain{
|
||||
name: "INPUT",
|
||||
@@ -112,17 +111,6 @@ num pkts bytes target prot opt in out source destinati
|
||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
},
|
||||
{
|
||||
lineNumber: 5,
|
||||
packets: 0,
|
||||
bytes: 0,
|
||||
target: "ACCEPT",
|
||||
protocol: "",
|
||||
inputInterface: "tun0",
|
||||
outputInterface: "*",
|
||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -131,18 +131,9 @@ func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
|
||||
c.configMutex.Lock()
|
||||
ip := c.icmpTarget
|
||||
c.configMutex.Unlock()
|
||||
tryTimeouts := []time.Duration{
|
||||
5 * time.Second,
|
||||
5 * time.Second,
|
||||
5 * time.Second,
|
||||
10 * time.Second,
|
||||
10 * time.Second,
|
||||
10 * time.Second,
|
||||
15 * time.Second,
|
||||
15 * time.Second,
|
||||
15 * time.Second,
|
||||
30 * time.Second,
|
||||
}
|
||||
const maxTries = 3
|
||||
const timeout = 10 * time.Second
|
||||
const extraTryTime = 10 * time.Second // 10s added for each subsequent retry
|
||||
check := func(ctx context.Context) error {
|
||||
if c.icmpNotPermitted {
|
||||
return c.dnsClient.Check(ctx)
|
||||
@@ -156,17 +147,19 @@ func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
return withRetries(ctx, tryTimeouts, c.logger, c.smallCheckName, check)
|
||||
return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, c.smallCheckName, check)
|
||||
}
|
||||
|
||||
func (c *Checker) fullPeriodicCheck(ctx context.Context) error {
|
||||
const maxTries = 2
|
||||
// 20s timeout in case the connection is under stress
|
||||
// See https://github.com/qdm12/gluetun/issues/2270
|
||||
tryTimeouts := []time.Duration{10 * time.Second, 15 * time.Second, 30 * time.Second}
|
||||
const timeout = 20 * time.Second
|
||||
const extraTryTime = 10 * time.Second // 10s added for each subsequent retry
|
||||
check := func(ctx context.Context) error {
|
||||
return tcpTLSCheck(ctx, c.dialer, c.tlsDialAddr)
|
||||
}
|
||||
return withRetries(ctx, tryTimeouts, c.logger, "TCP+TLS dial", check)
|
||||
return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, "TCP+TLS dial", check)
|
||||
}
|
||||
|
||||
func tcpTLSCheck(ctx context.Context, dialer *net.Dialer, targetAddress string) error {
|
||||
@@ -225,17 +218,13 @@ func makeAddressToDial(address string) (addressToDial string, err error) {
|
||||
|
||||
var ErrAllCheckTriesFailed = errors.New("all check tries failed")
|
||||
|
||||
func withRetries(ctx context.Context, tryTimeouts []time.Duration,
|
||||
func withRetries(ctx context.Context, maxTries uint, tryTimeout, extraTryTime time.Duration,
|
||||
logger Logger, checkName string, check func(ctx context.Context) error,
|
||||
) error {
|
||||
maxTries := len(tryTimeouts)
|
||||
type errData struct {
|
||||
err error
|
||||
duration time.Duration
|
||||
}
|
||||
errs := make([]errData, maxTries)
|
||||
for i, timeout := range tryTimeouts {
|
||||
start := time.Now()
|
||||
try := uint(0)
|
||||
var errs []error
|
||||
for {
|
||||
timeout := tryTimeout + time.Duration(try)*extraTryTime //nolint:gosec
|
||||
checkCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
err := check(checkCtx)
|
||||
cancel()
|
||||
@@ -245,14 +234,17 @@ func withRetries(ctx context.Context, tryTimeouts []time.Duration,
|
||||
case ctx.Err() != nil:
|
||||
return fmt.Errorf("%s: %w", checkName, ctx.Err())
|
||||
}
|
||||
logger.Debugf("%s attempt %d/%d failed: %s", checkName, i+1, maxTries, err)
|
||||
errs[i].err = err
|
||||
errs[i].duration = time.Since(start)
|
||||
logger.Debugf("%s attempt %d/%d failed: %s", checkName, try+1, maxTries, err)
|
||||
errs = append(errs, err)
|
||||
try++
|
||||
if try < maxTries {
|
||||
continue
|
||||
}
|
||||
errStrings := make([]string, len(errs))
|
||||
for i, err := range errs {
|
||||
errStrings[i] = fmt.Sprintf("attempt %d: %s", i+1, err.Error())
|
||||
}
|
||||
return fmt.Errorf("%w: after %d %s attempts (%s)",
|
||||
ErrAllCheckTriesFailed, maxTries, checkName, strings.Join(errStrings, "; "))
|
||||
}
|
||||
|
||||
errStrings := make([]string, len(errs))
|
||||
for i, err := range errs {
|
||||
errStrings[i] = fmt.Sprintf("attempt %d (%s): %s", i+1, err.duration, err.err)
|
||||
}
|
||||
return fmt.Errorf("%w: %s", ErrAllCheckTriesFailed, strings.Join(errStrings, ", "))
|
||||
}
|
||||
|
||||
@@ -5,60 +5,33 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
)
|
||||
|
||||
// Client is a simple plaintext UDP DNS client, to be used for healthchecks.
|
||||
// Note the client connects to a DNS server only over UDP on port 53,
|
||||
// because we don't want to use DoT or DoH and impact the TCP connections
|
||||
// when running a healthcheck.
|
||||
type Client struct {
|
||||
serverAddrs []netip.AddrPort
|
||||
dnsIPIndex int
|
||||
}
|
||||
type Client struct{}
|
||||
|
||||
func New() *Client {
|
||||
return &Client{
|
||||
serverAddrs: concatAddrPorts([][]netip.AddrPort{
|
||||
provider.Cloudflare().Plain.IPv4,
|
||||
provider.Google().Plain.IPv4,
|
||||
provider.Quad9().Plain.IPv4,
|
||||
provider.OpenDNS().Plain.IPv4,
|
||||
provider.LibreDNS().Plain.IPv4,
|
||||
provider.Quadrant().Plain.IPv4,
|
||||
provider.CiraProtected().Plain.IPv4,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func concatAddrPorts(addrs [][]netip.AddrPort) []netip.AddrPort {
|
||||
var result []netip.AddrPort
|
||||
for _, addrList := range addrs {
|
||||
result = append(result, addrList...)
|
||||
}
|
||||
return result
|
||||
return &Client{}
|
||||
}
|
||||
|
||||
var ErrLookupNoIPs = errors.New("no IPs found from DNS lookup")
|
||||
|
||||
func (c *Client) Check(ctx context.Context) error {
|
||||
dnsAddr := c.serverAddrs[c.dnsIPIndex].String()
|
||||
resolver := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
dialer := net.Dialer{}
|
||||
return dialer.DialContext(ctx, "udp", dnsAddr)
|
||||
return dialer.DialContext(ctx, "udp", "1.1.1.1:53")
|
||||
},
|
||||
}
|
||||
ips, err := resolver.LookupIP(ctx, "ip", "github.com")
|
||||
switch {
|
||||
case err != nil:
|
||||
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
||||
return err
|
||||
case len(ips) == 0:
|
||||
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
||||
return fmt.Errorf("%w", ErrLookupNoIPs)
|
||||
default:
|
||||
return nil
|
||||
|
||||
@@ -10,13 +10,11 @@ import (
|
||||
func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
const readHeaderTimeout = 100 * time.Millisecond
|
||||
const readTimeout = 500 * time.Millisecond
|
||||
server := http.Server{
|
||||
Addr: s.config.ServerAddress,
|
||||
Handler: s.handler,
|
||||
ReadHeaderTimeout: readHeaderTimeout,
|
||||
ReadTimeout: readTimeout,
|
||||
ReadHeaderTimeout: s.config.ReadHeaderTimeout,
|
||||
ReadTimeout: s.config.ReadTimeout,
|
||||
}
|
||||
serverDone := make(chan struct{})
|
||||
go func() {
|
||||
|
||||
@@ -18,7 +18,6 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
||||
}
|
||||
portsString := strings.Join(portStrings, ",")
|
||||
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
||||
commandString = strings.ReplaceAll(commandString, "{{PORT}}", portStrings[0])
|
||||
args, err := command.Split(commandString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing command: %w", err)
|
||||
|
||||
@@ -14,11 +14,7 @@ func (s *Service) writePortForwardedFile(ports []uint16) (err error) {
|
||||
fileData := []byte(strings.Join(portStrings, "\n"))
|
||||
|
||||
filepath := s.settings.Filepath
|
||||
if len(ports) == 0 {
|
||||
s.logger.Info("clearing port file " + filepath)
|
||||
} else {
|
||||
s.logger.Info("writing port file " + filepath)
|
||||
}
|
||||
s.logger.Info("writing port file " + filepath)
|
||||
const perms = os.FileMode(0o644)
|
||||
err = os.WriteFile(filepath, fileData, perms)
|
||||
if err != nil {
|
||||
|
||||
@@ -59,6 +59,8 @@ func (s *Service) cleanup() (err error) {
|
||||
|
||||
s.ports = nil
|
||||
|
||||
filepath := s.settings.Filepath
|
||||
s.logger.Info("clearing port file " + filepath)
|
||||
err = s.writePortForwardedFile(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clearing port file: %w", err)
|
||||
|
||||
@@ -13,7 +13,6 @@ 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,
|
||||
updaterWarner common.Warner, parallelResolver common.ParallelResolver,
|
||||
parallelResolver common.ParallelResolver,
|
||||
) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(parallelResolver, updaterWarner),
|
||||
Fetcher: updater.New(parallelResolver),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,7 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||
|
||||
possibleHosts := possibleServers.hostsSlice()
|
||||
resolveSettings := parallelResolverSettings(possibleHosts)
|
||||
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||
for _, warning := range warnings {
|
||||
u.warner.Warn(warning)
|
||||
}
|
||||
hostToIPs, _, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import (
|
||||
|
||||
type Updater struct {
|
||||
parallelResolver common.ParallelResolver
|
||||
warner common.Warner
|
||||
}
|
||||
|
||||
func New(parallelResolver common.ParallelResolver, warner common.Warner) *Updater {
|
||||
func New(parallelResolver common.ParallelResolver) *Updater {
|
||||
return &Updater{
|
||||
parallelResolver: parallelResolver,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,11 @@ type Provider struct {
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
client *http.Client, updaterWarner common.Warner,
|
||||
username, password string,
|
||||
) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(client, updaterWarner, username, password),
|
||||
Fetcher: updater.New(client, updaterWarner),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,567 +1,15 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// 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, username, 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,
|
||||
}
|
||||
modulusPGPClearSigned, serverEphemeralBase64, saltBase64,
|
||||
srpSessionHex, version, err := c.authInfo(ctx, username, 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, username, 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")
|
||||
ErrUsernameMismatch = errors.New("username in response does not match request username")
|
||||
)
|
||||
|
||||
// authInfo fetches SRP parameters for the account.
|
||||
func (c *apiClient) authInfo(ctx context.Context, username string, unauthCookie cookie) (
|
||||
modulusPGPClearSigned, serverEphemeralBase64, saltBase64, srpSessionHex string,
|
||||
version int, err error,
|
||||
) {
|
||||
type requestBodySchema struct {
|
||||
Intent string `json:"Intent"` // "Proton"
|
||||
Username string `json:"Username"` // username without @domain.com
|
||||
}
|
||||
requestBody := requestBodySchema{
|
||||
Intent: "Proton",
|
||||
Username: username,
|
||||
}
|
||||
|
||||
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 !strings.EqualFold(info.Username, username):
|
||||
return "", "", "", "", 0, fmt.Errorf("%w: expected %s got %s",
|
||||
ErrUsernameMismatch, username, info.Username)
|
||||
case info.Version == nil:
|
||||
return "", "", "", "", 0, fmt.Errorf("%w: version is missing", ErrDataFieldMissing)
|
||||
}
|
||||
|
||||
version = int(*info.Version) //nolint:gosec
|
||||
return 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()
|
||||
}
|
||||
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
|
||||
type apiData struct {
|
||||
LogicalServers []logicalServer `json:"LogicalServers"`
|
||||
@@ -585,25 +33,25 @@ type physicalServer struct {
|
||||
X25519PublicKey string `json:"X25519PublicKey"`
|
||||
}
|
||||
|
||||
func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
||||
data apiData, err error,
|
||||
) {
|
||||
const url = "https://account.proton.me/api/vpn/logicals"
|
||||
const url = "https://api.protonmail.ch/vpn/logicals"
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
c.setHeaders(request, cookie)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(response.Body)
|
||||
return data, buildError(response.StatusCode, b)
|
||||
return data, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
||||
response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
@@ -611,31 +59,9 @@ func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
||||
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,26 +13,9 @@ import (
|
||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||
servers []models.Server, err error,
|
||||
) {
|
||||
switch {
|
||||
case u.username == "":
|
||||
return nil, fmt.Errorf("%w: username is empty", common.ErrCredentialsMissing)
|
||||
case u.password == "":
|
||||
return nil, fmt.Errorf("%w: password is empty", common.ErrCredentialsMissing)
|
||||
}
|
||||
|
||||
apiClient, err := newAPIClient(ctx, u.client)
|
||||
data, err := fetchAPI(ctx, u.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating API client: %w", err)
|
||||
}
|
||||
|
||||
cookie, err := apiClient.authenticate(ctx, u.username, 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)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
countryCodes := constants.CountryCodes()
|
||||
|
||||
@@ -7,17 +7,13 @@ import (
|
||||
)
|
||||
|
||||
type Updater struct {
|
||||
client *http.Client
|
||||
username string
|
||||
password string
|
||||
warner common.Warner
|
||||
client *http.Client
|
||||
warner common.Warner
|
||||
}
|
||||
|
||||
func New(client *http.Client, warner common.Warner, username, password string) *Updater {
|
||||
func New(client *http.Client, warner common.Warner) *Updater {
|
||||
return &Updater{
|
||||
client: client,
|
||||
username: username,
|
||||
password: password,
|
||||
warner: warner,
|
||||
client: client,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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, credentials settings.Updater,
|
||||
extractor custom.Extractor,
|
||||
) *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, updaterWarner, parallelResolver),
|
||||
providers.Cyberghost: cyberghost.New(storage, randSource, 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, *credentials.ProtonUsername, *credentials.ProtonPassword),
|
||||
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner),
|
||||
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),
|
||||
|
||||
@@ -25,14 +25,13 @@ func newHandler(ctx context.Context, logger Logger, logging bool,
|
||||
handler := &handler{}
|
||||
|
||||
vpn := newVPNHandler(ctx, vpnLooper, storage, ipv6Supported, logger)
|
||||
openvpn := newOpenvpnHandler(ctx, vpnLooper, logger)
|
||||
openvpn := newOpenvpnHandler(ctx, vpnLooper, pfGetter, logger)
|
||||
dns := newDNSHandler(ctx, dnsLooper, logger)
|
||||
updater := newUpdaterHandler(ctx, updaterLooper, logger)
|
||||
publicip := newPublicIPHandler(publicIPLooper, logger)
|
||||
portForward := newPortForwardHandler(ctx, pfGetter, logger)
|
||||
|
||||
handler.v0 = newHandlerV0(ctx, logger, vpnLooper, dnsLooper, updaterLooper)
|
||||
handler.v1 = newHandlerV1(logger, buildInfo, vpn, openvpn, dns, updater, publicip, portForward)
|
||||
handler.v1 = newHandlerV1(logger, buildInfo, vpn, openvpn, dns, updater, publicip)
|
||||
|
||||
authMiddleware, err := auth.New(authSettings, logger)
|
||||
if err != nil {
|
||||
|
||||
@@ -52,7 +52,7 @@ func (h *handlerV0) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Warn(err.Error())
|
||||
}
|
||||
case "/openvpn/portforwarded":
|
||||
http.Redirect(w, r, "/v1/portforward", http.StatusPermanentRedirect)
|
||||
http.Redirect(w, r, "/v1/openvpn/portforwarded", http.StatusPermanentRedirect)
|
||||
case "/openvpn/settings":
|
||||
http.Redirect(w, r, "/v1/openvpn/settings", http.StatusPermanentRedirect)
|
||||
case "/updater/restart":
|
||||
|
||||
@@ -10,29 +10,27 @@ import (
|
||||
)
|
||||
|
||||
func newHandlerV1(w warner, buildInfo models.BuildInformation,
|
||||
vpn, openvpn, dns, updater, publicip, portForward http.Handler,
|
||||
vpn, openvpn, dns, updater, publicip http.Handler,
|
||||
) http.Handler {
|
||||
return &handlerV1{
|
||||
warner: w,
|
||||
buildInfo: buildInfo,
|
||||
vpn: vpn,
|
||||
openvpn: openvpn,
|
||||
dns: dns,
|
||||
updater: updater,
|
||||
publicip: publicip,
|
||||
portForward: portForward,
|
||||
warner: w,
|
||||
buildInfo: buildInfo,
|
||||
vpn: vpn,
|
||||
openvpn: openvpn,
|
||||
dns: dns,
|
||||
updater: updater,
|
||||
publicip: publicip,
|
||||
}
|
||||
}
|
||||
|
||||
type handlerV1 struct {
|
||||
warner warner
|
||||
buildInfo models.BuildInformation
|
||||
vpn http.Handler
|
||||
openvpn http.Handler
|
||||
dns http.Handler
|
||||
updater http.Handler
|
||||
publicip http.Handler
|
||||
portForward http.Handler
|
||||
warner warner
|
||||
buildInfo models.BuildInformation
|
||||
vpn http.Handler
|
||||
openvpn http.Handler
|
||||
dns http.Handler
|
||||
updater http.Handler
|
||||
publicip http.Handler
|
||||
}
|
||||
|
||||
func (h *handlerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -49,8 +47,6 @@ func (h *handlerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.updater.ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.RequestURI, "/publicip"):
|
||||
h.publicip.ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.RequestURI, "/portforward"):
|
||||
h.portForward.ServeHTTP(w, r)
|
||||
default:
|
||||
errString := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI)
|
||||
http.Error(w, errString, http.StatusBadRequest)
|
||||
|
||||
@@ -15,7 +15,6 @@ type infoWarner interface {
|
||||
|
||||
type infoer interface {
|
||||
Info(s string)
|
||||
Infof(format string, args ...any)
|
||||
}
|
||||
|
||||
type warner interface {
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
func Read(filepath string) (settings Settings, err error) {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return Settings{}, nil
|
||||
}
|
||||
return settings, fmt.Errorf("opening file: %w", err)
|
||||
}
|
||||
decoder := toml.NewDecoder(file)
|
||||
|
||||
@@ -20,7 +20,6 @@ func New(settings Settings, debugLogger DebugLogger) (
|
||||
routeToRoles: routeToRoles,
|
||||
unprotectedRoutes: map[string]struct{}{
|
||||
http.MethodGet + " /openvpn/actions/restart": {},
|
||||
http.MethodGet + " /openvpn/portforwarded": {},
|
||||
http.MethodGet + " /unbound/actions/restart": {},
|
||||
http.MethodGet + " /updater/restart": {},
|
||||
http.MethodGet + " /v1/version": {},
|
||||
@@ -37,7 +36,6 @@ func New(settings Settings, debugLogger DebugLogger) (
|
||||
http.MethodGet + " /v1/updater/status": {},
|
||||
http.MethodPut + " /v1/updater/status": {},
|
||||
http.MethodGet + " /v1/publicip/ip": {},
|
||||
http.MethodGet + " /v1/portforward": {},
|
||||
},
|
||||
logger: debugLogger,
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/validate"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
@@ -19,50 +15,6 @@ type Settings struct {
|
||||
Roles []Role
|
||||
}
|
||||
|
||||
// SetDefaultRole sets a default role to apply to all routes without a
|
||||
// previously user-defined role assigned to. Note the role argument
|
||||
// routes are ignored. This should be called BEFORE calling [Settings.SetDefaults].
|
||||
func (s *Settings) SetDefaultRole(jsonRole string) error {
|
||||
var role Role
|
||||
decoder := json.NewDecoder(bytes.NewBufferString(jsonRole))
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(&role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding default role: %w", err)
|
||||
}
|
||||
if role.Auth == "" {
|
||||
return nil // no default role to set
|
||||
}
|
||||
err = role.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating default role: %w", err)
|
||||
}
|
||||
|
||||
authenticatedRoutes := make(map[string]struct{}, len(validRoutes))
|
||||
for _, role := range s.Roles {
|
||||
for _, route := range role.Routes {
|
||||
authenticatedRoutes[route] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(authenticatedRoutes) == len(validRoutes) {
|
||||
return nil
|
||||
}
|
||||
|
||||
unauthenticatedRoutes := make([]string, 0, len(validRoutes))
|
||||
for route := range validRoutes {
|
||||
_, authenticated := authenticatedRoutes[route]
|
||||
if !authenticated {
|
||||
unauthenticatedRoutes = append(unauthenticatedRoutes, route)
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(unauthenticatedRoutes)
|
||||
role.Routes = unauthenticatedRoutes
|
||||
s.Roles = append(s.Roles, role)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Settings) SetDefaults() {
|
||||
s.Roles = gosettings.DefaultSlice(s.Roles, []Role{{ // TODO v3.41.0 leave empty
|
||||
Name: "public",
|
||||
@@ -70,7 +22,6 @@ func (s *Settings) SetDefaults() {
|
||||
Routes: []string{
|
||||
http.MethodGet + " /openvpn/actions/restart",
|
||||
http.MethodGet + " /unbound/actions/restart",
|
||||
http.MethodGet + " /openvpn/portforwarded",
|
||||
http.MethodGet + " /updater/restart",
|
||||
http.MethodGet + " /v1/version",
|
||||
http.MethodGet + " /v1/vpn/status",
|
||||
@@ -83,14 +34,13 @@ func (s *Settings) SetDefaults() {
|
||||
http.MethodGet + " /v1/updater/status",
|
||||
http.MethodPut + " /v1/updater/status",
|
||||
http.MethodGet + " /v1/publicip/ip",
|
||||
http.MethodGet + " /v1/portforward",
|
||||
},
|
||||
}})
|
||||
}
|
||||
|
||||
func (s Settings) Validate() (err error) {
|
||||
for i, role := range s.Roles {
|
||||
err = role.Validate()
|
||||
err = role.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("role %s (%d of %d): %w",
|
||||
role.Name, i+1, len(s.Roles), err)
|
||||
@@ -111,18 +61,18 @@ const (
|
||||
type Role struct {
|
||||
// Name is the role name and is only used for documentation
|
||||
// and in the authentication middleware debug logs.
|
||||
Name string `json:"name"`
|
||||
// Auth is the authentication method to use, which can be 'none', 'basic' or 'apikey'.
|
||||
Auth string `json:"auth"`
|
||||
Name string
|
||||
// Auth is the authentication method to use, which can be 'none' or 'apikey'.
|
||||
Auth string
|
||||
// APIKey is the API key to use when using the 'apikey' authentication.
|
||||
APIKey string `json:"apikey"`
|
||||
APIKey string
|
||||
// Username for HTTP Basic authentication method.
|
||||
Username string `json:"username"`
|
||||
Username string
|
||||
// Password for HTTP Basic authentication method.
|
||||
Password string `json:"password"`
|
||||
Password string
|
||||
// Routes is a list of routes that the role can access in the format
|
||||
// "HTTP_METHOD PATH", for example "GET /v1/vpn/status"
|
||||
Routes []string `json:"-"`
|
||||
Routes []string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -133,7 +83,7 @@ var (
|
||||
ErrRouteNotSupported = errors.New("route not supported by the control server")
|
||||
)
|
||||
|
||||
func (r Role) Validate() (err error) {
|
||||
func (r Role) validate() (err error) {
|
||||
err = validate.IsOneOf(r.Auth, AuthNone, AuthAPIKey, AuthBasic)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrMethodNotSupported, r.Auth)
|
||||
@@ -162,8 +112,6 @@ func (r Role) Validate() (err error) {
|
||||
// WARNING: do not mutate programmatically.
|
||||
var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
|
||||
http.MethodGet + " /openvpn/actions/restart": {},
|
||||
http.MethodGet + " /openvpn/portforwarded": {},
|
||||
http.MethodGet + " /openvpn/settings": {},
|
||||
http.MethodGet + " /unbound/actions/restart": {},
|
||||
http.MethodGet + " /updater/restart": {},
|
||||
http.MethodGet + " /v1/version": {},
|
||||
@@ -180,22 +128,4 @@ var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
|
||||
http.MethodGet + " /v1/updater/status": {},
|
||||
http.MethodPut + " /v1/updater/status": {},
|
||||
http.MethodGet + " /v1/publicip/ip": {},
|
||||
http.MethodGet + " /v1/portforward": {},
|
||||
}
|
||||
|
||||
func (r Role) ToLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Role " + r.Name)
|
||||
node.Appendf("Authentication method: %s", r.Auth)
|
||||
switch r.Auth {
|
||||
case AuthNone:
|
||||
case AuthBasic:
|
||||
node.Appendf("Username: %s", r.Username)
|
||||
node.Appendf("Password: %s", gosettings.ObfuscateKey(r.Password))
|
||||
case AuthAPIKey:
|
||||
node.Appendf("API key: %s", gosettings.ObfuscateKey(r.APIKey))
|
||||
default:
|
||||
panic("missing code for authentication method: " + r.Auth)
|
||||
}
|
||||
node.Appendf("Number of routes covered: %d", len(r.Routes))
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -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.URL.String() +
|
||||
r.Method + " " + r.RequestURI +
|
||||
" wrote " + strconv.Itoa(statefulWriter.length) + "B to " +
|
||||
r.RemoteAddr + " in " + duration.String())
|
||||
}
|
||||
|
||||
@@ -10,10 +10,13 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
)
|
||||
|
||||
func newOpenvpnHandler(ctx context.Context, looper VPNLooper, w warner) http.Handler {
|
||||
func newOpenvpnHandler(ctx context.Context, looper VPNLooper,
|
||||
pfGetter PortForwardedGetter, w warner,
|
||||
) http.Handler {
|
||||
return &openvpnHandler{
|
||||
ctx: ctx,
|
||||
looper: looper,
|
||||
pf: pfGetter,
|
||||
warner: w,
|
||||
}
|
||||
}
|
||||
@@ -21,6 +24,7 @@ func newOpenvpnHandler(ctx context.Context, looper VPNLooper, w warner) http.Han
|
||||
type openvpnHandler struct {
|
||||
ctx context.Context //nolint:containedctx
|
||||
looper VPNLooper
|
||||
pf PortForwardedGetter
|
||||
warner warner
|
||||
}
|
||||
|
||||
@@ -43,10 +47,10 @@ func (h *openvpnHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
errMethodNotSupported(w, r.Method)
|
||||
}
|
||||
case "/portforwarded": // TODO v4 remove
|
||||
case "/portforwarded":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
http.Redirect(w, r, "/v1/portforward", http.StatusMovedPermanently)
|
||||
h.getPortForwarded(w)
|
||||
default:
|
||||
errMethodNotSupported(w, r.Method)
|
||||
}
|
||||
@@ -118,3 +122,23 @@ func (h *openvpnHandler) getSettings(w http.ResponseWriter) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *openvpnHandler) getPortForwarded(w http.ResponseWriter) {
|
||||
ports := h.pf.GetPortsForwarded()
|
||||
encoder := json.NewEncoder(w)
|
||||
var data any
|
||||
switch len(ports) {
|
||||
case 0:
|
||||
data = portWrapper{Port: 0} // TODO v4 change to portsWrapper
|
||||
case 1:
|
||||
data = portWrapper{Port: ports[0]} // TODO v4 change to portsWrapper
|
||||
default:
|
||||
data = portsWrapper{Ports: ports}
|
||||
}
|
||||
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
h.warner.Warn(err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func newPortForwardHandler(ctx context.Context,
|
||||
portForward PortForwardedGetter, warner warner,
|
||||
) http.Handler {
|
||||
return &portForwardHandler{
|
||||
ctx: ctx,
|
||||
portForward: portForward,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
|
||||
type portForwardHandler struct {
|
||||
ctx context.Context //nolint:containedctx
|
||||
portForward PortForwardedGetter
|
||||
warner warner
|
||||
}
|
||||
|
||||
func (h *portForwardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.getPortForwarded(w)
|
||||
default:
|
||||
errMethodNotSupported(w, r.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *portForwardHandler) getPortForwarded(w http.ResponseWriter) {
|
||||
ports := h.portForward.GetPortsForwarded()
|
||||
encoder := json.NewEncoder(w)
|
||||
var data any
|
||||
switch len(ports) {
|
||||
case 0:
|
||||
data = portWrapper{Port: 0} // TODO v4 change to portsWrapper
|
||||
case 1:
|
||||
data = portWrapper{Port: ports[0]} // TODO v4 change to portsWrapper
|
||||
default:
|
||||
data = portsWrapper{Ports: ports}
|
||||
}
|
||||
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
h.warner.Warn(err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,31 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/httpserver"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
||||
)
|
||||
|
||||
func New(ctx context.Context, settings settings.ControlServer, logger Logger,
|
||||
buildInfo models.BuildInformation, openvpnLooper VPNLooper,
|
||||
func New(ctx context.Context, address string, logEnabled bool, logger Logger,
|
||||
authConfigPath string, buildInfo models.BuildInformation, openvpnLooper VPNLooper,
|
||||
pfGetter PortForwardedGetter, dnsLooper DNSLoop,
|
||||
updaterLooper UpdaterLooper, publicIPLooper PublicIPLoop, storage Storage,
|
||||
ipv6Supported bool) (
|
||||
server *httpserver.Server, err error,
|
||||
) {
|
||||
authSettings, err := setupAuthMiddleware(settings.AuthFilePath, settings.AuthDefaultRole, logger)
|
||||
authSettings, err := auth.Read(authConfigPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("building authentication middleware settings: %w", err)
|
||||
return nil, fmt.Errorf("reading auth settings: %w", err)
|
||||
}
|
||||
authSettings.SetDefaults()
|
||||
err = authSettings.Validate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating auth settings: %w", err)
|
||||
}
|
||||
|
||||
handler, err := newHandler(ctx, logger, *settings.Log, authSettings, buildInfo,
|
||||
handler, err := newHandler(ctx, logger, logEnabled, authSettings, buildInfo,
|
||||
openvpnLooper, pfGetter, dnsLooper, updaterLooper, publicIPLooper,
|
||||
storage, ipv6Supported)
|
||||
if err != nil {
|
||||
@@ -32,7 +34,7 @@ func New(ctx context.Context, settings settings.ControlServer, logger Logger,
|
||||
}
|
||||
|
||||
httpServerSettings := httpserver.Settings{
|
||||
Address: *settings.Address,
|
||||
Address: address,
|
||||
Handler: handler,
|
||||
Logger: logger,
|
||||
}
|
||||
@@ -44,26 +46,3 @@ func New(ctx context.Context, settings settings.ControlServer, logger Logger,
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func setupAuthMiddleware(authPath, jsonDefaultRole string, logger Logger) (
|
||||
authSettings auth.Settings, err error,
|
||||
) {
|
||||
authSettings, err = auth.Read(authPath)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist): // no auth file present
|
||||
case err != nil:
|
||||
return auth.Settings{}, fmt.Errorf("reading auth settings: %w", err)
|
||||
default:
|
||||
logger.Infof("read %d roles from authentication file", len(authSettings.Roles))
|
||||
}
|
||||
err = authSettings.SetDefaultRole(jsonDefaultRole)
|
||||
if err != nil {
|
||||
return auth.Settings{}, fmt.Errorf("setting default role: %w", err)
|
||||
}
|
||||
authSettings.SetDefaults()
|
||||
err = authSettings.Validate()
|
||||
if err != nil {
|
||||
return auth.Settings{}, fmt.Errorf("validating auth settings: %w", err)
|
||||
}
|
||||
return authSettings, nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package storage
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Logger
|
||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Infoer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Logger)
|
||||
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Infoer)
|
||||
|
||||
// Package storage is a generated GoMock package.
|
||||
package storage
|
||||
@@ -10,49 +10,37 @@ import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
// MockInfoer is a mock of Infoer interface.
|
||||
type MockInfoer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
recorder *MockInfoerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
// MockInfoerMockRecorder is the mock recorder for MockInfoer.
|
||||
type MockInfoerMockRecorder struct {
|
||||
mock *MockInfoer
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
// NewMockInfoer creates a new mock instance.
|
||||
func NewMockInfoer(ctrl *gomock.Controller) *MockInfoer {
|
||||
mock := &MockInfoer{ctrl: ctrl}
|
||||
mock.recorder = &MockInfoerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
func (m *MockInfoer) EXPECT() *MockInfoerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Info mocks base method.
|
||||
func (m *MockLogger) Info(arg0 string) {
|
||||
func (m *MockInfoer) Info(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Info", arg0)
|
||||
}
|
||||
|
||||
// Info indicates an expected call of Info.
|
||||
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||
func (mr *MockInfoerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
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)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockInfoer)(nil).Info), arg0)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger := NewMockInfoer(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,35 +13,30 @@ type Storage struct {
|
||||
// the embedded JSON file on every call to the
|
||||
// SyncServers method.
|
||||
hardcodedServers models.AllServers
|
||||
logger Logger
|
||||
logger Infoer
|
||||
filepath string
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
type Infoer 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 the reading and writing of
|
||||
// servers.
|
||||
func New(logger Logger, filepath string) (storage *Storage, err error) {
|
||||
// Passing an empty filepath disables writing servers to a file.
|
||||
func New(logger Infoer, 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()
|
||||
|
||||
storage = &Storage{
|
||||
hardcodedServers: hardcodedServers,
|
||||
mergedServers: hardcodedServers,
|
||||
logger: logger,
|
||||
filepath: filepath,
|
||||
}
|
||||
|
||||
if filepath != "" {
|
||||
if err := storage.syncServers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := storage.syncServers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
|
||||
@@ -46,13 +46,13 @@ func (s *Storage) syncServers() (err error) {
|
||||
}
|
||||
|
||||
// Eventually write file
|
||||
if reflect.DeepEqual(serversOnFile, s.mergedServers) {
|
||||
if s.filepath == "" || reflect.DeepEqual(serversOnFile, s.mergedServers) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.flushToFile(s.filepath)
|
||||
if err != nil {
|
||||
s.logger.Warn("failed writing servers to file: " + err.Error())
|
||||
return fmt.Errorf("writing servers to file: %w", err)
|
||||
}
|
||||
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 %s servers: %w", providerName, err)
|
||||
return fmt.Errorf("getting servers: %w", err)
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
|
||||
@@ -2,11 +2,9 @@ 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"
|
||||
@@ -50,22 +48,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)
|
||||
switch {
|
||||
case err == nil:
|
||||
if err == nil {
|
||||
continue
|
||||
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())
|
||||
}
|
||||
|
||||
// return the only error for the single provider.
|
||||
if len(providers) == 1 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -56,14 +56,14 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
password: settings.Provider.PortForwarding.Password,
|
||||
}
|
||||
|
||||
vpnCtx, vpnCancel := context.WithCancel(context.Background())
|
||||
openvpnCtx, openvpnCancel := context.WithCancel(context.Background())
|
||||
waitError := make(chan error)
|
||||
tunnelReady := make(chan struct{})
|
||||
|
||||
go vpnRunner.Run(vpnCtx, waitError, tunnelReady)
|
||||
go vpnRunner.Run(openvpnCtx, waitError, tunnelReady)
|
||||
|
||||
if err := l.waitForError(ctx, waitError); err != nil {
|
||||
vpnCancel()
|
||||
openvpnCancel()
|
||||
l.crashed(ctx, err)
|
||||
continue
|
||||
}
|
||||
@@ -75,10 +75,10 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
for stayHere {
|
||||
select {
|
||||
case <-tunnelReady:
|
||||
go l.onTunnelUp(vpnCtx, ctx, tunnelUpData)
|
||||
go l.onTunnelUp(openvpnCtx, ctx, tunnelUpData)
|
||||
case <-ctx.Done():
|
||||
l.cleanup()
|
||||
vpnCancel()
|
||||
openvpnCancel()
|
||||
<-waitError
|
||||
close(waitError)
|
||||
return
|
||||
@@ -86,7 +86,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
l.userTrigger = true
|
||||
l.logger.Info("stopping")
|
||||
l.cleanup()
|
||||
vpnCancel()
|
||||
openvpnCancel()
|
||||
<-waitError
|
||||
// do not close waitError or the waitError
|
||||
// select case will trigger
|
||||
@@ -99,7 +99,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
l.statusManager.Lock() // prevent SetStatus from running in parallel
|
||||
|
||||
l.cleanup()
|
||||
vpnCancel()
|
||||
openvpnCancel()
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
l.logAndWait(ctx, err)
|
||||
stayHere = false
|
||||
@@ -107,6 +107,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
l.statusManager.Unlock()
|
||||
}
|
||||
}
|
||||
vpnCancel()
|
||||
openvpnCancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/check"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/version"
|
||||
)
|
||||
@@ -45,7 +46,14 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
|
||||
if *l.dnsLooper.GetSettings().DoT.Enabled {
|
||||
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
|
||||
} else {
|
||||
err := check.WaitForDNS(ctx, check.Settings{})
|
||||
if err != nil {
|
||||
l.logger.Error("waiting for DNS to be ready: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err = l.publicip.RunOnce(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Maintenance
|
||||
|
||||
- Rename `UNBLOCK` to `DNS_HOSTNAMES_UNBLOCKED`
|
||||
- Change `Run` methods to `Start`+`Stop`, returning channels rather than injecting them
|
||||
- Go 1.18
|
||||
- gofumpt
|
||||
|
||||
Reference in New Issue
Block a user