Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daff23bfb3 | ||
|
|
aa6d26e062 | ||
|
|
b2859d5a06 | ||
|
|
ad8b0657cb | ||
|
|
c930a4e1be | ||
|
|
22834e9477 | ||
|
|
62c2679da2 | ||
|
|
5e9ae9fa1f | ||
|
|
0f19bcfebd | ||
|
|
83fc91d3c6 | ||
|
|
4adeec8223 | ||
|
|
64bfbaa45d | ||
|
|
e890c50da6 | ||
|
|
ddd9f4d021 | ||
|
|
7e58b4baee | ||
|
|
a21fbb9a4f | ||
|
|
3b7d27c919 | ||
|
|
68ddbfc0fe | ||
|
|
a2047cb800 | ||
|
|
fdd499146c | ||
|
|
37900341cf | ||
|
|
36bb368cad | ||
|
|
f9bdb219d0 | ||
|
|
0374c14e42 | ||
|
|
a035a151bd | ||
|
|
e69966381d | ||
|
|
94dfb2b1f2 |
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -59,40 +59,13 @@ jobs:
|
|||||||
- name: Run tests in test container
|
- name: Run tests in test container
|
||||||
run: |
|
run: |
|
||||||
touch coverage.txt
|
touch coverage.txt
|
||||||
docker run --rm \
|
docker run --rm --device /dev/net/tun \
|
||||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||||
test-container
|
test-container
|
||||||
|
|
||||||
- name: Build final image
|
- name: Build final image
|
||||||
run: docker build -t final-image .
|
run: docker build -t final-image .
|
||||||
|
|
||||||
- name: Run Wireguard
|
|
||||||
if: |
|
|
||||||
github.repository == 'qdm12/gluetun' &&
|
|
||||||
(
|
|
||||||
github.event_name == 'push' ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
|
|
||||||
)
|
|
||||||
run: |
|
|
||||||
docker run -it --rm --cap-add=NET_ADMIN -e VPNSP=mullvad -e VPN_TYPE=wireguard \
|
|
||||||
-e WIREGUARD_PRIVATE_KEY=${{ secrets.WIREGUARD_PRIVATE_KEY }} \
|
|
||||||
-e WIREGUARD_ADDRESS=${{ secrets.WIREGUARD_ADDRESS }} \
|
|
||||||
final-image \
|
|
||||||
exit-once-connected
|
|
||||||
|
|
||||||
- name: Run OpenVPN
|
|
||||||
if: |
|
|
||||||
github.repository == 'qdm12/gluetun' &&
|
|
||||||
(
|
|
||||||
github.event_name == 'push' ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
|
|
||||||
)
|
|
||||||
run: |
|
|
||||||
docker run -it --rm --cap-add=NET_ADMIN -e VPNSP=mullvad -e VPN_TYPE=openvpn \
|
|
||||||
-e OPENVPN_PASSWORD=${{ secrets.OPENVPN_PASSWORD }} \
|
|
||||||
final-image \
|
|
||||||
exit-once-connected
|
|
||||||
|
|
||||||
codeql:
|
codeql:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
2
.github/workflows/markdown.yml
vendored
2
.github/workflows/markdown.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
- uses: DavidAnson/markdownlint-cli2-action@v18
|
||||||
with:
|
with:
|
||||||
globs: "**.md"
|
globs: "**.md"
|
||||||
config: .markdownlint.json
|
config: .markdownlint.json
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ issues:
|
|||||||
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
||||||
linters:
|
linters:
|
||||||
- ireturn
|
- ireturn
|
||||||
|
- source: "^\\/\\/ https\\:\\/\\/.+$"
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||||
VPN_PORT_FORWARDING_USERNAME= \
|
VPN_PORT_FORWARDING_USERNAME= \
|
||||||
VPN_PORT_FORWARDING_PASSWORD= \
|
VPN_PORT_FORWARDING_PASSWORD= \
|
||||||
|
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||||
|
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||||
# # Cyberghost only:
|
# # Cyberghost only:
|
||||||
OPENVPN_CERT= \
|
OPENVPN_CERT= \
|
||||||
OPENVPN_KEY= \
|
OPENVPN_KEY= \
|
||||||
@@ -202,6 +204,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
UPDATER_PERIOD=0 \
|
UPDATER_PERIOD=0 \
|
||||||
UPDATER_MIN_RATIO=0.8 \
|
UPDATER_MIN_RATIO=0.8 \
|
||||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||||
|
UPDATER_PROTONVPN_EMAIL= \
|
||||||
|
UPDATER_PROTONVPN_PASSWORD= \
|
||||||
# Public IP
|
# Public IP
|
||||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||||
PUBLICIP_ENABLED=on \
|
PUBLICIP_ENABLED=on \
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
tun Tun, netLinker netLinker, cmder RunStarter,
|
tun Tun, netLinker netLinker, cmder RunStarter,
|
||||||
cli clier,
|
cli clier,
|
||||||
) error {
|
) error {
|
||||||
var exitOnceConnected bool
|
|
||||||
if len(args) > 1 { // cli operation
|
if len(args) > 1 { // cli operation
|
||||||
switch args[1] {
|
switch args[1] {
|
||||||
case "healthcheck":
|
case "healthcheck":
|
||||||
@@ -160,8 +159,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return cli.FormatServers(args[2:])
|
return cli.FormatServers(args[2:])
|
||||||
case "genkey":
|
case "genkey":
|
||||||
return cli.GenKey(args[2:])
|
return cli.GenKey(args[2:])
|
||||||
case "exit-once-connected":
|
|
||||||
exitOnceConnected = true
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
||||||
}
|
}
|
||||||
@@ -383,7 +380,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
|
|
||||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||||
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
|
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
|
||||||
portForwardRunError, err := portForwardLooper.Start(ctx)
|
portForwardRunError, err := portForwardLooper.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||||
@@ -423,7 +420,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
|
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||||
|
openvpnFileExtractor, allSettings.Updater)
|
||||||
|
|
||||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||||
@@ -480,8 +478,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
controlGroupHandler.Add(httpServerHandler)
|
controlGroupHandler.Add(httpServerHandler)
|
||||||
|
|
||||||
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
||||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper,
|
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
|
||||||
exitOnceConnected)
|
|
||||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||||
|
|||||||
22
go.mod
22
go.mod
@@ -3,35 +3,40 @@ module github.com/qdm12/gluetun
|
|||||||
go 1.23
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/breml/rootcerts v0.2.18
|
github.com/ProtonMail/go-srp v0.0.7
|
||||||
|
github.com/breml/rootcerts v0.2.19
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/klauspost/compress v1.17.11
|
github.com/klauspost/compress v1.17.11
|
||||||
github.com/klauspost/pgzip v1.2.6
|
github.com/klauspost/pgzip v1.2.6
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3
|
github.com/pelletier/go-toml/v2 v2.2.3
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc8
|
github.com/qdm12/dns/v2 v2.0.0-rc8
|
||||||
github.com/qdm12/gosettings v0.4.3
|
github.com/qdm12/gosettings v0.4.4
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
github.com/qdm12/gosplash v0.2.0
|
github.com/qdm12/gosplash v0.2.0
|
||||||
github.com/qdm12/gotree v0.3.0
|
github.com/qdm12/gotree v0.3.0
|
||||||
github.com/qdm12/log v0.1.0
|
github.com/qdm12/log v0.1.0
|
||||||
github.com/qdm12/ss-server v0.6.0
|
github.com/qdm12/ss-server v0.6.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/ulikunitz/xz v0.5.11
|
github.com/ulikunitz/xz v0.5.11
|
||||||
github.com/vishvananda/netlink v1.2.1
|
github.com/vishvananda/netlink v1.2.1
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
golang.org/x/net v0.30.0
|
golang.org/x/net v0.31.0
|
||||||
golang.org/x/sys v0.27.0
|
golang.org/x/sys v0.30.0
|
||||||
golang.org/x/text v0.19.0
|
golang.org/x/text v0.22.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.0 // indirect
|
||||||
|
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
@@ -42,6 +47,7 @@ require (
|
|||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/miekg/dns v1.1.62 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
@@ -50,9 +56,9 @@ require (
|
|||||||
github.com/qdm12/goservices v0.1.0 // indirect
|
github.com/qdm12/goservices v0.1.0 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/tools v0.26.0 // indirect
|
golang.org/x/tools v0.26.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/protobuf v1.35.1 // indirect
|
google.golang.org/protobuf v1.35.1 // indirect
|
||||||
|
|||||||
72
go.sum
72
go.sum
@@ -1,9 +1,23 @@
|
|||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||||
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
|
github.com/breml/rootcerts v0.2.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U=
|
||||||
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
github.com/breml/rootcerts v0.2.19/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||||
|
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||||
|
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||||
|
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
@@ -43,6 +57,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
@@ -57,8 +73,8 @@ github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ1
|
|||||||
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
|
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 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
|
||||||
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
||||||
github.com/qdm12/gosettings v0.4.3 h1:oGAjiKVtml9oHVlPQo6H3yk6TmtWpVYicNeGFcM7AP8=
|
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||||
github.com/qdm12/gosettings v0.4.3/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||||
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||||
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||||
@@ -73,8 +89,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/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 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
|
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
|
||||||
@@ -84,46 +100,70 @@ github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZla
|
|||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||||
|
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ func newNoopLogger() *noopLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *noopLogger) Info(string) {}
|
func (l *noopLogger) Info(string) {}
|
||||||
|
func (l *noopLogger) Warn(string) {}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
allSettings.SetDefaults()
|
||||||
|
|
||||||
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,7 +76,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
|||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
|
|
||||||
providers := provider.NewProviders(storage, time.Now, warner, client,
|
providers := provider.NewProviders(storage, time.Now, warner, client,
|
||||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
||||||
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
||||||
connection, err := providerConf.GetConnection(
|
connection, err := providerConf.GetConnection(
|
||||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
||||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||||
|
ErrUsernameMissing = errors.New("username is required for this provider")
|
||||||
|
ErrPasswordMissing = errors.New("password is required for this provider")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdaterLogger interface {
|
type UpdaterLogger interface {
|
||||||
@@ -35,7 +38,7 @@ type UpdaterLogger interface {
|
|||||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||||
options := settings.Updater{}
|
options := settings.Updater{}
|
||||||
var endUserMode, maintainerMode, updateAll bool
|
var endUserMode, maintainerMode, updateAll bool
|
||||||
var csvProviders, ipToken string
|
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
||||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||||
@@ -47,6 +50,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
||||||
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
||||||
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
||||||
|
flagSet.StringVar(&protonUsername, "proton-username", "",
|
||||||
|
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
|
||||||
|
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
|
||||||
|
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -64,6 +71,16 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
options.Providers = strings.Split(csvProviders, ",")
|
options.Providers = strings.Split(csvProviders, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||||
|
if protonEmail == "" && protonUsername != "" {
|
||||||
|
protonEmail = protonUsername + "@protonmail.com"
|
||||||
|
logger.Warn("use -proton-email instead of -proton-username in the future. " +
|
||||||
|
"This assumes the email is " + protonEmail + " and may not work.")
|
||||||
|
}
|
||||||
|
options.ProtonEmail = &protonEmail
|
||||||
|
options.ProtonPassword = &protonPassword
|
||||||
|
}
|
||||||
|
|
||||||
options.SetDefaults(options.Providers[0])
|
options.SetDefaults(options.Providers[0])
|
||||||
|
|
||||||
err := options.Validate()
|
err := options.Validate()
|
||||||
@@ -94,7 +111,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
|
|
||||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||||
|
|
||||||
updater := updater.New(httpClient, storage, providers, logger)
|
updater := updater.New(httpClient, storage, providers, logger)
|
||||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||||
|
|||||||
150
internal/command/split.go
Normal file
150
internal/command/split.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCommandEmpty = errors.New("command is empty")
|
||||||
|
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
||||||
|
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
||||||
|
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Split splits a command string into a slice of arguments.
|
||||||
|
// This is especially important for commands such as:
|
||||||
|
// /bin/sh -c "echo hello"
|
||||||
|
// which should be split into: ["/bin/sh", "-c", "echo hello"]
|
||||||
|
// It supports backslash-escapes, single-quotes and double-quotes.
|
||||||
|
// It does not support:
|
||||||
|
// - the $" quoting style.
|
||||||
|
// - expansion (brace, shell or pathname).
|
||||||
|
func Split(command string) (words []string, err error) {
|
||||||
|
if command == "" {
|
||||||
|
return nil, fmt.Errorf("%w", ErrCommandEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufferSize = 1024
|
||||||
|
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
||||||
|
|
||||||
|
startIndex := 0
|
||||||
|
|
||||||
|
for startIndex < len(command) {
|
||||||
|
// skip any split characters at the start
|
||||||
|
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
|
||||||
|
switch {
|
||||||
|
case strings.ContainsRune(" \n\t", character):
|
||||||
|
startIndex += runeSize
|
||||||
|
case character == '\\':
|
||||||
|
// Look ahead to eventually skip an escaped newline
|
||||||
|
if command[startIndex+runeSize:] == "" {
|
||||||
|
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
|
||||||
|
}
|
||||||
|
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
|
||||||
|
if character == '\n' {
|
||||||
|
startIndex += runeSize + runeSize // backslash and newline
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
var word string
|
||||||
|
buffer.Reset()
|
||||||
|
word, startIndex, err = splitWord(command, startIndex, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
|
||||||
|
}
|
||||||
|
words = append(words, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return words, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: buffer must be cleared before calling this function.
|
||||||
|
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
|
||||||
|
word string, newStartIndex int, err error,
|
||||||
|
) {
|
||||||
|
cursor := startIndex
|
||||||
|
for cursor < len(input) {
|
||||||
|
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
|
||||||
|
cursor += runeLength
|
||||||
|
if character == '"' ||
|
||||||
|
character == '\'' ||
|
||||||
|
character == '\\' ||
|
||||||
|
character == ' ' ||
|
||||||
|
character == '\n' ||
|
||||||
|
character == '\t' {
|
||||||
|
buffer.WriteString(input[startIndex : cursor-runeLength])
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.ContainsRune(" \n\t", character): // spacing character
|
||||||
|
return buffer.String(), cursor, nil
|
||||||
|
case character == '"':
|
||||||
|
return handleDoubleQuoted(input, cursor, buffer)
|
||||||
|
case character == '\'':
|
||||||
|
return handleSingleQuoted(input, cursor, buffer)
|
||||||
|
case character == '\\':
|
||||||
|
return handleEscaped(input, cursor, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(input[startIndex:])
|
||||||
|
return buffer.String(), len(input), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||||
|
word string, newStartIndex int, err error,
|
||||||
|
) {
|
||||||
|
cursor := startIndex
|
||||||
|
for cursor < len(input) {
|
||||||
|
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||||
|
cursor += nextRuneLength
|
||||||
|
switch nextCharacter {
|
||||||
|
case '"': // end of the double quoted string
|
||||||
|
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
|
||||||
|
return splitWord(input, cursor, buffer)
|
||||||
|
case '\\': // escaped character
|
||||||
|
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||||
|
cursor += escapedRuneLength
|
||||||
|
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
|
||||||
|
if escapedCharacter != '\n' {
|
||||||
|
// skip backslash entirely for the newline character
|
||||||
|
buffer.WriteRune(escapedCharacter)
|
||||||
|
}
|
||||||
|
startIndex = cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||||
|
word string, newStartIndex int, err error,
|
||||||
|
) {
|
||||||
|
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
|
||||||
|
if closingQuoteIndex == -1 {
|
||||||
|
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
|
||||||
|
}
|
||||||
|
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
|
||||||
|
const singleQuoteRuneLength = 1
|
||||||
|
startIndex += closingQuoteIndex + singleQuoteRuneLength
|
||||||
|
return splitWord(input, startIndex, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
|
||||||
|
word string, newStartIndex int, err error,
|
||||||
|
) {
|
||||||
|
if input[startIndex:] == "" {
|
||||||
|
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
|
||||||
|
}
|
||||||
|
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
|
||||||
|
if character != '\n' { // backslash-escaped newline is ignored
|
||||||
|
buffer.WriteString(input[startIndex : startIndex+runeLength])
|
||||||
|
}
|
||||||
|
startIndex += runeLength
|
||||||
|
return splitWord(input, startIndex, buffer)
|
||||||
|
}
|
||||||
110
internal/command/split_test.go
Normal file
110
internal/command/split_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Split(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
command string
|
||||||
|
words []string
|
||||||
|
errWrapped error
|
||||||
|
errMessage string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
command: "",
|
||||||
|
errWrapped: ErrCommandEmpty,
|
||||||
|
errMessage: "command is empty",
|
||||||
|
},
|
||||||
|
"concrete_sh_command": {
|
||||||
|
command: `/bin/sh -c "echo 123"`,
|
||||||
|
words: []string{"/bin/sh", "-c", "echo 123"},
|
||||||
|
},
|
||||||
|
"single_word": {
|
||||||
|
command: "word1",
|
||||||
|
words: []string{"word1"},
|
||||||
|
},
|
||||||
|
"two_words_single_space": {
|
||||||
|
command: "word1 word2",
|
||||||
|
words: []string{"word1", "word2"},
|
||||||
|
},
|
||||||
|
"two_words_multiple_space": {
|
||||||
|
command: "word1 word2",
|
||||||
|
words: []string{"word1", "word2"},
|
||||||
|
},
|
||||||
|
"two_words_no_expansion": {
|
||||||
|
command: "word1* word2?",
|
||||||
|
words: []string{"word1*", "word2?"},
|
||||||
|
},
|
||||||
|
"escaped_single quote": {
|
||||||
|
command: "ain\\'t good",
|
||||||
|
words: []string{"ain't", "good"},
|
||||||
|
},
|
||||||
|
"escaped_single_quote_all_single_quoted": {
|
||||||
|
command: "'ain'\\''t good'",
|
||||||
|
words: []string{"ain't good"},
|
||||||
|
},
|
||||||
|
"empty_single_quoted": {
|
||||||
|
command: "word1 '' word2",
|
||||||
|
words: []string{"word1", "", "word2"},
|
||||||
|
},
|
||||||
|
"escaped_newline": {
|
||||||
|
command: "word1\\\nword2",
|
||||||
|
words: []string{"word1word2"},
|
||||||
|
},
|
||||||
|
"quoted_newline": {
|
||||||
|
command: "text \"with\na\" quoted newline",
|
||||||
|
words: []string{"text", "with\na", "quoted", "newline"},
|
||||||
|
},
|
||||||
|
"quoted_escaped_newline": {
|
||||||
|
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
|
||||||
|
words: []string{"word1\\d\\\" word2word3 word4"},
|
||||||
|
},
|
||||||
|
"escaped_separated_newline": {
|
||||||
|
command: "word1 \\\n word2",
|
||||||
|
words: []string{"word1", "word2"},
|
||||||
|
},
|
||||||
|
"double_quotes_no_spacing": {
|
||||||
|
command: "word1\"word2\"word3",
|
||||||
|
words: []string{"word1word2word3"},
|
||||||
|
},
|
||||||
|
"unterminated_single_quote": {
|
||||||
|
command: "'abc'\\''def",
|
||||||
|
errWrapped: ErrSingleQuoteUnterminated,
|
||||||
|
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
||||||
|
},
|
||||||
|
"unterminated_double_quote": {
|
||||||
|
command: "\"abc'def",
|
||||||
|
errWrapped: ErrDoubleQuoteUnterminated,
|
||||||
|
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
||||||
|
},
|
||||||
|
"unterminated_escape": {
|
||||||
|
command: "abc\\",
|
||||||
|
errWrapped: ErrEscapeUnterminated,
|
||||||
|
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
||||||
|
},
|
||||||
|
"unterminated_escape_only": {
|
||||||
|
command: " \\",
|
||||||
|
errWrapped: ErrEscapeUnterminated,
|
||||||
|
errMessage: `unterminated backslash-escape: " \\"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
words, err := Split(testCase.command)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.words, words)
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,8 @@ var (
|
|||||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||||
|
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
|
||||||
|
ErrUpdaterProtonEmailMissing = errors.New("proton email is missing")
|
||||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||||
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ type PortForwarding struct {
|
|||||||
// to write to a file. It cannot be nil for the
|
// to write to a file. It cannot be nil for the
|
||||||
// internal state
|
// internal state
|
||||||
Filepath *string `json:"status_file_path"`
|
Filepath *string `json:"status_file_path"`
|
||||||
|
// UpCommand is the command to use when the port forwarding is up.
|
||||||
|
// It can be the empty string to indicate not to run a command.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
UpCommand *string `json:"up_command"`
|
||||||
|
// DownCommand is the command to use after the port forwarding goes down.
|
||||||
|
// It can be the empty string to indicate to NOT run a command.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
DownCommand *string `json:"down_command"`
|
||||||
// ListeningPort is the port traffic would be redirected to from the
|
// ListeningPort is the port traffic would be redirected to from the
|
||||||
// forwarded port. The redirection is disabled if it is set to 0, which
|
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||||
// is its default as well.
|
// is its default as well.
|
||||||
@@ -84,6 +92,8 @@ func (p *PortForwarding) Copy() (copied PortForwarding) {
|
|||||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
Provider: gosettings.CopyPointer(p.Provider),
|
Provider: gosettings.CopyPointer(p.Provider),
|
||||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||||
|
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||||
|
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||||
Username: p.Username,
|
Username: p.Username,
|
||||||
Password: p.Password,
|
Password: p.Password,
|
||||||
@@ -94,6 +104,8 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
|||||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||||
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||||
|
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
||||||
|
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
||||||
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
||||||
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||||
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||||
@@ -103,6 +115,8 @@ func (p *PortForwarding) setDefaults() {
|
|||||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||||
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
||||||
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||||
|
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
||||||
|
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
||||||
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +149,13 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
|||||||
}
|
}
|
||||||
node.Appendf("Forwarded port file path: %s", filepath)
|
node.Appendf("Forwarded port file path: %s", filepath)
|
||||||
|
|
||||||
|
if *p.UpCommand != "" {
|
||||||
|
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
|
||||||
|
}
|
||||||
|
if *p.DownCommand != "" {
|
||||||
|
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
|
||||||
|
}
|
||||||
|
|
||||||
if p.Username != "" {
|
if p.Username != "" {
|
||||||
credentialsNode := node.Appendf("Credentials:")
|
credentialsNode := node.Appendf("Credentials:")
|
||||||
credentialsNode.Appendf("Username: %s", p.Username)
|
credentialsNode.Appendf("Username: %s", p.Username)
|
||||||
@@ -163,6 +184,12 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
|
|||||||
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
|
||||||
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
|
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
|
||||||
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -31,6 +32,10 @@ type Updater struct {
|
|||||||
// Providers is the list of VPN service providers
|
// Providers is the list of VPN service providers
|
||||||
// to update server information for.
|
// to update server information for.
|
||||||
Providers []string
|
Providers []string
|
||||||
|
// ProtonEmail is the email to authenticate with the Proton API.
|
||||||
|
ProtonEmail *string
|
||||||
|
// ProtonPassword is the password to authenticate with the Proton API.
|
||||||
|
ProtonPassword *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Updater) Validate() (err error) {
|
func (u Updater) Validate() (err error) {
|
||||||
@@ -51,6 +56,18 @@ func (u Updater) Validate() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if provider == providers.Protonvpn {
|
||||||
|
authenticatedAPI := *u.ProtonEmail != "" || *u.ProtonPassword != ""
|
||||||
|
if authenticatedAPI {
|
||||||
|
switch {
|
||||||
|
case *u.ProtonEmail == "":
|
||||||
|
return fmt.Errorf("%w", ErrUpdaterProtonEmailMissing)
|
||||||
|
case *u.ProtonPassword == "":
|
||||||
|
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -58,10 +75,12 @@ func (u Updater) Validate() (err error) {
|
|||||||
|
|
||||||
func (u *Updater) copy() (copied Updater) {
|
func (u *Updater) copy() (copied Updater) {
|
||||||
return Updater{
|
return Updater{
|
||||||
Period: gosettings.CopyPointer(u.Period),
|
Period: gosettings.CopyPointer(u.Period),
|
||||||
DNSAddress: u.DNSAddress,
|
DNSAddress: u.DNSAddress,
|
||||||
MinRatio: u.MinRatio,
|
MinRatio: u.MinRatio,
|
||||||
Providers: gosettings.CopySlice(u.Providers),
|
Providers: gosettings.CopySlice(u.Providers),
|
||||||
|
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
|
||||||
|
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +92,8 @@ func (u *Updater) overrideWith(other Updater) {
|
|||||||
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
||||||
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||||
|
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
|
||||||
|
u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||||
@@ -87,6 +108,10 @@ func (u *Updater) SetDefaults(vpnProvider string) {
|
|||||||
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
||||||
u.Providers = []string{vpnProvider}
|
u.Providers = []string{vpnProvider}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set these to empty strings to avoid nil pointer panics
|
||||||
|
u.ProtonEmail = gosettings.DefaultPointer(u.ProtonEmail, "")
|
||||||
|
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Updater) String() string {
|
func (u Updater) String() string {
|
||||||
@@ -103,6 +128,10 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||||
|
if slices.Contains(u.Providers, providers.Protonvpn) {
|
||||||
|
node.Appendf("Proton API email: %s", *u.ProtonEmail)
|
||||||
|
node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword))
|
||||||
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@@ -125,6 +154,14 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
|||||||
|
|
||||||
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
||||||
|
|
||||||
|
u.ProtonEmail = r.Get("UPDATER_PROTONVPN_EMAIL")
|
||||||
|
if u.ProtonEmail == nil {
|
||||||
|
protonUsername := r.String("UPDATER_PROTONVPN_USERNAME", reader.IsRetro("UPDATER_PROTONVPN_EMAIL"))
|
||||||
|
protonEmail := protonUsername + "@protonmail.com"
|
||||||
|
u.ProtonEmail = &protonEmail
|
||||||
|
}
|
||||||
|
u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
l.backoffTime = defaultBackoffTime
|
l.backoffTime = defaultBackoffTime
|
||||||
l.logger.Info("ready")
|
l.logger.Info("ready")
|
||||||
l.signalOrSetStatus(constants.Running)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +53,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.logAndWait(ctx, err)
|
l.logAndWait(ctx, err)
|
||||||
settings = l.GetSettings()
|
settings = l.GetSettings()
|
||||||
}
|
}
|
||||||
|
l.signalOrSetStatus(constants.Running)
|
||||||
|
|
||||||
settings = l.GetSettings()
|
settings = l.GetSettings()
|
||||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||||
@@ -74,15 +74,19 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
l.stopServer()
|
if !*l.GetSettings().KeepNameserver {
|
||||||
// TODO revert OS and Go nameserver when exiting
|
l.stopServer()
|
||||||
|
// TODO revert OS and Go nameserver when exiting
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
case <-l.stop:
|
case <-l.stop:
|
||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
l.logger.Info("stopping")
|
l.logger.Info("stopping")
|
||||||
const fallback = false
|
if !*l.GetSettings().KeepNameserver {
|
||||||
l.useUnencryptedDNS(fallback)
|
const fallback = false
|
||||||
l.stopServer()
|
l.useUnencryptedDNS(fallback)
|
||||||
|
l.stopServer()
|
||||||
|
}
|
||||||
l.stopped <- struct{}{}
|
l.stopped <- struct{}{}
|
||||||
case <-l.start:
|
case <-l.start:
|
||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type chainRule struct {
|
|||||||
packets uint64
|
packets uint64
|
||||||
bytes uint64
|
bytes uint64
|
||||||
target string // "ACCEPT", "DROP", "REJECT" or "REDIRECT"
|
target string // "ACCEPT", "DROP", "REJECT" or "REDIRECT"
|
||||||
protocol string // "tcp", "udp" or "" for all protocols.
|
protocol string // "icmp", "tcp", "udp" or "" for all protocols.
|
||||||
inputInterface string // input interface, for example "tun0" or "*""
|
inputInterface string // input interface, for example "tun0" or "*""
|
||||||
outputInterface string // output interface, for example "eth0" or "*""
|
outputInterface string // output interface, for example "eth0" or "*""
|
||||||
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
|
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
|
||||||
@@ -324,6 +324,8 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
|
|||||||
func parseProtocol(s string) (protocol string, err error) {
|
func parseProtocol(s string) (protocol string, err error) {
|
||||||
switch s {
|
switch s {
|
||||||
case "0":
|
case "0":
|
||||||
|
case "1":
|
||||||
|
protocol = "icmp"
|
||||||
case "6":
|
case "6":
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
case "17":
|
case "17":
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ num pkts bytes target prot opt in out source destinati
|
|||||||
num pkts bytes target prot opt in out source destination
|
num pkts bytes target prot opt in out source destination
|
||||||
1 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
|
1 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
|
||||||
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
||||||
3 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
|
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
|
||||||
`,
|
`,
|
||||||
table: chain{
|
table: chain{
|
||||||
name: "INPUT",
|
name: "INPUT",
|
||||||
@@ -92,6 +93,17 @@ num pkts bytes target prot opt in out source destinati
|
|||||||
lineNumber: 3,
|
lineNumber: 3,
|
||||||
packets: 0,
|
packets: 0,
|
||||||
bytes: 0,
|
bytes: 0,
|
||||||
|
target: "ACCEPT",
|
||||||
|
protocol: "icmp",
|
||||||
|
inputInterface: "tun0",
|
||||||
|
outputInterface: "*",
|
||||||
|
source: netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lineNumber: 4,
|
||||||
|
packets: 0,
|
||||||
|
bytes: 0,
|
||||||
target: "DROP",
|
target: "DROP",
|
||||||
protocol: "",
|
protocol: "",
|
||||||
inputInterface: "tun0",
|
inputInterface: "tun0",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func testIptablesPath(ctx context.Context, path string,
|
|||||||
// Set policy as the existing policy so no mutation is done.
|
// Set policy as the existing policy so no mutation is done.
|
||||||
// This is an extra check for some buggy kernels where setting the policy
|
// This is an extra check for some buggy kernels where setting the policy
|
||||||
// does not work.
|
// does not work.
|
||||||
cmd = exec.CommandContext(ctx, path, "-L", "INPUT")
|
cmd = exec.CommandContext(ctx, path, "-nL", "INPUT")
|
||||||
output, err = runner.Run(cmd)
|
output, err = runner.Run(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
|
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func newDeleteTestRuleMatcher(path string) *cmdMatcher {
|
|||||||
|
|
||||||
func newListInputRulesMatcher(path string) *cmdMatcher {
|
func newListInputRulesMatcher(path string) *cmdMatcher {
|
||||||
return newCmdMatcher(path,
|
return newCmdMatcher(path,
|
||||||
"^-L$", "^INPUT$")
|
"^-nL$", "^INPUT$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSetPolicyMatcher(path, inputPolicy string) *cmdMatcher { //nolint:unparam
|
func newSetPolicyMatcher(path, inputPolicy string) *cmdMatcher { //nolint:unparam
|
||||||
|
|||||||
@@ -9,16 +9,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
logger Logger
|
logger Logger
|
||||||
handler *handler
|
handler *handler
|
||||||
dialer *net.Dialer
|
dialer *net.Dialer
|
||||||
config settings.Health
|
config settings.Health
|
||||||
vpn vpnHealth
|
vpn vpnHealth
|
||||||
exitOnceConnected bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(config settings.Health,
|
func NewServer(config settings.Health,
|
||||||
logger Logger, vpnLoop StatusApplier, exitOnceConnected bool,
|
logger Logger, vpnLoop StatusApplier,
|
||||||
) *Server {
|
) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package natpmp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -23,14 +24,15 @@ func Test_Client_ExternalAddress(t *testing.T) {
|
|||||||
durationSinceStartOfEpoch time.Duration
|
durationSinceStartOfEpoch time.Duration
|
||||||
externalIPv4Address netip.Addr
|
externalIPv4Address netip.Addr
|
||||||
err error
|
err error
|
||||||
errMessage string
|
errMessageRegex string
|
||||||
}{
|
}{
|
||||||
"failure": {
|
"failure": {
|
||||||
ctx: canceledCtx,
|
ctx: canceledCtx,
|
||||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||||
initialConnDuration: initialConnectionDuration,
|
initialConnDuration: initialConnectionDuration,
|
||||||
err: context.Canceled,
|
err: net.ErrClosed,
|
||||||
errMessage: "executing remote procedure call: reading from udp connection: context canceled",
|
errMessageRegex: "executing remote procedure call: setting connection deadline: " +
|
||||||
|
"set udp 127.0.0.1:[1-9][0-9]{1,4}: use of closed network connection",
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@@ -60,7 +62,7 @@ func Test_Client_ExternalAddress(t *testing.T) {
|
|||||||
durationSinceStartOfEpoch, externalIPv4Address, err := client.ExternalAddress(testCase.ctx, testCase.gateway)
|
durationSinceStartOfEpoch, externalIPv4Address, err := client.ExternalAddress(testCase.ctx, testCase.gateway)
|
||||||
assert.ErrorIs(t, err, testCase.err)
|
assert.ErrorIs(t, err, testCase.err)
|
||||||
if testCase.err != nil {
|
if testCase.err != nil {
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
assert.Regexp(t, testCase.errMessageRegex, err.Error())
|
||||||
}
|
}
|
||||||
assert.Equal(t, testCase.durationSinceStartOfEpoch, durationSinceStartOfEpoch)
|
assert.Equal(t, testCase.durationSinceStartOfEpoch, durationSinceStartOfEpoch)
|
||||||
assert.Equal(t, testCase.externalIPv4Address, externalIPv4Address)
|
assert.Equal(t, testCase.externalIPv4Address, externalIPv4Address)
|
||||||
|
|||||||
@@ -45,8 +45,10 @@ func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
|
|||||||
cancel()
|
cancel()
|
||||||
<-endGoroutineDone
|
<-endGoroutineDone
|
||||||
}()
|
}()
|
||||||
|
ctxListeningReady := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
defer close(endGoroutineDone)
|
defer close(endGoroutineDone)
|
||||||
|
close(ctxListeningReady)
|
||||||
// Context is canceled either by the parent context or
|
// Context is canceled either by the parent context or
|
||||||
// when this function returns.
|
// when this function returns.
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@@ -60,6 +62,7 @@ func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
|
|||||||
}
|
}
|
||||||
err = fmt.Errorf("%w; closing connection: %w", err, closeErr)
|
err = fmt.Errorf("%w; closing connection: %w", err, closeErr)
|
||||||
}()
|
}()
|
||||||
|
<-ctxListeningReady // really to make unit testing reliable
|
||||||
|
|
||||||
const maxResponseSize = 16
|
const maxResponseSize = 16
|
||||||
response = make([]byte, maxResponseSize)
|
response = make([]byte, maxResponseSize)
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package netlink
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/qdm12/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_IsIPv6Supported(t *testing.T) {
|
|
||||||
n := New(log.New(log.SetLevel(log.LevelDebug)))
|
|
||||||
supported, err := n.IsIPv6Supported()
|
|
||||||
t.Log(supported, err)
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package portforward
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
@@ -29,3 +30,8 @@ type Logger interface {
|
|||||||
Warn(s string)
|
Warn(s string)
|
||||||
Error(s string)
|
Error(s string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Cmder interface {
|
||||||
|
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||||
|
waitError <-chan error, startErr error)
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type Loop struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
portAllower PortAllower
|
portAllower PortAllower
|
||||||
logger Logger
|
logger Logger
|
||||||
|
cmder Cmder
|
||||||
// Fixed parameters
|
// Fixed parameters
|
||||||
uid, gid int
|
uid, gid int
|
||||||
// Internal channels and locks
|
// Internal channels and locks
|
||||||
@@ -34,7 +35,7 @@ type Loop struct {
|
|||||||
|
|
||||||
func NewLoop(settings settings.PortForwarding, routing Routing,
|
func NewLoop(settings settings.PortForwarding, routing Routing,
|
||||||
client *http.Client, portAllower PortAllower,
|
client *http.Client, portAllower PortAllower,
|
||||||
logger Logger, uid, gid int,
|
logger Logger, cmder Cmder, uid, gid int,
|
||||||
) *Loop {
|
) *Loop {
|
||||||
return &Loop{
|
return &Loop{
|
||||||
settings: Settings{
|
settings: Settings{
|
||||||
@@ -42,6 +43,8 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
|
|||||||
Service: service.Settings{
|
Service: service.Settings{
|
||||||
Enabled: settings.Enabled,
|
Enabled: settings.Enabled,
|
||||||
Filepath: *settings.Filepath,
|
Filepath: *settings.Filepath,
|
||||||
|
UpCommand: *settings.UpCommand,
|
||||||
|
DownCommand: *settings.DownCommand,
|
||||||
ListeningPort: *settings.ListeningPort,
|
ListeningPort: *settings.ListeningPort,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -49,6 +52,7 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
|
|||||||
client: client,
|
client: client,
|
||||||
portAllower: portAllower,
|
portAllower: portAllower,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
cmder: cmder,
|
||||||
uid: uid,
|
uid: uid,
|
||||||
gid: gid,
|
gid: gid,
|
||||||
}
|
}
|
||||||
@@ -115,7 +119,7 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
*serviceSettings.Enabled = *serviceSettings.Enabled && *l.settings.VPNIsUp
|
*serviceSettings.Enabled = *serviceSettings.Enabled && *l.settings.VPNIsUp
|
||||||
|
|
||||||
l.service = service.New(serviceSettings, l.routing, l.client,
|
l.service = service.New(serviceSettings, l.routing, l.client,
|
||||||
l.portAllower, l.logger, l.uid, l.gid)
|
l.portAllower, l.logger, l.cmder, l.uid, l.gid)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
serviceRunError, err = l.service.Start(runCtx)
|
serviceRunError, err = l.service.Start(runCtx)
|
||||||
|
|||||||
59
internal/portforward/service/command.go
Normal file
59
internal/portforward/service/command.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
||||||
|
commandTemplate string, ports []uint16,
|
||||||
|
) (err error) {
|
||||||
|
portStrings := make([]string, len(ports))
|
||||||
|
for i, port := range ports {
|
||||||
|
portStrings[i] = fmt.Sprint(int(port))
|
||||||
|
}
|
||||||
|
portsString := strings.Join(portStrings, ",")
|
||||||
|
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
||||||
|
args, err := command.Split(commandString)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec G204
|
||||||
|
stdout, stderr, waitError, err := cmder.Start(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||||
|
streamDone := make(chan struct{})
|
||||||
|
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
|
||||||
|
|
||||||
|
err = <-waitError
|
||||||
|
streamCancel()
|
||||||
|
<-streamDone
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamLines(ctx context.Context, done chan<- struct{},
|
||||||
|
logger Logger, stdout, stderr <-chan string,
|
||||||
|
) {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
var line string
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case line = <-stdout:
|
||||||
|
logger.Info(line)
|
||||||
|
case line = <-stderr:
|
||||||
|
logger.Error(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
internal/portforward/service/command_test.go
Normal file
28
internal/portforward/service/command_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
"github.com/qdm12/gluetun/internal/command"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Service_runCommand(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
cmder := command.New()
|
||||||
|
const commandTemplate = `/bin/sh -c "echo {{PORTS}}"`
|
||||||
|
ports := []uint16{1234, 5678}
|
||||||
|
logger := NewMockLogger(ctrl)
|
||||||
|
logger.EXPECT().Info("1234,5678")
|
||||||
|
|
||||||
|
err := runCommand(ctx, cmder, logger, commandTemplate, ports)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||||
)
|
)
|
||||||
@@ -32,3 +33,8 @@ type PortForwarder interface {
|
|||||||
ports []uint16, err error)
|
ports []uint16, err error)
|
||||||
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Cmder interface {
|
||||||
|
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||||
|
waitError <-chan error, startErr error)
|
||||||
|
}
|
||||||
|
|||||||
3
internal/portforward/service/mocks_generate_test.go
Normal file
3
internal/portforward/service/mocks_generate_test.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Logger
|
||||||
82
internal/portforward/service/mocks_test.go
Normal file
82
internal/portforward/service/mocks_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/qdm12/gluetun/internal/portforward/service (interfaces: Logger)
|
||||||
|
|
||||||
|
// Package service is a generated GoMock package.
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockLogger is a mock of Logger interface.
|
||||||
|
type MockLogger struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockLoggerMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||||
|
type MockLoggerMockRecorder struct {
|
||||||
|
mock *MockLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockLogger creates a new mock instance.
|
||||||
|
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||||
|
mock := &MockLogger{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug mocks base method.
|
||||||
|
func (m *MockLogger) Debug(arg0 string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "Debug", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug indicates an expected call of Debug.
|
||||||
|
func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error mocks base method.
|
||||||
|
func (m *MockLogger) Error(arg0 string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "Error", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error indicates an expected call of Error.
|
||||||
|
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info mocks base method.
|
||||||
|
func (m *MockLogger) Info(arg0 string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "Info", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info indicates an expected call of Info.
|
||||||
|
func (mr *MockLoggerMockRecorder) 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)
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ type Service struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
portAllower PortAllower
|
portAllower PortAllower
|
||||||
logger Logger
|
logger Logger
|
||||||
|
cmder Cmder
|
||||||
// Internal channels and locks
|
// Internal channels and locks
|
||||||
startStopMutex sync.Mutex
|
startStopMutex sync.Mutex
|
||||||
keepPortCancel context.CancelFunc
|
keepPortCancel context.CancelFunc
|
||||||
@@ -26,7 +27,7 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(settings Settings, routing Routing, client *http.Client,
|
func New(settings Settings, routing Routing, client *http.Client,
|
||||||
portAllower PortAllower, logger Logger, puid, pgid int,
|
portAllower PortAllower, logger Logger, cmder Cmder, puid, pgid int,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
// Fixed parameters
|
// Fixed parameters
|
||||||
@@ -38,6 +39,7 @@ func New(settings Settings, routing Routing, client *http.Client,
|
|||||||
client: client,
|
client: client,
|
||||||
portAllower: portAllower,
|
portAllower: portAllower,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
cmder: cmder,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ type Settings struct {
|
|||||||
Enabled *bool
|
Enabled *bool
|
||||||
PortForwarder PortForwarder
|
PortForwarder PortForwarder
|
||||||
Filepath string
|
Filepath string
|
||||||
|
UpCommand string
|
||||||
|
DownCommand string
|
||||||
Interface string // needed for PIA, PrivateVPN and ProtonVPN, tun0 for example
|
Interface string // needed for PIA, PrivateVPN and ProtonVPN, tun0 for example
|
||||||
ServerName string // needed for PIA
|
ServerName string // needed for PIA
|
||||||
CanPortForward bool // needed for PIA
|
CanPortForward bool // needed for PIA
|
||||||
@@ -24,6 +26,8 @@ func (s Settings) Copy() (copied Settings) {
|
|||||||
copied.Enabled = gosettings.CopyPointer(s.Enabled)
|
copied.Enabled = gosettings.CopyPointer(s.Enabled)
|
||||||
copied.PortForwarder = s.PortForwarder
|
copied.PortForwarder = s.PortForwarder
|
||||||
copied.Filepath = s.Filepath
|
copied.Filepath = s.Filepath
|
||||||
|
copied.UpCommand = s.UpCommand
|
||||||
|
copied.DownCommand = s.DownCommand
|
||||||
copied.Interface = s.Interface
|
copied.Interface = s.Interface
|
||||||
copied.ServerName = s.ServerName
|
copied.ServerName = s.ServerName
|
||||||
copied.CanPortForward = s.CanPortForward
|
copied.CanPortForward = s.CanPortForward
|
||||||
@@ -37,6 +41,8 @@ func (s *Settings) OverrideWith(update Settings) {
|
|||||||
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, update.Enabled)
|
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, update.Enabled)
|
||||||
s.PortForwarder = gosettings.OverrideWithComparable(s.PortForwarder, update.PortForwarder)
|
s.PortForwarder = gosettings.OverrideWithComparable(s.PortForwarder, update.PortForwarder)
|
||||||
s.Filepath = gosettings.OverrideWithComparable(s.Filepath, update.Filepath)
|
s.Filepath = gosettings.OverrideWithComparable(s.Filepath, update.Filepath)
|
||||||
|
s.UpCommand = gosettings.OverrideWithComparable(s.UpCommand, update.UpCommand)
|
||||||
|
s.DownCommand = gosettings.OverrideWithComparable(s.DownCommand, update.DownCommand)
|
||||||
s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface)
|
s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface)
|
||||||
s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName)
|
s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName)
|
||||||
s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward)
|
s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward)
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
|
|||||||
s.ports = ports
|
s.ports = ports
|
||||||
s.portMutex.Unlock()
|
s.portMutex.Unlock()
|
||||||
|
|
||||||
|
if s.settings.UpCommand != "" {
|
||||||
|
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("running up command: %w", err)
|
||||||
|
s.logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keepPortCtx, keepPortCancel := context.WithCancel(context.Background())
|
keepPortCtx, keepPortCancel := context.WithCancel(context.Background())
|
||||||
s.keepPortCancel = keepPortCancel
|
s.keepPortCancel = keepPortCancel
|
||||||
runErrorCh := make(chan error)
|
runErrorCh := make(chan error)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) Stop() (err error) {
|
func (s *Service) Stop() (err error) {
|
||||||
@@ -30,6 +30,17 @@ func (s *Service) cleanup() (err error) {
|
|||||||
s.portMutex.Lock()
|
s.portMutex.Lock()
|
||||||
defer s.portMutex.Unlock()
|
defer s.portMutex.Unlock()
|
||||||
|
|
||||||
|
if s.settings.DownCommand != "" {
|
||||||
|
const downTimeout = 60 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), downTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err = runCommand(ctx, s.cmder, s.logger, s.settings.DownCommand, s.ports)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("running down command: %w", err)
|
||||||
|
s.logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, port := range s.ports {
|
for _, port := range s.ports {
|
||||||
err = s.portAllower.RemoveAllowedPort(context.Background(), port)
|
err = s.portAllower.RemoveAllowedPort(context.Background(), port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,10 +60,10 @@ func (s *Service) cleanup() (err error) {
|
|||||||
s.ports = nil
|
s.ports = nil
|
||||||
|
|
||||||
filepath := s.settings.Filepath
|
filepath := s.settings.Filepath
|
||||||
s.logger.Info("removing port file " + filepath)
|
s.logger.Info("clearing port file " + filepath)
|
||||||
err = os.Remove(filepath)
|
err = s.writePortForwardedFile(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("removing port file: %w", err)
|
return fmt.Errorf("clearing port file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ var (
|
|||||||
ErrNotEnoughServers = errors.New("not enough servers found")
|
ErrNotEnoughServers = errors.New("not enough servers found")
|
||||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||||
ErrIPFetcherUnsupported = errors.New("IP fetcher not supported")
|
ErrIPFetcherUnsupported = errors.New("IP fetcher not supported")
|
||||||
|
ErrCredentialsMissing = errors.New("credentials missing")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Fetcher interface {
|
type Fetcher interface {
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ type Provider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
parallelResolver common.ParallelResolver,
|
updaterWarner common.Warner, parallelResolver common.ParallelResolver,
|
||||||
) *Provider {
|
) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
Fetcher: updater.New(parallelResolver),
|
Fetcher: updater.New(parallelResolver, updaterWarner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
|||||||
|
|
||||||
possibleHosts := possibleServers.hostsSlice()
|
possibleHosts := possibleServers.hostsSlice()
|
||||||
resolveSettings := parallelResolverSettings(possibleHosts)
|
resolveSettings := parallelResolverSettings(possibleHosts)
|
||||||
hostToIPs, _, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||||
|
for _, warning := range warnings {
|
||||||
|
u.warner.Warn(warning)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import (
|
|||||||
|
|
||||||
type Updater struct {
|
type Updater struct {
|
||||||
parallelResolver common.ParallelResolver
|
parallelResolver common.ParallelResolver
|
||||||
|
warner common.Warner
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(parallelResolver common.ParallelResolver) *Updater {
|
func New(parallelResolver common.ParallelResolver, warner common.Warner) *Updater {
|
||||||
return &Updater{
|
return &Updater{
|
||||||
parallelResolver: parallelResolver,
|
parallelResolver: parallelResolver,
|
||||||
|
warner: warner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
|||||||
providerSettings := utils.OpenVPNProviderSettings{
|
providerSettings := utils.OpenVPNProviderSettings{
|
||||||
AuthUserPass: true,
|
AuthUserPass: true,
|
||||||
Ciphers: []string{
|
Ciphers: []string{
|
||||||
|
openvpn.AES256gcm,
|
||||||
openvpn.AES256cbc,
|
openvpn.AES256cbc,
|
||||||
},
|
},
|
||||||
Auth: openvpn.SHA256,
|
Auth: openvpn.SHA256,
|
||||||
VerifyX509Type: "name",
|
VerifyX509Type: "name",
|
||||||
TLSCipher: "TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA",
|
TLSCipher: "TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA",
|
||||||
CAs: []string{"MIIErTCCA5WgAwIBAgIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wHhcNMTIwMTExMTkzMjIwWhcNMjgxMTAyMTkzMjIwWjCBlTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkZMMRQwEgYDVQQHEwtXaW50ZXIgUGFyazERMA8GA1UEChMISVBWYW5pc2gxFTATBgNVBAsTDElQVmFuaXNoIFZQTjEUMBIGA1UEAxMLSVBWYW5pc2ggQ0ExIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAaXB2YW5pc2guY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt9DBWNr/IKOuY3TmDP5x7vYZR0DGxLbXU8TyAzBbjUtFFMbhxlHiXVQrZHmgzih94x7BgXM7tWpmMKYVb+gNaqMdWE680Qm3nOwmhy/dulXDkEHAwD05i/iTx4ZaUdtV2vsKBxRg1vdC4AEiwD7bqV4HOi13xcG971aQ55Mj1KeCdA0aNvpat1LWx2jjWxsfI8s2Lv5Fkoi1HO1+vTnnaEsJZrBgAkLXpItqP29Lik3/OBIvkBIxlKrhiVPixE5qNiD+eSPirsmROvsyIonoJtuY4Dw5K6pcNlKyYiwo1IOFYU3YxffwFJk+bSW4WVBhsdf5dGxq/uOHmuz5gdwxCwIDAQABo4H9MIH6MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFEv9FCWJHefBcIPX9p8RHCVOGe6uMIHKBgNVHSMEgcIwgb+AFEv9FCWJHefBcIPX9p8RHCVOGe6uoYGbpIGYMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb22CCQDGCs0kvLjygzANBgkqhkiG9w0BAQ0FAAOCAQEAI2dkh/43ksV2fdYpVGhYaFZPVqCJoToCez0IvOmLeLGzow+EOSrY508oyjYeNP4VJEjApqo0NrMbKl8g/8bpLBcotOCF1c1HZ+y9v7648uumh01SMjsbBeHOuQcLb+7gX6c0pEmxWv8qj5JiW3/1L1bktnjW5Yp5oFkFSMXjOnIoYKHyKLjN2jtwH6XowUNYpg4qVtKU0CXPdOznWcd9/zSfa393HwJPeeVLbKYaFMC4IEbIUmKYtWyoJ9pJ58smU3pWsHZUg9Zc0LZZNjkNlBdQSLmUHAJ33Bd7pJS0JQeiWviC+4UTmzEWRKa7pDGnYRYNu2cUo0/voStphv8EVA=="}, //nolint:lll
|
CAs: []string{"MIIErzCCA5egAwIBAgIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wIBcNMjIwNTA5MjAyMDQ1WhgPMjA4MjA0MjQyMDIwNDVaMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC30MFY2v8go65jdOYM/nHu9hlHQMbEttdTxPIDMFuNS0UUxuHGUeJdVCtkeaDOKH3jHsGBczu1amYwphVv6A1qox1YTrzRCbec7CaHL926VcOQQcDAPTmL+JPHhlpR21Xa+woHFGDW90LgASLAPtupXgc6LXfFwb3vVpDnkyPUp4J0DRo2+lq3UtbHaONbGx8jyzYu/kWSiLUc7X69OedoSwlmsGACQteki2o/b0uKTf84Ei+QEjGUquGJU+LETmo2IP55I+KuyZE6+zIiiegm25jgPDkrqlw2UrJiLCjUg4VhTdjF9/AUmT5tJbhZUGGx1/l0bGr+44ea7PmB3DELAgMBAAGjgf0wgfowDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUS/0UJYkd58Fwg9f2nxEcJU4Z7q4wgcoGA1UdIwSBwjCBv4AUS/0UJYkd58Fwg9f2nxEcJU4Z7q6hgZukgZgwgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJGTDEUMBIGA1UEBxMLV2ludGVyIFBhcmsxETAPBgNVBAoTCElQVmFuaXNoMRUwEwYDVQQLEwxJUFZhbmlzaCBWUE4xFDASBgNVBAMTC0lQVmFuaXNoIENBMSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QGlwdmFuaXNoLmNvbYIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAA4IBAQCc9JV7IR8BfBrF/BQTXg0SZMZyyMAxR2jfW9qMHKSeJuZVVjfHiqoynEgBCNbn71wZWv3OF/Thu9BJ4GiYJ2Bc9nIa90D1NGYgiOVYLGXfUUqy5FgfrsWh0Go5oYm9l7W9pWfIifwsaZynkY0rTIHn32FF0H3+wZrGrEUzVL6qi+KD8iR3cBbLT+xUzulMTBp4JYaQnxpV4fZNS0ZsNrWKFWz4Iz1SSBcsnvUhfWs1aKx4yOJQx33Pc+KwpUI+meTlMjoh+AoTriooKU2MbOqLQl32y3pR0MP3fX4HDVFRylxdckEc+VryGNHQLUJiIBKBCORih/YiRhtEhpoBxmkw"}, //nolint:lll
|
||||||
MssFix: 1320,
|
MssFix: 1320,
|
||||||
|
ExtraLines: []string{
|
||||||
|
"comp-lzo", // Explicitly disable compression
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
|
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ type Provider struct {
|
|||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
client *http.Client, updaterWarner common.Warner,
|
client *http.Client, updaterWarner common.Warner,
|
||||||
|
email, password string,
|
||||||
) *Provider {
|
) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
Fetcher: updater.New(client, updaterWarner),
|
Fetcher: updater.New(client, updaterWarner, email, password),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,562 @@
|
|||||||
package updater
|
package updater
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
crand "crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
srp "github.com/ProtonMail/go-srp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
// apiClient is a minimal Proton v4 API client which can handle all the
|
||||||
|
// oddities of Proton's authentication flow they want to keep hidden
|
||||||
|
// from the public.
|
||||||
|
type apiClient struct {
|
||||||
|
apiURLBase string
|
||||||
|
httpClient *http.Client
|
||||||
|
appVersion string
|
||||||
|
userAgent string
|
||||||
|
generator *rand.ChaCha8
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAPIClient returns an [apiClient] with sane defaults matching Proton's
|
||||||
|
// insane expectations.
|
||||||
|
func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClient, err error) {
|
||||||
|
var seed [32]byte
|
||||||
|
_, _ = crand.Read(seed[:])
|
||||||
|
generator := rand.NewChaCha8(seed)
|
||||||
|
|
||||||
|
// Pick a random user agent from this list. Because I'm not going to tell
|
||||||
|
// Proton shit on where all these funny requests are coming from, given their
|
||||||
|
// unhelpfulness in figuring out their authentication flow.
|
||||||
|
userAgents := [...]string{
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||||
|
}
|
||||||
|
userAgent := userAgents[generator.Uint64()%uint64(len(userAgents))]
|
||||||
|
|
||||||
|
appVersion, err := getMostRecentStableTag(ctx, httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting most recent version for proton app: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apiClient{
|
||||||
|
apiURLBase: "https://account.proton.me/api",
|
||||||
|
httpClient: httpClient,
|
||||||
|
appVersion: appVersion,
|
||||||
|
userAgent: userAgent,
|
||||||
|
generator: generator,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrCodeNotSuccess = errors.New("response code is not success")
|
||||||
|
|
||||||
|
// setHeaders sets the minimal necessary headers for Proton API requests
|
||||||
|
// to succeed without being blocked by their "security" measures.
|
||||||
|
// See for example [getMostRecentStableTag] on how the app version must
|
||||||
|
// be set to a recent version or they block your request. "SeCuRiTy"...
|
||||||
|
func (c *apiClient) setHeaders(request *http.Request, cookie cookie) {
|
||||||
|
request.Header.Set("Cookie", cookie.String())
|
||||||
|
request.Header.Set("User-Agent", c.userAgent)
|
||||||
|
request.Header.Set("x-pm-appversion", c.appVersion)
|
||||||
|
request.Header.Set("x-pm-locale", "en_US")
|
||||||
|
request.Header.Set("x-pm-uid", cookie.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate performs the full Proton authentication flow
|
||||||
|
// to obtain an authenticated cookie (uid, token and session ID).
|
||||||
|
func (c *apiClient) authenticate(ctx context.Context, email, password string,
|
||||||
|
) (authCookie cookie, err error) {
|
||||||
|
sessionID, err := c.getSessionID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("getting session ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenType, accessToken, refreshToken, uid, err := c.getUnauthSession(ctx, sessionID)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("getting unauthenticated session data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieToken, err := c.cookieToken(ctx, sessionID, tokenType, accessToken, refreshToken, uid)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("getting cookie token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unauthCookie := cookie{
|
||||||
|
uid: uid,
|
||||||
|
token: cookieToken,
|
||||||
|
sessionID: sessionID,
|
||||||
|
}
|
||||||
|
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64,
|
||||||
|
srpSessionHex, version, err := c.authInfo(ctx, email, unauthCookie)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("getting auth information: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare SRP proof generator using Proton's official SRP parameters and hashing.
|
||||||
|
srpAuth, err := srp.NewAuth(version, username, []byte(password),
|
||||||
|
saltBase64, modulusPGPClearSigned, serverEphemeralBase64)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("initializing SRP auth: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate SRP proofs (A, M1) with the usual 2048-bit modulus.
|
||||||
|
const modulusBits = 2048
|
||||||
|
proofs, err := srpAuth.GenerateProofs(modulusBits)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("generating SRP proofs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authCookie, err = c.auth(ctx, unauthCookie, email, srpSessionHex, proofs)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("authentifying: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authCookie, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrSessionIDNotFound = errors.New("session ID not found in cookies")
|
||||||
|
|
||||||
|
func (c *apiClient) getSessionID(ctx context.Context) (sessionID string, err error) {
|
||||||
|
const url = "https://account.proton.me/vpn"
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("closing response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cookie := range response.Cookies() {
|
||||||
|
if cookie.Name == "Session-Id" {
|
||||||
|
return cookie.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("%w", ErrSessionIDNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrDataFieldMissing = errors.New("data field missing in response")
|
||||||
|
|
||||||
|
func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
|
||||||
|
tokenType, accessToken, refreshToken, uid string, err error,
|
||||||
|
) {
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/auth/v4/sessions", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
unauthCookie := cookie{
|
||||||
|
sessionID: sessionID,
|
||||||
|
}
|
||||||
|
c.setHeaders(request, unauthCookie)
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", fmt.Errorf("reading response body: %w", err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
return "", "", "", "", buildError(response.StatusCode, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Code uint `json:"Code"` // 1000 on success
|
||||||
|
AccessToken string `json:"AccessToken"` // 32-chars lowercase and digits
|
||||||
|
RefreshToken string `json:"RefreshToken"` // 32-chars lowercase and digits
|
||||||
|
TokenType string `json:"TokenType"` // "Bearer"
|
||||||
|
Scopes []string `json:"Scopes"` // should be [] for our usage
|
||||||
|
UID string `json:"UID"` // 32-chars lowercase and digits
|
||||||
|
LocalID uint `json:"LocalID"` // 0 in my case
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(responseBody, &data)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", fmt.Errorf("decoding response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCode = 1000
|
||||||
|
switch {
|
||||||
|
case data.Code != successCode:
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: expected %d got %d",
|
||||||
|
ErrCodeNotSuccess, successCode, data.Code)
|
||||||
|
case data.AccessToken == "":
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: access token is empty", ErrDataFieldMissing)
|
||||||
|
case data.RefreshToken == "":
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: refresh token is empty", ErrDataFieldMissing)
|
||||||
|
case data.TokenType == "":
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: token type is empty", ErrDataFieldMissing)
|
||||||
|
case data.UID == "":
|
||||||
|
return "", "", "", "", fmt.Errorf("%w: UID is empty", ErrDataFieldMissing)
|
||||||
|
}
|
||||||
|
// Ignore Scopes and LocalID fields, we don't use them.
|
||||||
|
|
||||||
|
return data.TokenType, data.AccessToken, data.RefreshToken, data.UID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUIDMismatch = errors.New("UID in response does not match request UID")
|
||||||
|
|
||||||
|
func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, accessToken,
|
||||||
|
refreshToken, uid string,
|
||||||
|
) (cookieToken string, err error) {
|
||||||
|
type requestBodySchema struct {
|
||||||
|
GrantType string `json:"GrantType"` // "refresh_token"
|
||||||
|
Persistent uint `json:"Persistent"` // 0
|
||||||
|
RedirectURI string `json:"RedirectURI"` // "https://protonmail.com"
|
||||||
|
RefreshToken string `json:"RefreshToken"` // 32-chars lowercase and digits
|
||||||
|
ResponseType string `json:"ResponseType"` // "token"
|
||||||
|
State string `json:"State"` // 24-chars letters and digits
|
||||||
|
UID string `json:"UID"` // 32-chars lowercase and digits
|
||||||
|
}
|
||||||
|
requestBody := requestBodySchema{
|
||||||
|
GrantType: "refresh_token",
|
||||||
|
Persistent: 0,
|
||||||
|
RedirectURI: "https://protonmail.com",
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
ResponseType: "token",
|
||||||
|
State: generateLettersDigits(c.generator, 24), //nolint:mnd
|
||||||
|
UID: uid,
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
if err := encoder.Encode(requestBody); err != nil {
|
||||||
|
return "", fmt.Errorf("encoding request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth/cookies", buffer)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
unauthCookie := cookie{
|
||||||
|
uid: uid,
|
||||||
|
sessionID: sessionID,
|
||||||
|
}
|
||||||
|
c.setHeaders(request, unauthCookie)
|
||||||
|
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading response body: %w", err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
return "", buildError(response.StatusCode, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies struct {
|
||||||
|
Code uint `json:"Code"` // 1000 on success
|
||||||
|
UID string `json:"UID"` // should match request UID
|
||||||
|
LocalID uint `json:"LocalID"` // 0
|
||||||
|
RefreshCounter uint `json:"RefreshCounter"` // 1
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &cookies)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decoding response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCode = 1000
|
||||||
|
switch {
|
||||||
|
case cookies.Code != successCode:
|
||||||
|
return "", fmt.Errorf("%w: expected %d got %d",
|
||||||
|
ErrCodeNotSuccess, successCode, cookies.Code)
|
||||||
|
case cookies.UID != requestBody.UID:
|
||||||
|
return "", fmt.Errorf("%w: expected %s got %s",
|
||||||
|
ErrUIDMismatch, requestBody.UID, cookies.UID)
|
||||||
|
}
|
||||||
|
// Ignore LocalID and RefreshCounter fields, we don't use them.
|
||||||
|
|
||||||
|
for _, cookie := range response.Cookies() {
|
||||||
|
if cookie.Name == "AUTH-"+uid {
|
||||||
|
return cookie.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("%w", ErrAuthCookieNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUsernameDoesNotExist = errors.New("username does not exist")
|
||||||
|
|
||||||
|
// authInfo fetches SRP parameters for the account.
|
||||||
|
func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie cookie) (
|
||||||
|
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64, srpSessionHex string,
|
||||||
|
version int, err error,
|
||||||
|
) {
|
||||||
|
type requestBodySchema struct {
|
||||||
|
Intent string `json:"Intent"` // "Proton"
|
||||||
|
Username string `json:"Username"`
|
||||||
|
}
|
||||||
|
requestBody := requestBodySchema{
|
||||||
|
Intent: "Proton",
|
||||||
|
Username: email,
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
if err := encoder.Encode(requestBody); err != nil {
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("encoding request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth/info", buffer)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
c.setHeaders(request, unauthCookie)
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", 0, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("reading response body: %w", err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
return "", "", "", "", "", 0, buildError(response.StatusCode, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
var info struct {
|
||||||
|
Code uint `json:"Code"` // 1000 on success
|
||||||
|
Modulus string `json:"Modulus"` // PGP clearsigned modulus string
|
||||||
|
ServerEphemeral string `json:"ServerEphemeral"` // base64
|
||||||
|
Version *uint `json:"Version,omitempty"` // 4 as of 2025-10-26
|
||||||
|
Salt string `json:"Salt"` // base64
|
||||||
|
SRPSession string `json:"SRPSession"` // hexadecimal
|
||||||
|
Username string `json:"Username"` // user without @domain.com. Mine has its first letter capitalized.
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &info)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("decoding response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCode = 1000
|
||||||
|
switch {
|
||||||
|
case info.Code != successCode:
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: expected %d got %d",
|
||||||
|
ErrCodeNotSuccess, successCode, info.Code)
|
||||||
|
case info.Modulus == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: modulus is empty", ErrDataFieldMissing)
|
||||||
|
case info.ServerEphemeral == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: server ephemeral is empty", ErrDataFieldMissing)
|
||||||
|
case info.Salt == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w (salt data field is empty)", ErrUsernameDoesNotExist)
|
||||||
|
case info.SRPSession == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: SRP session is empty", ErrDataFieldMissing)
|
||||||
|
case info.Username == "":
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: username is empty", ErrDataFieldMissing)
|
||||||
|
case info.Version == nil:
|
||||||
|
return "", "", "", "", "", 0, fmt.Errorf("%w: version is missing", ErrDataFieldMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
version = int(*info.Version) //nolint:gosec
|
||||||
|
return info.Username, info.Modulus, info.ServerEphemeral, info.Salt,
|
||||||
|
info.SRPSession, version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type cookie struct {
|
||||||
|
uid string
|
||||||
|
token string
|
||||||
|
sessionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cookie) String() string {
|
||||||
|
s := ""
|
||||||
|
if c.token != "" {
|
||||||
|
s += fmt.Sprintf("AUTH-%s=%s; ", c.uid, c.token)
|
||||||
|
}
|
||||||
|
if c.sessionID != "" {
|
||||||
|
s += fmt.Sprintf("Session-Id=%s; ", c.sessionID)
|
||||||
|
}
|
||||||
|
if c.token != "" {
|
||||||
|
s += "Tag=default; iaas=W10; Domain=proton.me; Feature=VPNDashboard:A"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrServerProofNotValid indicates the M2 from the server didn't match the expected proof.
|
||||||
|
ErrServerProofNotValid = errors.New("server proof from server is not valid")
|
||||||
|
ErrVPNScopeNotFound = errors.New("VPN scope not found in scopes")
|
||||||
|
ErrTwoFANotSupported = errors.New("two factor authentication not supported in this client")
|
||||||
|
ErrAuthCookieNotFound = errors.New("auth cookie not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// auth performs the SRP proof submission (and optionally TOTP) to obtain tokens.
|
||||||
|
func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
|
||||||
|
username, srpSession string, proofs *srp.Proofs,
|
||||||
|
) (authCookie cookie, err error) {
|
||||||
|
clientEphemeral := base64.StdEncoding.EncodeToString(proofs.ClientEphemeral)
|
||||||
|
clientProof := base64.StdEncoding.EncodeToString(proofs.ClientProof)
|
||||||
|
|
||||||
|
type requestBodySchema struct {
|
||||||
|
ClientEphemeral string `json:"ClientEphemeral"` // base64(A)
|
||||||
|
ClientProof string `json:"ClientProof"` // base64(M1)
|
||||||
|
Payload map[string]string `json:"Payload,omitempty"` // not sure
|
||||||
|
SRPSession string `json:"SRPSession"` // hexadecimal
|
||||||
|
Username string `json:"Username"` // user@protonmail.com
|
||||||
|
}
|
||||||
|
requestBody := requestBodySchema{
|
||||||
|
ClientEphemeral: clientEphemeral,
|
||||||
|
ClientProof: clientProof,
|
||||||
|
SRPSession: srpSession,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
if err := encoder.Encode(requestBody); err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("encoding request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth", buffer)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
c.setHeaders(request, unauthCookie)
|
||||||
|
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("reading response body: %w", err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
return cookie{}, buildError(response.StatusCode, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
type twoFAStatus uint
|
||||||
|
//nolint:unused
|
||||||
|
const (
|
||||||
|
twoFADisabled twoFAStatus = iota
|
||||||
|
twoFAHasTOTP
|
||||||
|
twoFAHasFIDO2
|
||||||
|
twoFAHasFIDO2AndTOTP
|
||||||
|
)
|
||||||
|
type twoFAInfo struct {
|
||||||
|
Enabled twoFAStatus `json:"Enabled"`
|
||||||
|
FIDO2 struct {
|
||||||
|
AuthenticationOptions any `json:"AuthenticationOptions"`
|
||||||
|
RegisteredKeys []any `json:"RegisteredKeys"`
|
||||||
|
} `json:"FIDO2"`
|
||||||
|
TOTP uint `json:"TOTP"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var auth struct {
|
||||||
|
Code uint `json:"Code"` // 1000 on success
|
||||||
|
LocalID uint `json:"LocalID"` // 7 in my case
|
||||||
|
Scopes []string `json:"Scopes"` // this should contain "vpn". Same as `Scope` field value.
|
||||||
|
UID string `json:"UID"` // same as `Uid` field value
|
||||||
|
UserID string `json:"UserID"` // base64
|
||||||
|
EventID string `json:"EventID"` // base64
|
||||||
|
PasswordMode uint `json:"PasswordMode"` // 1 in my case
|
||||||
|
ServerProof string `json:"ServerProof"` // base64(M2)
|
||||||
|
TwoFactor uint `json:"TwoFactor"` // 0 if 2FA not required
|
||||||
|
TwoFA twoFAInfo `json:"2FA"`
|
||||||
|
TemporaryPassword uint `json:"TemporaryPassword"` // 0 in my case
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(responseBody, &auth)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("decoding response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m2, err := base64.StdEncoding.DecodeString(auth.ServerProof)
|
||||||
|
if err != nil {
|
||||||
|
return cookie{}, fmt.Errorf("decoding server proof: %w", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(m2, proofs.ExpectedServerProof) {
|
||||||
|
return cookie{}, fmt.Errorf("%w: expected %x got %x",
|
||||||
|
ErrServerProofNotValid, proofs.ExpectedServerProof, m2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCode = 1000
|
||||||
|
switch {
|
||||||
|
case auth.Code != successCode:
|
||||||
|
return cookie{}, fmt.Errorf("%w: expected %d got %d",
|
||||||
|
ErrCodeNotSuccess, successCode, auth.Code)
|
||||||
|
case auth.UID != unauthCookie.uid:
|
||||||
|
return cookie{}, fmt.Errorf("%w: expected %s got %s",
|
||||||
|
ErrUIDMismatch, unauthCookie.uid, auth.UID)
|
||||||
|
case auth.TwoFactor != 0:
|
||||||
|
return cookie{}, fmt.Errorf("%w", ErrTwoFANotSupported)
|
||||||
|
case !slices.Contains(auth.Scopes, "vpn"):
|
||||||
|
return cookie{}, fmt.Errorf("%w: in %v", ErrVPNScopeNotFound, auth.Scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, setCookieHeader := range response.Header.Values("Set-Cookie") {
|
||||||
|
parts := strings.Split(setCookieHeader, ";")
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.HasPrefix(part, "AUTH-"+unauthCookie.uid+"=") {
|
||||||
|
authCookie = unauthCookie
|
||||||
|
authCookie.token = strings.TrimPrefix(part, "AUTH-"+unauthCookie.uid+"=")
|
||||||
|
return authCookie, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie{}, fmt.Errorf("%w: in HTTP headers %s",
|
||||||
|
ErrAuthCookieNotFound, httpHeadersToString(response.Header))
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateLettersDigits mimicing Proton's own random string generator:
|
||||||
|
// https://github.com/ProtonMail/WebClients/blob/e4d7e4ab9babe15b79a131960185f9f8275512cd/packages/utils/generateLettersDigits.ts
|
||||||
|
func generateLettersDigits(rng *rand.ChaCha8, length uint) string {
|
||||||
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
return generateFromCharset(rng, length, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFromCharset(rng *rand.ChaCha8, length uint, charset string) string {
|
||||||
|
result := make([]byte, length)
|
||||||
|
randomBytes := make([]byte, length)
|
||||||
|
_, _ = rng.Read(randomBytes)
|
||||||
|
for i := range length {
|
||||||
|
result[i] = charset[int(randomBytes[i])%len(charset)]
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpHeadersToString(headers http.Header) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
first := true
|
||||||
|
for key, values := range headers {
|
||||||
|
for _, value := range values {
|
||||||
|
if !first {
|
||||||
|
builder.WriteString(", ")
|
||||||
|
}
|
||||||
|
builder.WriteString(fmt.Sprintf("%s: %s", key, value))
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
type apiData struct {
|
type apiData struct {
|
||||||
LogicalServers []logicalServer `json:"LogicalServers"`
|
LogicalServers []logicalServer `json:"LogicalServers"`
|
||||||
@@ -33,25 +580,25 @@ type physicalServer struct {
|
|||||||
X25519PublicKey string `json:"X25519PublicKey"`
|
X25519PublicKey string `json:"X25519PublicKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
||||||
data apiData, err error,
|
data apiData, err error,
|
||||||
) {
|
) {
|
||||||
const url = "https://api.protonmail.ch/vpn/logicals"
|
const url = "https://account.proton.me/api/vpn/logicals"
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
c.setHeaders(request, cookie)
|
||||||
|
|
||||||
response, err := client.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK {
|
if response.StatusCode != http.StatusOK {
|
||||||
return data, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
b, _ := io.ReadAll(response.Body)
|
||||||
response.StatusCode, response.Status)
|
return data, buildError(response.StatusCode, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
decoder := json.NewDecoder(response.Body)
|
decoder := json.NewDecoder(response.Body)
|
||||||
@@ -59,9 +606,31 @@ func fetchAPI(ctx context.Context, client *http.Client) (
|
|||||||
return data, fmt.Errorf("decoding response body: %w", err)
|
return data, fmt.Errorf("decoding response body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := response.Body.Close(); err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||||
|
|
||||||
|
func buildError(httpCode int, body []byte) error {
|
||||||
|
prettyCode := http.StatusText(httpCode)
|
||||||
|
var protonError struct {
|
||||||
|
Code *int `json:"Code,omitempty"`
|
||||||
|
Error *string `json:"Error,omitempty"`
|
||||||
|
Details map[string]string `json:"Details"`
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err := decoder.Decode(&protonError)
|
||||||
|
if err != nil || protonError.Error == nil || protonError.Code == nil {
|
||||||
|
return fmt.Errorf("%w: %s: %s",
|
||||||
|
ErrHTTPStatusCodeNotOK, prettyCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
details := make([]string, 0, len(protonError.Details))
|
||||||
|
for key, value := range protonError.Details {
|
||||||
|
details = append(details, fmt.Sprintf("%s: %s", key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%w: %s: %s (code %d with details: %s)",
|
||||||
|
ErrHTTPStatusCodeNotOK, prettyCode, *protonError.Error, *protonError.Code, strings.Join(details, ", "))
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,9 +13,26 @@ import (
|
|||||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||||
servers []models.Server, err error,
|
servers []models.Server, err error,
|
||||||
) {
|
) {
|
||||||
data, err := fetchAPI(ctx, u.client)
|
switch {
|
||||||
|
case u.email == "":
|
||||||
|
return nil, fmt.Errorf("%w: email is empty", common.ErrCredentialsMissing)
|
||||||
|
case u.password == "":
|
||||||
|
return nil, fmt.Errorf("%w: password is empty", common.ErrCredentialsMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiClient, err := newAPIClient(ctx, u.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("creating API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := apiClient.authenticate(ctx, u.email, u.password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("authentifying with Proton: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := apiClient.fetchServers(ctx, cookie)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching logical servers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
countryCodes := constants.CountryCodes()
|
countryCodes := constants.CountryCodes()
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Updater struct {
|
type Updater struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
warner common.Warner
|
email string
|
||||||
|
password string
|
||||||
|
warner common.Warner
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(client *http.Client, warner common.Warner) *Updater {
|
func New(client *http.Client, warner common.Warner, email, password string) *Updater {
|
||||||
return &Updater{
|
return &Updater{
|
||||||
client: client,
|
client: client,
|
||||||
warner: warner,
|
email: email,
|
||||||
|
password: password,
|
||||||
|
warner: warner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
internal/provider/protonvpn/updater/version.go
Normal file
64
internal/provider/protonvpn/updater/version.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getMostRecentStableTag finds the most recent proton-account stable tag version,
|
||||||
|
// in order to use it in the x-pm-appversion http request header. Because if we do
|
||||||
|
// fall behind on versioning, Proton doesn't like it because they like to create
|
||||||
|
// complications where there is no need for it. Hence this function.
|
||||||
|
func getMostRecentStableTag(ctx context.Context, client *http.Client) (version string, err error) {
|
||||||
|
page := 1
|
||||||
|
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
url := "https://api.github.com/repos/ProtonMail/WebClients/tags?per_page=30&page=" + fmt.Sprint(page)
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("%w: %s: %s", ErrHTTPStatusCodeNotOK, response.Status, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &tags)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decoding JSON response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
if !regexVersion.MatchString(tag.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
version := "web-account@" + strings.TrimPrefix(tag.Name, "proton-account@")
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("%w (queried %d pages)", context.Canceled, page)
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ type Extractor interface {
|
|||||||
func NewProviders(storage Storage, timeNow func() time.Time,
|
func NewProviders(storage Storage, timeNow func() time.Time,
|
||||||
updaterWarner common.Warner, client *http.Client, unzipper common.Unzipper,
|
updaterWarner common.Warner, client *http.Client, unzipper common.Unzipper,
|
||||||
parallelResolver common.ParallelResolver, ipFetcher common.IPFetcher,
|
parallelResolver common.ParallelResolver, ipFetcher common.IPFetcher,
|
||||||
extractor custom.Extractor,
|
extractor custom.Extractor, credentials settings.Updater,
|
||||||
) *Providers {
|
) *Providers {
|
||||||
randSource := rand.NewSource(timeNow().UnixNano())
|
randSource := rand.NewSource(timeNow().UnixNano())
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
|||||||
providerNameToProvider := map[string]Provider{
|
providerNameToProvider := map[string]Provider{
|
||||||
providers.Airvpn: airvpn.New(storage, randSource, client),
|
providers.Airvpn: airvpn.New(storage, randSource, client),
|
||||||
providers.Custom: custom.New(extractor),
|
providers.Custom: custom.New(extractor),
|
||||||
providers.Cyberghost: cyberghost.New(storage, randSource, parallelResolver),
|
providers.Cyberghost: cyberghost.New(storage, randSource, updaterWarner, parallelResolver),
|
||||||
providers.Expressvpn: expressvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Expressvpn: expressvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
providers.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
@@ -75,7 +75,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
|||||||
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
|
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
|
||||||
providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner),
|
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword),
|
||||||
providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
|||||||
Ciphers: []string{openvpn.AES256cbc},
|
Ciphers: []string{openvpn.AES256cbc},
|
||||||
Auth: openvpn.SHA512,
|
Auth: openvpn.SHA512,
|
||||||
CAs: []string{
|
CAs: []string{
|
||||||
"MIID7jCCA1CgAwIBAgIQQTT3w3N+5i8OMfe565xaSjAKBggqhkjOPQQDBDCBojELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xGjAYBgNVBAsMEUtlZXBTb2xpZCBSb290IENBMRowGAYDVQQDDBFLZWVwU29saWQgUm9vdCBDQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5Aa2VlcHNvbGlkLmNvbTAeFw0yMDA0MDExNjI3MTRaFw0yNTAzMzExNjI3MTRaMIGgMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEVMBMGA1UECwwMS2VlcFNvbGlkIENBMR0wGwYDVQQDDBRPcGVuVlBOIFNlcnZlciBTdWJDQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5Aa2VlcHNvbGlkLmNvbTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAR9nmoZUraRSSPUhYwIQBLSx+phJdIlqU7F7Hszh95ivnWYkwuizKLaUYy6lSISDohlUtQl9URBlRrGroVctOGlOAdpL2ARTljw5gmUcaavc5cvLiAV7fPJ7BFUgVxInmaVcaMlDwGgKLxmjU2Fw85VLROHbWQjYc93x/BTSFcYO/np4o4IBIzCCAR8wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUrUCjH8xe37lJihyzpqjWwxxNOiswgeIGA1UdIwSB2jCB14AU/LRRnTRaEbxct895Pk9DoymNQIqhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb22CFEssZFYAz8WhYnIDxLeDgKTLD8p2MAsGA1UdDwQEAwIBBjAKBggqhkjOPQQDBAOBiwAwgYcCQgGuK8UNnpE8k8hAamnT9gxCSs5APqrgmdLe6BxYSz7AptpF2/MPzLFsXgj4YxC6vJP8Rs8e3Hw9VJ7DF0aYgu8DvQJBeyFWjRnk8kmu2zEU+wF9fkvN9AJ7v0xF0iEaFVsdPKv6sJQP1sAL+AIepJQ7TYvh9Q9G/WaRCfItCtcOAEz3SKA=", //nolint:lll
|
"MIIECjCCA2ygAwIBAgIRAJ/aLZu0PCO7LlOTcPQE9UwwCgYIKoZIzj0EAwQwgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wHhcNMjUwMzMxMTQ0OTU4WhcNMzAwNjEzMTQ0OTU4WjCBqTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xHTAbBgNVBAsMFEtlZXBTb2xpZCBPcGVuVlBOIENBMR0wGwYDVQQDDBRLZWVwU29saWQgT3BlblZQTiBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAEHfJRyn9MZ7HQctQULIxVUNFFw+tWetokml5PvIsS1i3mM4NQnj0HHL5zCCQRKUmSiiWtGvbGlsHEWX/hz+NiVoQGjMqBD2ykdLimiFrceonIofEBZW8to6jTjG3wmJkRykDqsuLyBLUKGc2F5dR3YFGgwyDoRz0NaAYI+qgqWfE+cVaOCASwwggEoMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFB4IhTj1gStDx+fNq+ubBcr+lEbwMIHrBgNVHSMEgeMwgeCAFOEcFx6OcN8T1R8lTdCLhFlYuk5joYGxpIGuMIGrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEeMBwGA1UECwwVS2VlcFNvbGlkIFZQTiBSb290IENBMR4wHAYDVQQDDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExIzAhBgkqhkiG9w0BCQEWFGFkbWluc0BrZWVwc29saWQuY29tghRnfb8jJuxu5dJzLm5ZdurkedrxzjALBgNVHQ8EBAMCAQYwCgYIKoZIzj0EAwQDgYsAMIGHAkIBg8Cdu474VlljCoP8WEr6xErKL6Bygy5+SO1Ey0Uu3B7q8R22F0EWvrOmqmyNZ3oRyqhpUGaEBqB2aqDGT7u7wGsCQUP3nyMlDbXqCF05byMbhQrBsCz1nyqDNnfzM2uGmT09XwWXGCYTIGdynyJJLzdOlpf3T19ZLvqLSf6Kvq45u6si", //nolint:lll
|
||||||
"MIID9zCCA1igAwIBAgIUSyxkVgDPxaFicgPEt4OApMsPynYwCgYIKoZIzj0EAwQwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb20wIBcNMTkxMjMxMTY1NzMyWhgPMjA1NzA1MTUxNjU3MzJaMIGiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEaMBgGA1UECwwRS2VlcFNvbGlkIFJvb3QgQ0ExGjAYBgNVBAMMEUtlZXBTb2xpZCBSb290IENBMSIwIAYJKoZIhvcNAQkBFhNhZG1pbkBrZWVwc29saWQuY29tMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBlmcBvPVcV7mVgiUVkO9Dh8b4Hdw/eyU8OLWJ0qyRiqI1q0ad1cxgi6asy33xwilrMkRxhArDSfB87zpUpUboTEMBSf9n+dCoGRncGfW9G+8IvhzPY3Z3nzVHBGhoKlN1jsCuKzzpjGawqTAeCkJNBPQNd75Dp6Tgl198bAowD+iPX3WjggEjMIIBHzAdBgNVHQ4EFgQU/LRRnTRaEbxct895Pk9DoymNQIowgeIGA1UdIwSB2jCB14AU/LRRnTRaEbxct895Pk9DoymNQIqhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb22CFEssZFYAz8WhYnIDxLeDgKTLD8p2MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMAoGCCqGSM49BAMEA4GMADCBiAJCAbDIYRjZYaDwMf8Gq4udKVS4aWtZt73lVulCVmr951tQ9J2Dzh4OEQZvU5+M688o2N/fVxNQoxwm/NsiJxpc/prQAkIBiRbrcEGvalu9h6UqE6yAXe0JZcF5xn/BIe5XygglOput4kvZKLKtIqPe2bwBmL/dqq6XDL7s5QaTWPo5MtpzGjA=", //nolint:lll
|
"MIIEEDCCA3GgAwIBAgIUZ32/IybsbuXScy5uWXbq5Hna8c4wCgYIKoZIzj0EAwQwgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wHhcNMjUwMzMxMTQ0NTUzWhcNMzUwODI2MTQ0NTUzWjCBqzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xHjAcBgNVBAsMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEeMBwGA1UEAwwVS2VlcFNvbGlkIFZQTiBSb290IENBMSMwIQYJKoZIhvcNAQkBFhRhZG1pbnNAa2VlcHNvbGlkLmNvbTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAN77xqCz3wrFDnRMtggwScgvO6wPFZYECTUu5WW0JaowgmuIgo+BiQQyTeUzJEICulc1Hg7EaUEV+z8jsSrB+4/EAWazn/ufWOx/51fa5FCv4YooCbgLPb1CzYDuTc7MUR5PLQ88o3W01wCCgT8RoNH8uChyPBLUBh2f4rUfpzl20Bqdo4IBLDCCASgwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQU4RwXHo5w3xPVHyVN0IuEWVi6TmMwgesGA1UdIwSB4zCB4IAU4RwXHo5w3xPVHyVN0IuEWVi6TmOhgbGkga4wgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb22CFGd9vyMm7G7l0nMubll26uR52vHOMAsGA1UdDwQEAwIBBjAKBggqhkjOPQQDBAOBjAAwgYgCQgCZtqE+wXwH0ixjWafX3SClp8O3bYeyB/7jbzf8MprXRYBVQ8JjvugjaZTvX82Uy++LaN3oHqK+NUhJUdfZx/eIuQJCAad7HpsKyTYuUUkgAgWXJma4MstxyO9PVRNYozi1oc45Z8deSvwy404n3u1kY5QXLZQaaMY7m2pF+ECs4WkKCh5s", //nolint:lll
|
||||||
},
|
},
|
||||||
ExtraLines: []string{
|
ExtraLines: []string{
|
||||||
"route-metric 1",
|
"route-metric 1",
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !*l.settings.Enabled {
|
||||||
|
singleRunResult <- nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
result, err := l.fetcher.FetchInfo(singleRunCtx, netip.Addr{})
|
result, err := l.fetcher.FetchInfo(singleRunCtx, netip.Addr{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("fetching information: %w", err)
|
err = fmt.Errorf("fetching information: %w", err)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m *logMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
m.childHandler.ServeHTTP(statefulWriter, r)
|
m.childHandler.ServeHTTP(statefulWriter, r)
|
||||||
duration := m.timeNow().Sub(tStart)
|
duration := m.timeNow().Sub(tStart)
|
||||||
m.logger.Info(strconv.Itoa(statefulWriter.statusCode) + " " +
|
m.logger.Info(strconv.Itoa(statefulWriter.statusCode) + " " +
|
||||||
r.Method + " " + r.RequestURI +
|
r.Method + " " + r.URL.String() +
|
||||||
" wrote " + strconv.Itoa(statefulWriter.length) + "B to " +
|
" wrote " + strconv.Itoa(statefulWriter.length) + "B to " +
|
||||||
r.RemoteAddr + " in " + duration.String())
|
r.RemoteAddr + " in " + duration.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Infoer
|
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Logger
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Infoer)
|
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Logger)
|
||||||
|
|
||||||
// Package storage is a generated GoMock package.
|
// Package storage is a generated GoMock package.
|
||||||
package storage
|
package storage
|
||||||
@@ -10,37 +10,49 @@ import (
|
|||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockInfoer is a mock of Infoer interface.
|
// MockLogger is a mock of Logger interface.
|
||||||
type MockInfoer struct {
|
type MockLogger struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
recorder *MockInfoerMockRecorder
|
recorder *MockLoggerMockRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockInfoerMockRecorder is the mock recorder for MockInfoer.
|
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||||
type MockInfoerMockRecorder struct {
|
type MockLoggerMockRecorder struct {
|
||||||
mock *MockInfoer
|
mock *MockLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockInfoer creates a new mock instance.
|
// NewMockLogger creates a new mock instance.
|
||||||
func NewMockInfoer(ctrl *gomock.Controller) *MockInfoer {
|
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||||
mock := &MockInfoer{ctrl: ctrl}
|
mock := &MockLogger{ctrl: ctrl}
|
||||||
mock.recorder = &MockInfoerMockRecorder{mock}
|
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||||
return mock
|
return mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
func (m *MockInfoer) EXPECT() *MockInfoerMockRecorder {
|
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info mocks base method.
|
// Info mocks base method.
|
||||||
func (m *MockInfoer) Info(arg0 string) {
|
func (m *MockLogger) Info(arg0 string) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
m.ctrl.Call(m, "Info", arg0)
|
m.ctrl.Call(m, "Info", arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info indicates an expected call of Info.
|
// Info indicates an expected call of Info.
|
||||||
func (mr *MockInfoerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockInfoer)(nil).Info), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn mocks base method.
|
||||||
|
func (m *MockLogger) Warn(arg0 string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "Warn", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn indicates an expected call of Warn.
|
||||||
|
func (mr *MockLoggerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), arg0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
logger := NewMockInfoer(ctrl)
|
logger := NewMockLogger(ctrl)
|
||||||
var previousLogCall *gomock.Call
|
var previousLogCall *gomock.Call
|
||||||
for _, logged := range testCase.logged {
|
for _, logged := range testCase.logged {
|
||||||
call := logger.EXPECT().Info(logged)
|
call := logger.EXPECT().Info(logged)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,18 +13,20 @@ type Storage struct {
|
|||||||
// the embedded JSON file on every call to the
|
// the embedded JSON file on every call to the
|
||||||
// SyncServers method.
|
// SyncServers method.
|
||||||
hardcodedServers models.AllServers
|
hardcodedServers models.AllServers
|
||||||
logger Infoer
|
logger Logger
|
||||||
filepath string
|
filepath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Infoer interface {
|
type Logger interface {
|
||||||
Info(s string)
|
Info(s string)
|
||||||
|
Warn(s string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new storage and reads the servers from the
|
// New creates a new storage and reads the servers from the
|
||||||
// embedded servers file and the file on disk.
|
// embedded servers file and the file on disk.
|
||||||
// Passing an empty filepath disables writing servers to a file.
|
// Passing an empty filepath disables the reading and writing of
|
||||||
func New(logger Infoer, filepath string) (storage *Storage, err error) {
|
// servers.
|
||||||
|
func New(logger Logger, filepath string) (storage *Storage, err error) {
|
||||||
// A unit test prevents any error from being returned
|
// A unit test prevents any error from being returned
|
||||||
// and ensures all providers are part of the servers returned.
|
// and ensures all providers are part of the servers returned.
|
||||||
hardcodedServers, _ := parseHardcodedServers()
|
hardcodedServers, _ := parseHardcodedServers()
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (s *Storage) syncServers() (err error) {
|
|||||||
|
|
||||||
err = s.flushToFile(s.filepath)
|
err = s.flushToFile(s.filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writing servers to file: %w", err)
|
s.logger.Warn("failed writing servers to file: " + err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func (u *Updater) updateProvider(ctx context.Context, provider Provider,
|
|||||||
u.logger.Warn("note: if running the update manually, you can use the flag " +
|
u.logger.Warn("note: if running the update manually, you can use the flag " +
|
||||||
"-minratio to allow the update to succeed with less servers found")
|
"-minratio to allow the update to succeed with less servers found")
|
||||||
}
|
}
|
||||||
return fmt.Errorf("getting servers: %w", err)
|
return fmt.Errorf("getting %s servers: %w", providerName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package updater
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
@@ -48,22 +50,22 @@ func (u *Updater) UpdateServers(ctx context.Context, providers []string,
|
|||||||
// TODO support servers offering only TCP or only UDP
|
// TODO support servers offering only TCP or only UDP
|
||||||
// for NordVPN and PureVPN
|
// for NordVPN and PureVPN
|
||||||
err := u.updateProvider(ctx, fetcher, minRatio)
|
err := u.updateProvider(ctx, fetcher, minRatio)
|
||||||
if err == nil {
|
switch {
|
||||||
|
case err == nil:
|
||||||
continue
|
continue
|
||||||
}
|
case errors.Is(err, common.ErrCredentialsMissing):
|
||||||
|
u.logger.Warn(err.Error() + " - skipping update for " + providerName)
|
||||||
// return the only error for the single provider.
|
continue
|
||||||
if len(providers) == 1 {
|
case len(providers) == 1:
|
||||||
|
// return the only error for the single provider.
|
||||||
return err
|
return err
|
||||||
|
case ctx.Err() != nil:
|
||||||
|
// stop updating other providers if context is done
|
||||||
|
return ctx.Err()
|
||||||
|
default: // error encountered updating one of multiple providers
|
||||||
|
// Log the error and continue updating the next provider.
|
||||||
|
u.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop updating the next providers if context is canceled.
|
|
||||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
||||||
return ctxErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the error and continue updating the next provider.
|
|
||||||
u.logger.Error(err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -32,9 +32,14 @@ func (w *Wireguard) addRoutes(link netlink.Link, destinations []netip.Prefix,
|
|||||||
func (w *Wireguard) addRoute(link netlink.Link, dst netip.Prefix,
|
func (w *Wireguard) addRoute(link netlink.Link, dst netip.Prefix,
|
||||||
firewallMark uint32,
|
firewallMark uint32,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
|
family := netlink.FamilyV4
|
||||||
|
if dst.Addr().Is6() {
|
||||||
|
family = netlink.FamilyV6
|
||||||
|
}
|
||||||
route := netlink.Route{
|
route := netlink.Route{
|
||||||
LinkIndex: link.Index,
|
LinkIndex: link.Index,
|
||||||
Dst: dst,
|
Dst: dst,
|
||||||
|
Family: family,
|
||||||
Table: int(firewallMark),
|
Table: int(firewallMark),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func Test_Wireguard_addRoute(t *testing.T) {
|
|||||||
expectedRoute: netlink.Route{
|
expectedRoute: netlink.Route{
|
||||||
LinkIndex: linkIndex,
|
LinkIndex: linkIndex,
|
||||||
Dst: ipPrefix,
|
Dst: ipPrefix,
|
||||||
|
Family: netlink.FamilyV4,
|
||||||
Table: firewallMark,
|
Table: firewallMark,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -49,6 +50,7 @@ func Test_Wireguard_addRoute(t *testing.T) {
|
|||||||
expectedRoute: netlink.Route{
|
expectedRoute: netlink.Route{
|
||||||
LinkIndex: linkIndex,
|
LinkIndex: linkIndex,
|
||||||
Dst: ipPrefix,
|
Dst: ipPrefix,
|
||||||
|
Family: netlink.FamilyV4,
|
||||||
Table: firewallMark,
|
Table: firewallMark,
|
||||||
},
|
},
|
||||||
routeAddErr: errDummy,
|
routeAddErr: errDummy,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package wireguard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/netlink"
|
"github.com/qdm12/gluetun/internal/netlink"
|
||||||
)
|
)
|
||||||
@@ -16,6 +17,10 @@ func (w *Wireguard) addRule(rulePriority int, firewallMark uint32,
|
|||||||
rule.Table = int(firewallMark)
|
rule.Table = int(firewallMark)
|
||||||
rule.Family = family
|
rule.Family = family
|
||||||
if err := w.netlink.RuleAdd(rule); err != nil {
|
if err := w.netlink.RuleAdd(rule); err != nil {
|
||||||
|
if strings.HasSuffix(err.Error(), "file exists") {
|
||||||
|
w.logger.Info("if you are using Kubernetes, this may fix the error below: " +
|
||||||
|
"https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/kubernetes.md#adding-ipv6-rule--file-exists")
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("adding %s: %w", rule, err)
|
return nil, fmt.Errorf("adding %s: %w", rule, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user