Compare commits

..

48 Commits

Author SHA1 Message Date
Quentin McGaw 3d1e20c555 hotfix(portforward): add missing break statements for log streaming 2026-07-30 11:49:18 +00:00
Quentin McGaw 44143aa3ba hotfix(ci): fix secrets access 2026-07-29 11:18:09 +00:00
Quentin McGaw 56e5cad66b fix(ci): update if conditions from qdm12/gluetun to passteque/gluetun 2026-07-29 06:26:21 +00:00
Matt Van Horn c0073c9567 fix(healthcheck): prevent race condition on the healthchecker (#3400)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Quentin McGaw <quentin.mcgaw@gmail.com>

Thanks also to @arturhoo for also trying to resolve this issue in #3410
2026-07-29 06:19:33 +00:00
Quentin McGaw cccd7c4c1b hotfix(command): fix unit test 2026-07-29 06:19:33 +00:00
Quentin McGaw 4ddf7c9a57 fix(command): wait for all stdout and stderr streams to complete correctly 2026-07-29 06:19:33 +00:00
ligistx a4df514e4b fix(providers/custom): use proto tcp-client instead of proto tcp (#3350) 2026-07-29 05:49:22 +00:00
Quentin McGaw 35847bb3b2 fix(portforwarding): handle empty ports without panicing 2026-07-29 05:49:20 +00:00
Quentin McGaw d36d0f84d7 fix(command): fix rare race condition on log line stream at command completion 2026-07-29 05:49:18 +00:00
Quentin McGaw e68b70ff8b fix(wireguard): skip tun device checks when using kernelspace 2026-07-29 05:49:16 +00:00
Immanuel Tikhonov 73c3ce23da fix(openvpn/extract): trim spaces in config lines before parsing (#3327) 2026-07-29 05:32:50 +00:00
Quentin McGaw 6857210429 fix(provider/pia): handle "port is busy" messages and retry port forwarding logic 2026-07-29 05:32:49 +00:00
Quentin McGaw 0f7f35d6dc fix(configuration/settings/wireguard): ignore empty address strings 2026-07-29 05:30:23 +00:00
Quentin McGaw ec71f1881b fix(provider/pia): update default OpenVPN ports
- 8080 for UDP
- 8443 for TCP
- According to https://github.com/pia-foss/manual-connections/commit/8a75e46be81583d17f9ab3570881419b35000969
- Credits to @darthShadow
2026-07-29 05:30:21 +00:00
Quentin McGaw 44f2df6f87 fix(configuration/sources/files/wireguard): fix parsing ipv6 endpoint port 2026-07-29 05:30:18 +00:00
Quentin McGaw 650c61e93c fix(pia): allow ports 501 and 502 as custom ports given they are the defaults 2026-07-29 05:30:16 +00:00
Quentin McGaw 2f98539848 hotfix(openvpn): fix support for tcp-client
- always use `proto tcp-client` when using TCP
- parses `tcp-client` (on top of `tcp`, `tcp4`, `tcp6`) as meaning TCP
- Fix #3302
2026-07-29 05:30:14 +00:00
Quentin McGaw 5c02e2106c hotfix(pia): fix servers data updater and update servers data
- use v7 API endpoint to get correct list of servers
- skip offline regions
- do not skip *.pvt.site
2026-07-29 05:30:12 +00:00
Quentin McGaw e6815054d9 fix(pia): ignore *.pvt.site regions 2026-07-29 05:30:10 +00:00
Quentin McGaw 9fe34266f0 fix(pia): try x.y.128.1 and x.y.0.1 from the gateway IP to find the API IP address 2026-07-29 05:30:08 +00:00
Quentin McGaw cdfd644c04 fix(portforward): no longer stuck after failed port forwarding 2026-07-29 05:30:06 +00:00
Quentin McGaw 1c722b0f80 hotfix(dns): fix pool panicing (again) 2026-07-29 05:30:04 +00:00
Quentin McGaw 2e204e11b6 hotfix(dns): fix race condition with DoT pool 2026-07-29 05:30:02 +00:00
Quentin McGaw c745f61156 fix(privateinternetaccess): use AES-GCM for all presets 2026-07-29 05:29:59 +00:00
Quentin McGaw 4301196c21 fix(privateinternetaccess): remove none encryption preset 2026-07-29 05:29:57 +00:00
Quentin McGaw ef48826e48 fix(dns): DNS over TLS pool behavior fixed
- handle timed out connections the same as closed connections
- close connection on TLS handshake failure
- improve mutex handling during connection renewal and retrieval
2026-07-29 05:29:55 +00:00
Quentin McGaw 3fca8b3dfe fix(proton): fix updater code
- simplest fix ever
- proton: how can you return such obscure error messages
- ai: you suck hard at fixing anything still it's embarassing
2026-07-29 05:29:53 +00:00
Drew Wells 8ab97bb4b4 fix(wireguard): support IPv6 address formatting from config files (#3273) 2026-07-29 05:29:51 +00:00
Quentin McGaw 8269fad3b2 fix(server/auth): return 404 or 405 depending on route
- Fix #3275
2026-07-29 05:29:49 +00:00
Damoon Tahmasbi 16870d6385 fix(vyprvpn/updater): update OpenVPN configs zip URL (#3264) 2026-07-29 05:28:32 +00:00
Quentin McGaw d41081f4b3 fix(server/portforward): use port and ports for both single port and multiple ports forwarded 2026-07-29 05:28:30 +00:00
Michael Bisbjerg 5fbe277f9c fix(openvpn): bundle provider CA certificates in one block (#3258) 2026-07-29 05:28:28 +00:00
Quentin McGaw c5b20a3b06 fix(firewall/iptables): shared mutex for both iptables and ip6tables 2026-07-29 05:28:27 +00:00
Quentin McGaw 1b871496ea fix(ci): set hash of PR commit instead of synthetic commit in docker build argument 2026-07-29 05:24:31 +00:00
Quentin McGaw 05795ad2d1 fix(dns): correct error wrapping for DNS listening address validation
- Fix #3216
2026-07-29 05:24:27 +00:00
Quentin McGaw f9a86f48aa fix(updater): only uses DoH to cloudflare+google
- prevent dns plaintext manipulation both the periodic update and when running in cli mode
- possibly higher reliability on poor connections versus UDP
- drop `-dns` flag in update command
- for now no configuration allowed since it makes everything rather complex
2026-07-29 05:24:03 +00:00
Quentin McGaw 3a39236fa8 fix(expressvpn): remove pakistan server 2026-07-29 05:24:01 +00:00
github-actions[bot] 998ae46e97 feat(airvpn): update servers data (#3186) 2026-07-29 05:23:59 +00:00
Dennis Nienhuis c4e65f8cc7 fix(expressvpn): fix missing characters in CA string (#3192) 2026-07-29 05:23:57 +00:00
Dennis Nienhuis a20795df9e fix(expressvpn): add new CA3 certificate to fix TLS handshake failure (#3184) 2026-07-29 05:22:40 +00:00
Quentin McGaw 78929cd6e1 fix(openvpn/extract): restrict custom openvpn config protocol to tcp or udp internally
- Fix #3179
- I believe specifying tcp4, tcp6 or tcp-client does not change anything versus tcp + remote ip address
- I believe specifying udp4 or udp6 does not change anything versus tcp + remote ip address
- Simplify firewall code to not account for tcp-client etc.
2026-07-29 05:22:38 +00:00
Quentin McGaw 75c2404179 fix(privado): allow additional OpenVPN ports 443, 8080 and 8443 for both tcp and udp 2026-07-29 05:22:36 +00:00
Quentin McGaw bcbc8c41f2 fix(privado): allow OpenVPN TCP protocol 2026-07-29 05:22:34 +00:00
Quentin McGaw 3da0c21c82 fix(mod): probe searches for features built-in the kernel 2026-07-29 05:22:32 +00:00
Quentin McGaw 009b8e45b7 fix(privado): update servers data using JSON API
- Fixes #3159
- Fixes #2118
- Fixes #2657
2026-07-29 05:22:29 +00:00
Quentin McGaw 05dc21ed61 fix(all): increase global http client timeout to 35s and precise lower timeouts where needed
- Fix DNS blocklists slow downloads, fix #3102
- Leave 35s timeout for updaters
- Set timeouts to 1s for local calls
- Set timeouts to 5s for LAN VPN calls and small external calls
- Set timeouts to 10s external VPN API calls
2026-07-29 05:22:27 +00:00
Quentin McGaw 270dce9acb fix(dns): skip blocking if block lists download fails 2026-07-29 05:22:25 +00:00
Quentin McGaw 23cf3231f0 fix(healthcheck): corret behavior when HEALTH_RESTART_VPN=off and startup check fails 2026-07-29 05:16:23 +00:00
85 changed files with 10032 additions and 7606 deletions
+9 -4
View File
@@ -68,7 +68,7 @@ jobs:
verify-private: verify-private:
if: | if: |
github.repository == 'qdm12/gluetun' && github.repository == 'passteque/gluetun' &&
( (
github.event_name == 'push' || github.event_name == 'push' ||
github.event_name == 'release' || github.event_name == 'release' ||
@@ -115,7 +115,7 @@ jobs:
publish: publish:
if: | if: |
github.repository == 'qdm12/gluetun' && github.repository == 'passteque/gluetun' &&
( (
github.event_name == 'push' || github.event_name == 'push' ||
github.event_name == 'release' || github.event_name == 'release' ||
@@ -127,6 +127,7 @@ jobs:
contents: read contents: read
packages: write packages: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: secrets
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -161,11 +162,15 @@ jobs:
with: with:
registry: ghcr.io registry: ghcr.io
username: qdm12 username: qdm12
password: ${{ github.token }} password: ${{ secrets.GHCR_PAT }}
- name: Short commit - name: Short commit
id: shortcommit id: shortcommit
run: echo "::set-output name=value::$(git rev-parse --short HEAD)" run: |
# Use the PR head SHA if it exists, otherwise fallback to GITHUB_SHA
FULL_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
SHORT_SHA=
echo "value=$(echo $FULL_SHA | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Build and push final image - name: Build and push final image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
config-file: .github/workflows/configs/mlc-config.json config-file: .github/workflows/configs/mlc-config.json
- uses: peter-evans/dockerhub-description@v4 - uses: peter-evans/dockerhub-description@v4
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push' if: github.repository == 'passteque/gluetun' && github.event_name == 'push'
with: with:
username: qmcgaw username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
+14 -24
View File
@@ -15,6 +15,8 @@ import (
_ "time/tzdata" _ "time/tzdata"
_ "github.com/breml/rootcerts" _ "github.com/breml/rootcerts"
"github.com/qdm12/dns/v2/pkg/doh"
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/alpine" "github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/cli" "github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/command" "github.com/qdm12/gluetun/internal/command"
@@ -39,7 +41,6 @@ import (
"github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun"
updater "github.com/qdm12/gluetun/internal/updater/loop" updater "github.com/qdm12/gluetun/internal/updater/loop"
"github.com/qdm12/gluetun/internal/updater/resolver" "github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip" "github.com/qdm12/gluetun/internal/updater/unzip"
@@ -76,7 +77,6 @@ func main() {
logger := log.New(log.SetLevel(log.LevelInfo)) logger := log.New(log.SetLevel(log.LevelInfo))
args := os.Args args := os.Args
tun := tun.New()
netLinkDebugLogger := logger.New(log.SetComponent("netlink")) netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
netLinker := netlink.New(netLinkDebugLogger) netLinker := netlink.New(netLinkDebugLogger)
cli := cli.New() cli := cli.New()
@@ -96,7 +96,7 @@ func main() {
errorCh := make(chan error) errorCh := make(chan error)
go func() { go func() {
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli) errorCh <- _main(ctx, buildInfo, args, logger, reader, netLinker, cmder, cli)
}() }()
// Wait for OS signal or run error // Wait for OS signal or run error
@@ -143,7 +143,7 @@ var errCommandUnknown = errors.New("command is unknown")
//nolint:gocognit,gocyclo,maintidx //nolint:gocognit,gocyclo,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation, func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger log.LoggerInterface, reader *reader.Reader, args []string, logger log.LoggerInterface, reader *reader.Reader,
tun Tun, netLinker netLinker, cmder RunStarter, netLinker netLinker, cmder RunStarter,
cli clier, cli clier,
) error { ) error {
if len(args) > 1 { // cli operation if len(args) > 1 { // cli operation
@@ -263,7 +263,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID) puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
const clientTimeout = 15 * time.Second const clientTimeout = 35 * time.Second
httpClient := &http.Client{Timeout: clientTimeout} httpClient := &http.Client{Timeout: clientTimeout}
// Create configurators // Create configurators
alpineConf := alpine.New() alpineConf := alpine.New()
@@ -335,19 +335,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return fmt.Errorf("adding local rules: %w", err) return fmt.Errorf("adding local rules: %w", err)
} }
const tunDevice = "/dev/net/tun"
err = tun.Check(tunDevice)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
}
logger.Info(err.Error() + "; creating it...")
err = tun.Create(tunDevice)
if err != nil {
return fmt.Errorf("creating tun device: %w", err)
}
}
for _, port := range allSettings.Firewall.InputPorts { for _, port := range allSettings.Firewall.InputPorts {
for _, defaultRoute := range defaultRoutes { for _, defaultRoute := range defaultRoutes {
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface) err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
@@ -427,10 +414,18 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go healthcheckServer.Run(healthServerCtx, healthServerDone) go healthcheckServer.Run(healthServerCtx, healthServerDone)
healthChecker := healthcheck.NewChecker(healthLogger) healthChecker := healthcheck.NewChecker(healthLogger)
// Note: we use a separate DoH dialer for the VPN servers data updater, separate from the
// main DNS local server to make sure no request is blocked by filters.
dohDialer, err := doh.New(doh.Settings{
UpstreamResolvers: []dnsprovider.Provider{dnsprovider.Cloudflare(), dnsprovider.Google()},
})
if err != nil {
return fmt.Errorf("creating updater DoH dialer: %w", err)
}
updaterLogger := logger.New(log.SetComponent("updater")) updaterLogger := logger.New(log.SetComponent("updater"))
unzipper := unzip.New(httpClient) unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress) parallelResolver := resolver.NewParallelResolver(dohDialer)
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(), httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
@@ -596,11 +591,6 @@ type clier interface {
GenKey(args []string) error GenKey(args []string) error
} }
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
}
type RunStarter interface { type RunStarter interface {
Run(cmd *exec.Cmd) (output string, err error) Run(cmd *exec.Cmd) (output string, err error)
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string, Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/klauspost/compress v1.18.1 github.com/klauspost/compress v1.18.1
github.com/klauspost/pgzip v1.2.6 github.com/klauspost/pgzip v1.2.6
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc10 github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
github.com/qdm12/gosettings v0.4.4 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
+2 -2
View File
@@ -69,8 +69,8 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc10 h1:IyeNEYXfhBsaE1dwxx5eAqdAz1HS98dT+8c7xoKODa0= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
github.com/qdm12/dns/v2 v2.0.0-rc10/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4= github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
+20 -3
View File
@@ -10,6 +10,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/qdm12/dns/v2/pkg/doh"
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
@@ -38,12 +40,12 @@ 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, protonUsername, protonEmail, protonPassword string var dnsServer, 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,
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)") "Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use") flagSet.StringVar(&dnsServer, "dns", "", "no longer used, your DNS will use DoH with Cloudflare and Google")
const defaultMinRatio = 0.8 const defaultMinRatio = 0.8
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio, flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
"Minimum ratio of servers to find for the update to succeed") "Minimum ratio of servers to find for the update to succeed")
@@ -58,6 +60,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return err return err
} }
if dnsServer != "" {
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
}
if !endUserMode && !maintainerMode { if !endUserMode && !maintainerMode {
return fmt.Errorf("%w", ErrModeUnspecified) return fmt.Errorf("%w", ErrModeUnspecified)
} }
@@ -97,10 +103,21 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return fmt.Errorf("creating servers storage: %w", err) return fmt.Errorf("creating servers storage: %w", err)
} }
dohSettings := doh.Settings{
UpstreamResolvers: []dnsprovider.Provider{
dnsprovider.Cloudflare(),
dnsprovider.Google(),
},
}
dnsDialer, err := doh.New(dohSettings)
if err != nil {
return fmt.Errorf("creating DoH dialer: %w", err)
}
const clientTimeout = 10 * time.Second const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout} httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient) unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(options.DNSAddress) parallelResolver := resolver.NewParallelResolver(dnsDialer)
nameTokenPairs := []api.NameToken{ nameTokenPairs := []api.NameToken{
{Name: string(api.IPInfo), Token: ipToken}, {Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)}, {Name: string(api.IP2Location)},
+18 -21
View File
@@ -9,8 +9,9 @@ import (
) )
// Start launches a command and streams stdout and stderr to channels. // Start launches a command and streams stdout and stderr to channels.
// All the channels returned are ready only and won't be closed // stdoutLines and stderrLines channels will be closed when there is no more
// if the command fails later. // output to read, in order for the caller to catch all lines even after the
// command has finished. The waitError channel returned will never be closed.
func (c *Cmder) Start(cmd *exec.Cmd) ( func (c *Cmder) Start(cmd *exec.Cmd) (
stdoutLines, stderrLines <-chan string, stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error, waitError <-chan error, startErr error,
@@ -21,7 +22,6 @@ func (c *Cmder) Start(cmd *exec.Cmd) (
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string, func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error, waitError <-chan error, startErr error,
) { ) {
stop := make(chan struct{})
stdoutReady := make(chan struct{}) stdoutReady := make(chan struct{})
stdoutLinesCh := make(chan string) stdoutLinesCh := make(chan string)
stdoutDone := make(chan struct{}) stdoutDone := make(chan struct{})
@@ -33,43 +33,47 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh) go streamToChannel(stdoutReady, stdoutDone, stdout, stdoutLinesCh)
stderr, err := cmd.StderrPipe() stderr, err := cmd.StderrPipe()
if err != nil { if err != nil {
_ = stdout.Close() _ = stdout.Close()
close(stop)
<-stdoutDone <-stdoutDone
close(stdoutLinesCh)
return nil, nil, nil, err return nil, nil, nil, err
} }
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh) go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
_ = stdout.Close() _ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone <-stdoutDone
close(stdoutLinesCh)
_ = stderr.Close()
<-stderrDone <-stderrDone
close(stderrLinesCh)
return nil, nil, nil, err return nil, nil, nil, err
} }
waitErrorCh := make(chan error) waitErrorCh := make(chan error)
go func() { go func() {
err := cmd.Wait() err := cmd.Wait()
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone <-stdoutDone
close(stdoutLinesCh)
_ = stdout.Close()
<-stderrDone <-stderrDone
close(stderrLinesCh)
_ = stderr.Close()
waitErrorCh <- err waitErrorCh <- err
}() }()
<-stdoutReady
<-stderrReady
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
} }
func streamToChannel(ready chan<- struct{}, func streamToChannel(ready chan<- struct{}, done chan<- struct{},
stop <-chan struct{}, done chan<- struct{},
stream io.Reader, lines chan<- string, stream io.Reader, lines chan<- string,
) { ) {
defer close(done) defer close(done)
@@ -89,12 +93,5 @@ func streamToChannel(ready chan<- struct{},
if err == nil || errors.Is(err, os.ErrClosed) { if err == nil || errors.Is(err, os.ErrClosed) {
return return
} }
lines <- "stream error: " + err.Error()
// ignore the error if it is stopped.
select {
case <-stop:
return
default:
lines <- "stream error: " + err.Error()
}
} }
+42 -24
View File
@@ -89,30 +89,48 @@ func Test_start(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var stdoutIndex, stderrIndex int collectAndCheckChannels(t, stdoutLines, stderrLines, waitError,
testCase.stdout, testCase.stderr, testCase.waitErr)
done := false
for !done {
select {
case line := <-stdoutLines:
assert.Equal(t, testCase.stdout[stdoutIndex], line)
stdoutIndex++
case line := <-stderrLines:
assert.Equal(t, testCase.stderr[stderrIndex], line)
stderrIndex++
case err := <-waitError:
if testCase.waitErr != nil {
require.Error(t, err)
assert.Equal(t, testCase.waitErr.Error(), err.Error())
} else {
assert.NoError(t, err)
}
done = true
}
}
assert.Equal(t, len(testCase.stdout), stdoutIndex)
assert.Equal(t, len(testCase.stderr), stderrIndex)
}) })
} }
} }
func collectAndCheckChannels(t *testing.T, stdoutLines, stderrLines <-chan string,
waitError <-chan error, expectedStdout, expectedStderr []string, expectedWaitErr error,
) {
t.Helper()
stdoutIndex := 0
stderrIndex := 0
done := false
for !done {
select {
case line, ok := <-stdoutLines:
if !ok {
stdoutLines = nil
continue
}
assert.Equal(t, expectedStdout[stdoutIndex], line)
stdoutIndex++
case line, ok := <-stderrLines:
if !ok {
stderrLines = nil
continue
}
assert.Equal(t, expectedStderr[stderrIndex], line)
stderrIndex++
case err := <-waitError:
if expectedWaitErr != nil {
require.Error(t, err)
assert.Equal(t, expectedWaitErr.Error(), err.Error())
} else {
assert.NoError(t, err)
}
done = true
}
}
assert.Equal(t, len(expectedStdout), stdoutIndex)
assert.Equal(t, len(expectedStderr), stderrIndex)
}
@@ -60,7 +60,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
providers.Giganews, providers.Giganews,
providers.Ipvanish, providers.Ipvanish,
providers.Perfectprivacy, providers.Perfectprivacy,
providers.Privado,
providers.Vyprvpn, providers.Vyprvpn,
) { ) {
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
@@ -75,8 +74,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
providers.Privatevpn, providers.Torguard: providers.Privatevpn, providers.Torguard:
// no custom port allowed // no custom port allowed
case providers.Expressvpn, providers.Fastestvpn, case providers.Expressvpn, providers.Fastestvpn,
providers.Giganews, providers.Ipvanish, providers.Nordvpn, providers.Giganews, providers.Ipvanish,
providers.Privado, providers.Purevpn, providers.Nordvpn, providers.Purevpn,
providers.Surfshark, providers.VPNSecure, providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn: providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
@@ -99,8 +98,11 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
case providers.Perfectprivacy: case providers.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433} allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433} allowedUDP = []uint16{44, 443, 4433}
case providers.Privado:
allowedTCP = []uint16{443, 1194, 8080, 8443}
allowedUDP = []uint16{443, 1194, 8080, 8443}
case providers.PrivateInternetAccess: case providers.PrivateInternetAccess:
allowedTCP = []uint16{80, 110, 443} allowedTCP = []uint16{80, 110, 443, 501, 502, 8443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201} allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn: case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443} allowedTCP = []uint16{443, 5995, 8443}
@@ -130,7 +132,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate EncPreset // Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess { if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{ validEncryptionPresets := []string{
presets.None,
presets.Normal, presets.Normal,
presets.Strong, presets.Strong,
} }
@@ -21,10 +21,6 @@ type Updater struct {
// updater. It cannot be nil in the internal state. // updater. It cannot be nil in the internal state.
// TODO change to value and add Enabled field. // TODO change to value and add Enabled field.
Period *time.Duration Period *time.Duration
// DNSAddress is the DNS server address to use
// to resolve VPN server hostnames to IP addresses.
// It cannot be the empty string in the internal state.
DNSAddress string
// MinRatio is the minimum ratio of servers to // MinRatio is the minimum ratio of servers to
// find per provider, compared to the total current // find per provider, compared to the total current
// number of servers. It defaults to 0.8. // number of servers. It defaults to 0.8.
@@ -76,7 +72,6 @@ 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,
MinRatio: u.MinRatio, MinRatio: u.MinRatio,
Providers: gosettings.CopySlice(u.Providers), Providers: gosettings.CopySlice(u.Providers),
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail), ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
@@ -89,7 +84,6 @@ func (u *Updater) copy() (copied Updater) {
// settings. // settings.
func (u *Updater) overrideWith(other Updater) { func (u *Updater) overrideWith(other Updater) {
u.Period = gosettings.OverrideWithPointer(u.Period, other.Period) u.Period = gosettings.OverrideWithPointer(u.Period, other.Period)
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.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
@@ -98,7 +92,6 @@ func (u *Updater) overrideWith(other Updater) {
func (u *Updater) SetDefaults(vpnProvider string) { func (u *Updater) SetDefaults(vpnProvider string) {
u.Period = gosettings.DefaultPointer(u.Period, 0) u.Period = gosettings.DefaultPointer(u.Period, 0)
u.DNSAddress = gosettings.DefaultComparable(u.DNSAddress, "1.1.1.1:53")
if u.MinRatio == 0 { if u.MinRatio == 0 {
const defaultMinRatio = 0.8 const defaultMinRatio = 0.8
@@ -125,7 +118,6 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server data updater settings:") node = gotree.New("Server data updater settings:")
node.Appendf("Update period: %s", *u.Period) node.Appendf("Update period: %s", *u.Period)
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) { if slices.Contains(u.Providers, providers.Protonvpn) {
@@ -142,11 +134,6 @@ func (u *Updater) read(r *reader.Reader) (err error) {
return err return err
} }
u.DNSAddress, err = readUpdaterDNSAddress()
if err != nil {
return err
}
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO") u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
if err != nil { if err != nil {
return err return err
@@ -166,12 +153,3 @@ func (u *Updater) read(r *reader.Reader) (err error) {
return nil return nil
} }
func readUpdaterDNSAddress() (address string, err error) {
// TODO this is currently using Cloudflare in
// plaintext to not be blocked by DNS over TLS by default.
// If a plaintext address is set in the DNS settings, this one will be used.
// use custom future encrypted DNS written in Go without blocking
// as it's too much trouble to start another parallel unbound instance for now.
return "", nil
}
+4 -2
View File
@@ -251,10 +251,12 @@ func (w *Wireguard) read(r *reader.Reader) (err error) {
// WARNING: do not initialize w.Addresses to an empty slice // WARNING: do not initialize w.Addresses to an empty slice
// or the defaults for nordvpn will not work. // or the defaults for nordvpn will not work.
for _, addressString := range addressStrings { for _, addressString := range addressStrings {
if !strings.ContainsRune(addressString, '/') { addressString = strings.TrimSpace(addressString)
if addressString == "" {
continue
} else if !strings.ContainsRune(addressString, '/') {
addressString += "/32" addressString += "/32"
} }
addressString = strings.TrimSpace(addressString)
address, err := netip.ParsePrefix(addressString) address, err := netip.ParsePrefix(addressString)
if err != nil { if err != nil {
return fmt.Errorf("parsing address: %w", err) return fmt.Errorf("parsing address: %w", err)
@@ -3,10 +3,10 @@ package files
import ( import (
"errors" "errors"
"fmt" "fmt"
"net"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
) )
@@ -82,12 +82,12 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
publicKey = getINIKeyFromSection(peerSection, "PublicKey") publicKey = getINIKeyFromSection(peerSection, "PublicKey")
endpoint := getINIKeyFromSection(peerSection, "Endpoint") endpoint := getINIKeyFromSection(peerSection, "Endpoint")
if endpoint != nil { if endpoint != nil {
parts := strings.Split(*endpoint, ":") host, port, err := net.SplitHostPort(*endpoint)
endpointIP = &parts[0] if err == nil {
const partsWithPort = 2 endpointIP = &host
if len(parts) >= partsWithPort { endpointPort = &port
endpointPort = new(string) } else {
*endpointPort = strings.Join(parts[1:], ":") endpointIP = endpoint
} }
} }
@@ -179,6 +179,12 @@ Endpoint = 1.2.3.4:51820`,
endpointIP: ptrTo("1.2.3.4"), endpointIP: ptrTo("1.2.3.4"),
endpointPort: ptrTo("51820"), endpointPort: ptrTo("51820"),
}, },
"ipv6_endpoint": {
iniData: `[Peer]
Endpoint = [2a02:bbbb:aaaa:8075::10]:51820`,
endpointIP: ptrTo("2a02:bbbb:aaaa:8075::10"),
endpointPort: ptrTo("51820"),
},
} }
for testName, testCase := range testCases { for testName, testCase := range testCases {
-6
View File
@@ -2,7 +2,6 @@ package dns
import ( import (
"context" "context"
"errors"
"github.com/qdm12/dns/v2/pkg/nameserver" "github.com/qdm12/dns/v2/pkg/nameserver"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
@@ -53,11 +52,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
if ctx.Err() != nil { if ctx.Err() != nil {
return return
} }
if !errors.Is(err, errUpdateBlockLists) {
const fallback = true
l.useUnencryptedDNS(fallback)
}
l.logAndWait(ctx, err) l.logAndWait(ctx, err)
settings = l.GetSettings() settings = l.GetSettings()
} }
+10 -6
View File
@@ -2,24 +2,28 @@ package dns
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/netip" "net/netip"
"github.com/qdm12/dns/v2/pkg/check" "github.com/qdm12/dns/v2/pkg/check"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
"github.com/qdm12/dns/v2/pkg/nameserver" "github.com/qdm12/dns/v2/pkg/nameserver"
"github.com/qdm12/dns/v2/pkg/server" "github.com/qdm12/dns/v2/pkg/server"
) )
var errUpdateBlockLists = errors.New("cannot update filter block lists")
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) { func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) {
err = l.updateFiles(ctx) settings := l.GetSettings()
var updateSettings update.Settings
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
err = l.filter.Update(updateSettings)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: %w", errUpdateBlockLists, err) return nil, fmt.Errorf("updating filter for rebinding protection: %w", err)
} }
settings := l.GetSettings() err = l.updateFiles(ctx, settings)
if err != nil {
l.logger.Warn("downloading block lists failed, skipping: " + err.Error())
}
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger) serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
if err != nil { if err != nil {
+2 -2
View File
@@ -30,8 +30,9 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
lastTick = l.timeNow() lastTick = l.timeNow()
status := l.GetStatus() status := l.GetStatus()
settings := l.GetSettings()
if status == constants.Running { if status == constants.Running {
if err := l.updateFiles(ctx); err != nil { if err := l.updateFiles(ctx, settings); err != nil {
l.statusManager.SetStatus(constants.Crashed) l.statusManager.SetStatus(constants.Crashed)
l.logger.Error(err.Error()) l.logger.Error(err.Error())
l.logger.Warn("skipping DNS server restart due to failed files update") l.logger.Warn("skipping DNS server restart due to failed files update")
@@ -44,7 +45,6 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
_, _ = l.statusManager.ApplyStatus(ctx, constants.Stopped) _, _ = l.statusManager.ApplyStatus(ctx, constants.Stopped)
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running) _, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
settings := l.GetSettings()
timer.Reset(*settings.UpdatePeriod) timer.Reset(*settings.UpdatePeriod)
case <-l.updateTicker: case <-l.updateTicker:
if !timer.Stop() { if !timer.Stop() {
+2 -4
View File
@@ -6,11 +6,10 @@ import (
"github.com/qdm12/dns/v2/pkg/blockbuilder" "github.com/qdm12/dns/v2/pkg/blockbuilder"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update" "github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
"github.com/qdm12/gluetun/internal/configuration/settings"
) )
func (l *Loop) updateFiles(ctx context.Context) (err error) { func (l *Loop) updateFiles(ctx context.Context, settings settings.DNS) (err error) {
settings := l.GetSettings()
l.logger.Info("downloading hostnames and IP block lists") l.logger.Info("downloading hostnames and IP block lists")
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client) blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
@@ -37,7 +36,6 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
IPPrefixes: result.BlockedIPPrefixes, IPPrefixes: result.BlockedIPPrefixes,
} }
updateSettings.BlockHostnames(result.BlockedHostnames) updateSettings.BlockHostnames(result.BlockedHostnames)
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
err = l.filter.Update(updateSettings) err = l.filter.Update(updateSettings)
if err != nil { if err != nil {
return fmt.Errorf("updating filter: %w", err) return fmt.Errorf("updating filter: %w", err)
+5 -6
View File
@@ -10,12 +10,11 @@ import (
) )
type Config struct { type Config struct {
runner CmdRunner runner CmdRunner
logger Logger logger Logger
iptablesMutex sync.Mutex iptablesMutex sync.Mutex
ip6tablesMutex sync.Mutex defaultRoutes []routing.DefaultRoute
defaultRoutes []routing.DefaultRoute localNetworks []routing.LocalNetwork
localNetworks []routing.LocalNetwork
// Fixed state // Fixed state
ipTables string ipTables string
+2 -2
View File
@@ -36,8 +36,8 @@ func (c *Config) runIP6tablesInstruction(ctx context.Context, instruction string
if c.ip6Tables == "" { if c.ip6Tables == "" {
return nil return nil
} }
c.ip6tablesMutex.Lock() // only one ip6tables command at once c.iptablesMutex.Lock() // only one ip6tables command at once
defer c.ip6tablesMutex.Unlock() defer c.iptablesMutex.Unlock()
if isDeleteMatchInstruction(instruction) { if isDeleteMatchInstruction(instruction) {
return deleteIPTablesRule(ctx, c.ip6Tables, instruction, return deleteIPTablesRule(ctx, c.ip6Tables, instruction,
+3 -3
View File
@@ -148,9 +148,6 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
defaultInterface string, connection models.Connection, remove bool, defaultInterface string, connection models.Connection, remove bool,
) error { ) error {
protocol := connection.Protocol protocol := connection.Protocol
if protocol == "tcp-client" {
protocol = "tcp"
}
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT", instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
appendOrDelete(remove), connection.IP, defaultInterface, protocol, appendOrDelete(remove), connection.IP, defaultInterface, protocol,
protocol, connection.Port) protocol, connection.Port)
@@ -218,6 +215,9 @@ func (c *Config) redirectPort(ctx context.Context, intf string,
interfaceFlag = "" interfaceFlag = ""
} }
c.iptablesMutex.Lock()
defer c.iptablesMutex.Unlock()
err = c.runIptablesInstructions(ctx, []string{ err = c.runIptablesInstructions(ctx, []string{
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d", fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort), appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
+37 -17
View File
@@ -8,7 +8,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"strings" "strings"
"sync"
"time" "time"
"github.com/qdm12/gluetun/internal/healthcheck/dns" "github.com/qdm12/gluetun/internal/healthcheck/dns"
@@ -23,7 +22,7 @@ type Checker struct {
logger Logger logger Logger
icmpTargetIPs []netip.Addr icmpTargetIPs []netip.Addr
smallCheckType string smallCheckType string
configMutex sync.Mutex startupOnFail bool
icmpNotPermitted *bool icmpNotPermitted *bool
@@ -45,26 +44,41 @@ func NewChecker(logger Logger) *Checker {
} }
} }
// SetConfig sets the TCP+TLS dial addresses, the ICMP echo IP address // SetConfig sets the following:
// to target and the desired small check type (dns or icmp). // - TCP+TLS dial addresses
// - ICMP echo IP addresses to target
// - the desired small check type (dns or icmp)
// - whether to startup the periodic checks if the startup check fails.
// This function MUST be called before calling [Checker.Start]. // This function MUST be called before calling [Checker.Start].
func (c *Checker) SetConfig(tlsDialAddrs []string, icmpTargets []netip.Addr, func (c *Checker) SetConfig(tlsDialAddrs []string, icmpTargets []netip.Addr,
smallCheckType string, smallCheckType string, startupOnFail bool,
) { ) {
c.configMutex.Lock()
defer c.configMutex.Unlock()
c.tlsDialAddrs = tlsDialAddrs c.tlsDialAddrs = tlsDialAddrs
c.icmpTargetIPs = icmpTargets c.icmpTargetIPs = icmpTargets
c.smallCheckType = smallCheckType c.smallCheckType = smallCheckType
c.startupOnFail = startupOnFail
} }
// Start starts the checker by first running a blocking 6s-timed TCP+TLS check, // Start starts the [Checker] which behaves differently according to its
// and, on success, starts the periodic checks in a separate goroutine: // internal field startupOnFail, which is set by calling [Checker.SetConfig].
//
// By default, startupOnFail should be false and the behavior is as follows:
// A blocking 6s-timed TCP+TLS check is performed first. If it fails,
// an error is returned and the [Checker] is not started.
// On success, it starts the periodic checks in a separate goroutine, returning
// the runError error channel and a nil error.
//
// If startupOnFail is true, the behavior is as follows:
// A blocking 6s-timed TCP+TLS check is performed first. If it fails,
// the error is sent to the runError channel, but no error is returned
// and the [Checker] continues to start the periodic checks in a separate goroutine, returning
// the runError error channel and a nil error.
//
// The periodic checks consist in:
// - a "small" ICMP echo check every minute // - a "small" ICMP echo check every minute
// - a "full" TCP+TLS check every 5 minutes // - a "full" TCP+TLS check every 5 minutes
// It returns a channel `runError` that receives an error (nil or not) when a periodic check is performed. //
// It returns an error if the initial TCP+TLS check fails. // The [Checker] has to be ultimately stopped by calling [Checker.Stop].
// The Checker has to be ultimately stopped by calling [Checker.Stop].
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) { func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
if len(c.tlsDialAddrs) == 0 || len(c.icmpTargetIPs) == 0 || c.smallCheckType == "" { if len(c.tlsDialAddrs) == 0 || len(c.icmpTargetIPs) == 0 || c.smallCheckType == "" {
panic("call Checker.SetConfig with non empty values before Checker.Start") panic("call Checker.SetConfig with non empty values before Checker.Start")
@@ -76,9 +90,19 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
} }
c.echoer.Reset() c.echoer.Reset()
// runErrorCh MUST be buffered in the case startupOnFail is true, and
// a startup error was encountered, to avoid blocking the startup
// goroutine when sending the error, especially since the caller may
// not be ready to receive from the channel yet.
runErrorCh := make(chan error, 1)
runError = runErrorCh
err = c.startupCheck(ctx) err = c.startupCheck(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("startup check: %w", err) err = fmt.Errorf("startup check: %w", err)
if !c.startupOnFail {
return nil, err
}
runErrorCh <- err
} }
ready := make(chan struct{}) ready := make(chan struct{})
@@ -90,8 +114,6 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
smallCheckTimer := time.NewTimer(smallCheckPeriod) smallCheckTimer := time.NewTimer(smallCheckPeriod)
const fullCheckPeriod = 5 * time.Minute const fullCheckPeriod = 5 * time.Minute
fullCheckTimer := time.NewTimer(fullCheckPeriod) fullCheckTimer := time.NewTimer(fullCheckPeriod)
runErrorCh := make(chan error)
runError = runErrorCh
go func() { go func() {
defer close(done) defer close(done)
close(ready) close(ready)
@@ -140,10 +162,8 @@ func (c *Checker) Stop() error {
} }
func (c *Checker) smallPeriodicCheck(ctx context.Context) error { func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
c.configMutex.Lock()
icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs)) icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs))
copy(icmpTargetIPs, c.icmpTargetIPs) copy(icmpTargetIPs, c.icmpTargetIPs)
c.configMutex.Unlock()
tryTimeouts := []time.Duration{ tryTimeouts := []time.Duration{
5 * time.Second, 5 * time.Second,
5 * time.Second, 5 * time.Second,
+4
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"time"
) )
var ErrHTTPStatusNotOK = errors.New("HTTP response status is not OK") var ErrHTTPStatusNotOK = errors.New("HTTP response status is not OK")
@@ -21,6 +22,9 @@ func NewClient(httpClient *http.Client) *Client {
} }
func (c *Client) Check(ctx context.Context, url string) error { func (c *Client) Check(ctx context.Context, url string) error {
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
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 err return err
+33
View File
@@ -0,0 +1,33 @@
package mod
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
var errBuiltinModuleNotFound = errors.New("builtin module not found")
func checkModulesBuiltin(modulesPath, moduleName string) error {
f, err := os.Open(filepath.Join(modulesPath, "modules.builtin"))
if err != nil {
return err
}
defer f.Close()
moduleName = strings.TrimSuffix(moduleName, ".ko")
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSuffix(line, ".ko")
if strings.HasSuffix(line, "/"+moduleName) {
return nil
}
}
return fmt.Errorf("%w: %s", errBuiltinModuleNotFound, moduleName)
}
+132
View File
@@ -0,0 +1,132 @@
package mod
import (
"bufio"
"compress/gzip"
"errors"
"fmt"
"os"
"strings"
)
var (
errModuleNameUnknown = errors.New("unknown module name")
errKernelFeatureIsModule = errors.New("kernel feature is a module, not built-in")
errKernelFeatureNotSet = errors.New("kernel feature not set")
errKernelFeatureNotFound = errors.New("kernel feature not found")
)
// checkProcConfig checks /proc/config.gz for a the kernel feature corresponding
// to the given module name. If the kernel feature is found and set to "y", it returns nil.
// If the kernel feature is found and set to "m", it returns an error indicating that the kernel
// feature is a module, not built-in.
// If the kernel feature is found and not set, it returns an error indicating that the kernel
// feature is not set. If the kernel feature is not found, it returns an error indicating that the kernel
// feature is not found.
func checkProcConfig(moduleName string) error {
f, err := os.Open("/proc/config.gz")
if err != nil {
return err
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
return fmt.Errorf("creating gzip reader: %w", err)
}
defer gz.Close()
// If any group of kernel features is satisfied, then the module is considered supported.
kernelFeatureGroups, ok := moduleNameToKernelFeatureGroups(moduleName)
if !ok {
return fmt.Errorf("%w: %s", errModuleNameUnknown, moduleName)
}
groups := make([]map[string]bool, len(kernelFeatureGroups))
for i, group := range kernelFeatureGroups {
featureToOK := make(map[string]bool)
for _, feature := range group {
featureToOK[feature] = false
}
groups[i] = featureToOK
}
scanner := bufio.NewScanner(gz)
for scanner.Scan() {
line := scanner.Text()
for _, featureToOK := range groups {
for name, ok := range featureToOK {
switch {
case ok:
case strings.HasPrefix(line, name+"=m"):
return fmt.Errorf("%w: %s", errKernelFeatureIsModule, name)
case strings.HasPrefix(line, name+"=y"):
featureToOK[name] = true
if allFeaturesOK(featureToOK) {
return nil
}
case strings.HasPrefix(line, "# "+name+" is not set"):
return fmt.Errorf("%w: %s", errKernelFeatureNotSet, name)
}
}
}
}
return fmt.Errorf("%w: for module name %s", errKernelFeatureNotFound, moduleName)
}
func moduleNameToKernelFeatureGroups(moduleName string) (featureGroups [][]string, ok bool) {
moduleMap := map[string][][]string{
"nf_tables": {{"CONFIG_NF_TABLES"}},
// Netfilter Matches
"xt_conntrack": {{"CONFIG_NETFILTER_XT_MATCH_CONNTRACK"}},
"xt_connmark": {
{"CONFIG_NETFILTER_XT_CONNMARK"},
{"CONFIG_NETFILTER_XT_MATCH_CONNMARK", "CONFIG_NETFILTER_XT_TARGET_CONNMARK"},
},
"xt_mark": {
{"CONFIG_NETFILTER_XT_MARK"},
{"CONFIG_NETFILTER_XT_MATCH_MARK", "CONFIG_NETFILTER_XT_TARGET_MARK"},
},
"nf_conntrack_netlink": {{"CONFIG_NF_CT_NETLINK"}},
"nf_reject_ipv4": {{"CONFIG_NF_REJECT_IPV4"}},
// Common Netfilter Targets
"xt_log": {{"CONFIG_NETFILTER_XT_TARGET_LOG"}},
"xt_reject": {
{"CONFIG_IP_NF_TARGET_REJECT", "CONFIG_NF_REJECT_IPV4"},
{"CONFIG_NETFILTER_XT_TARGET_REJECT", "CONFIG_NF_REJECT_IPV4"},
},
"xt_masquerade": {{"CONFIG_NETFILTER_XT_TARGET_MASQUERADE"}},
// Additional Netfilter Matches
"xt_addrtype": {{"CONFIG_NETFILTER_XT_MATCH_ADDRTYPE"}},
"xt_comment": {{"CONFIG_NETFILTER_XT_MATCH_COMMENT"}},
"xt_multiport": {{"CONFIG_NETFILTER_XT_MATCH_MULTIPORT"}},
"xt_state": {{"CONFIG_NETFILTER_XT_MATCH_STATE"}},
"xt_tcpudp": {{"CONFIG_NETFILTER_XT_MATCH_TCPUDP"}},
// Tunneling and Virtualization
"tun": {{"CONFIG_TUN"}},
"bridge": {{"CONFIG_BRIDGE"}},
"veth": {{"CONFIG_VETH"}},
"vxlan": {{"CONFIG_VXLAN"}},
"wireguard": {{"CONFIG_WIREGUARD"}},
// Filesystems
"overlay": {{"CONFIG_OVERLAY_FS"}},
"fuse": {{"CONFIG_FUSE_FS"}},
}
featureGroups, ok = moduleMap[strings.ToLower(moduleName)]
return featureGroups, ok
}
func allFeaturesOK(featureToOK map[string]bool) bool {
for _, ok := range featureToOK {
if !ok {
return false
}
}
return true
}
+34 -30
View File
@@ -28,36 +28,7 @@ type moduleInfo struct {
var ErrModulesDirectoryNotFound = errors.New("modules directory not found") var ErrModulesDirectoryNotFound = errors.New("modules directory not found")
func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) { func getModulesInfo(modulesPath string) (modulesInfo map[string]moduleInfo, err error) {
var utsName unix.Utsname
err = unix.Uname(&utsName)
if err != nil {
return nil, fmt.Errorf("getting unix uname release: %w", err)
}
release := unix.ByteSliceToString(utsName.Release[:])
release = strings.TrimSpace(release)
modulePaths := []string{
filepath.Join("/lib/modules", release),
filepath.Join("/usr/lib/modules", release),
}
var modulesPath string
var found bool
for _, modulesPath = range modulePaths {
info, err := os.Stat(modulesPath)
if err == nil && info.IsDir() {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("%w: %s are not valid existing directories"+
"; have you bind mounted the /lib/modules directory?",
ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", "))
}
dependencyFilepath := filepath.Join(modulesPath, "modules.dep") dependencyFilepath := filepath.Join(modulesPath, "modules.dep")
dependencyFile, err := os.Open(dependencyFilepath) dependencyFile, err := os.Open(dependencyFilepath)
if err != nil { if err != nil {
@@ -109,6 +80,39 @@ func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) {
return modulesInfo, nil return modulesInfo, nil
} }
func getModulesPath() (string, error) {
release, err := getReleaseName()
if err != nil {
return "", fmt.Errorf("getting release name: %w", err)
}
modulePaths := []string{
filepath.Join("/lib/modules", release),
filepath.Join("/usr/lib/modules", release),
}
for _, modulesPath := range modulePaths {
info, err := os.Stat(modulesPath)
if err == nil && info.IsDir() {
return modulesPath, nil
}
}
return "", fmt.Errorf("%w: %s are not valid existing directories"+
"; have you bind mounted the /lib/modules directory?",
ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", "))
}
func getReleaseName() (release string, err error) {
var utsName unix.Utsname
err = unix.Uname(&utsName)
if err != nil {
return "", fmt.Errorf("getting unix uname release: %w", err)
}
release = unix.ByteSliceToString(utsName.Release[:])
release = strings.TrimSpace(release)
return release, nil
}
func getBuiltinModules(modulesDirPath string, modulesInfo map[string]moduleInfo) error { func getBuiltinModules(modulesDirPath string, modulesInfo map[string]moduleInfo) error {
file, err := os.Open(filepath.Join(modulesDirPath, "modules.builtin")) file, err := os.Open(filepath.Join(modulesDirPath, "modules.builtin"))
if err != nil { if err != nil {
+39 -2
View File
@@ -1,12 +1,49 @@
package mod package mod
import ( import (
"errors"
"fmt" "fmt"
) )
// Probe loads the given kernel module and its dependencies. // Probe is a expanded version of modprobe, in which it checks if the Kernel
// built-in features contain the given module name.
// It first tries to locate the modules directory in [getModulesPath].
// If it fails (like on WSL), it then only checks for the kernel feature
// in /proc/config.gz with [checkProcConfig].
// Otherwise, it first checks if the modules directory modules.builtin
// file contains the given module name in [checkModulesBuiltin].
// If the module is not found, it then runs the classic [modProbe] behavior,
// trying to load the module in the kernel.
// If this fails, it does one final try running [checkProcConfig].
func Probe(moduleName string) error { func Probe(moduleName string) error {
modulesInfo, err := getModulesInfo() modulesPath, err := getModulesPath()
if err != nil {
if errors.Is(err, ErrModulesDirectoryNotFound) {
err = checkProcConfig(moduleName)
if err != nil {
return fmt.Errorf("checking /proc/config.gz: %w", err)
}
return nil
}
return fmt.Errorf("getting modules path: %w", err)
}
err = checkModulesBuiltin(modulesPath, moduleName)
if err != nil {
err = modProbe(modulesPath, moduleName)
if err != nil {
err = checkProcConfig(moduleName)
if err != nil {
return fmt.Errorf("checking /proc/config.gz: %w", err)
}
}
}
return nil
}
// modProbe is the classic modprobe behavior.
func modProbe(modulesPath, moduleName string) error {
modulesInfo, err := getModulesInfo(modulesPath)
if err != nil { if err != nil {
return fmt.Errorf("getting modules information: %w", err) return fmt.Errorf("getting modules information: %w", err)
} }
+24 -15
View File
@@ -55,6 +55,8 @@ func extractDataFromLines(lines []string) (
func extractDataFromLine(line string) ( func extractDataFromLine(line string) (
ip netip.Addr, port uint16, protocol string, err error, ip netip.Addr, port uint16, protocol string, err error,
) { ) {
line = strings.TrimSpace(line)
switch { switch {
case strings.HasPrefix(line, "proto "): case strings.HasPrefix(line, "proto "):
protocol, err = extractProto(line) protocol, err = extractProto(line)
@@ -81,10 +83,7 @@ func extractDataFromLine(line string) (
return ip, 0, "", nil return ip, 0, "", nil
} }
var ( var errProtoLineFieldsCount = errors.New("proto line has not 2 fields as expected")
errProtoLineFieldsCount = errors.New("proto line has not 2 fields as expected")
errProtocolNotSupported = errors.New("network protocol not supported")
)
func extractProto(line string) (protocol string, err error) { func extractProto(line string) (protocol string, err error) {
fields := strings.Fields(line) fields := strings.Fields(line)
@@ -92,13 +91,25 @@ func extractProto(line string) (protocol string, err error) {
return "", fmt.Errorf("%w: %s", errProtoLineFieldsCount, line) return "", fmt.Errorf("%w: %s", errProtoLineFieldsCount, line)
} }
switch fields[1] { return parseProto(fields[1])
case "tcp", "tcp4", "tcp6", "tcp-client", "udp", "udp4", "udp6": }
default:
return "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[1])
}
return fields[1], nil var errProtocolNotSupported = errors.New("network protocol not supported")
func parseProto(field string) (protocol string, err error) {
switch field {
case "tcp", "tcp4", "tcp6", "tcp-client":
// tcp4, tcp6 can be assimilated as tcp since the IP version is
// determined by the remote IP address version.
// tcp-client is a synonym of tcp for OpenVPN 2.5+ acting in client mode.
return constants.TCP, nil
case "udp", "udp4", "udp6":
// udp4, udp6 can be assimilated as udp since the IP version is
// determined by the remote IP address version.
return constants.UDP, nil
default:
return "", fmt.Errorf("%w: %s", errProtocolNotSupported, field)
}
} }
var ( var (
@@ -136,11 +147,9 @@ func extractRemote(line string) (ip netip.Addr, port uint16,
} }
if n > 3 { //nolint:mnd if n > 3 { //nolint:mnd
switch fields[3] { protocol, err = parseProto(fields[3])
case "tcp", "udp": if err != nil {
protocol = fields[3] return netip.Addr{}, 0, "", fmt.Errorf("parsing protocol from remote line: %w", err)
default:
return netip.Addr{}, 0, "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[3])
} }
} }
+10 -2
View File
@@ -62,6 +62,14 @@ func Test_extractDataFromLines(t *testing.T) {
Protocol: constants.UDP, Protocol: constants.UDP,
}, },
}, },
"leading_whitespace": {
lines: []string{" proto tcp", "\tremote 1.2.3.4 443 tcp"},
connection: models.Connection{
IP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
Port: 443,
Protocol: constants.TCP,
},
},
} }
for name, testCase := range testCases { for name, testCase := range testCases {
@@ -105,7 +113,7 @@ func Test_extractDataFromLine(t *testing.T) {
}, },
"tcp-client": { "tcp-client": {
line: "proto tcp-client", line: "proto tcp-client",
protocol: "tcp-client", protocol: constants.TCP,
}, },
"extract remote error": { "extract remote error": {
line: "remote bad", line: "remote bad",
@@ -239,7 +247,7 @@ func Test_extractRemote(t *testing.T) {
}, },
"invalid protocol": { "invalid protocol": {
line: "remote 1.2.3.4 8000 bad", line: "remote 1.2.3.4 8000 bad",
err: errors.New("network protocol not supported: bad"), err: errors.New("parsing protocol from remote line: network protocol not supported: bad"),
}, },
"IP host and port and protocol": { "IP host and port and protocol": {
line: "remote 1.2.3.4 8000 udp", line: "remote 1.2.3.4 8000 udp",
+1 -4
View File
@@ -29,19 +29,16 @@ func (r *Runner) Run(ctx context.Context, errCh chan<- error, ready chan<- struc
return return
} }
streamCtx, streamCancel := context.WithCancel(context.Background())
streamDone := make(chan struct{}) streamDone := make(chan struct{})
go streamLines(streamCtx, streamDone, r.logger, go streamLines(streamDone, r.logger,
stdoutLines, stderrLines, ready) stdoutLines, stderrLines, ready)
select { select {
case <-ctx.Done(): case <-ctx.Done():
<-waitError <-waitError
streamCancel()
<-streamDone <-streamDone
errCh <- ctx.Err() errCh <- ctx.Err()
case err := <-waitError: case err := <-waitError:
streamCancel()
<-streamDone <-streamDone
errCh <- err errCh <- err
} }
+20 -9
View File
@@ -1,26 +1,37 @@
package openvpn package openvpn
import ( import (
"context"
"strings" "strings"
) )
func streamLines(ctx context.Context, done chan<- struct{}, func streamLines(done chan<- struct{},
logger Logger, stdout, stderr <-chan string, logger Logger, stdout, stderr <-chan string,
tunnelReady chan<- struct{}, tunnelReady chan<- struct{},
) { ) {
defer close(done) defer close(done)
var line string
for { for {
var line string
var ok bool
errLine := false errLine := false
select { select {
case <-ctx.Done(): case line, ok = <-stdout:
return if ok {
case line = <-stdout: break
case line = <-stderr: }
errLine = true if stderr == nil {
return
}
stdout = nil
case line, ok = <-stderr:
if ok {
errLine = true
break
}
if stdout == nil {
return
}
stderr = nil
} }
line, level := processLogLine(line) line, level := processLogLine(line)
if line == "" { if line == "" {
+1
View File
@@ -29,6 +29,7 @@ type Logger interface {
Info(s string) Info(s string)
Warn(s string) Warn(s string)
Error(s string) Error(s string)
Errorf(format string, args ...any)
} }
type Cmder interface { type Cmder interface {
+11 -2
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"sync" "sync"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/portforward/service" "github.com/qdm12/gluetun/internal/portforward/service"
@@ -85,6 +86,8 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
defer close(runDone) defer close(runDone)
var serviceRunError <-chan error var serviceRunError <-chan error
var retryAfter <-chan time.Time
const retryDelay = 5 * time.Second
for { for {
updateReceived := false updateReceived := false
select { select {
@@ -103,10 +106,12 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
l.settingsMutex.Unlock() l.settingsMutex.Unlock()
case err := <-serviceRunError: case err := <-serviceRunError:
l.logger.Error(err.Error()) l.logger.Error(err.Error())
case <-retryAfter:
// Retry starting the service after a delay
retryAfter = nil
} }
firstRun := serviceRunError == nil if l.service != nil {
if !firstRun {
err := l.service.Stop() err := l.service.Stop()
if err != nil { if err != nil {
runErrorCh <- fmt.Errorf("stopping previous service: %w", err) runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
@@ -130,6 +135,10 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
err = fmt.Errorf("starting port forwarding service: %w", err) err = fmt.Errorf("starting port forwarding service: %w", err)
} }
updateResult <- err updateResult <- err
} else if err != nil {
// Log the error and schedule a retry
l.logger.Errorf("starting port forwarding service: %s - retrying in %s", err, retryDelay)
retryAfter = time.After(retryDelay)
} }
} }
} }
+26 -14
View File
@@ -18,7 +18,11 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
} }
portsString := strings.Join(portStrings, ",") portsString := strings.Join(portStrings, ",")
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString) commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
commandString = strings.ReplaceAll(commandString, "{{PORT}}", portStrings[0]) var firstPort string
if len(portStrings) > 0 {
firstPort = portStrings[0]
}
commandString = strings.ReplaceAll(commandString, "{{PORT}}", firstPort)
commandString = strings.ReplaceAll(commandString, "{{VPN_INTERFACE}}", vpnInterface) commandString = strings.ReplaceAll(commandString, "{{VPN_INTERFACE}}", vpnInterface)
args, err := command.Split(commandString) args, err := command.Split(commandString)
if err != nil { if err != nil {
@@ -31,31 +35,39 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
return err return err
} }
streamCtx, streamCancel := context.WithCancel(context.Background())
streamDone := make(chan struct{}) streamDone := make(chan struct{})
go streamLines(streamCtx, streamDone, logger, stdout, stderr) go streamLines(streamDone, logger, stdout, stderr)
err = <-waitError err = <-waitError
streamCancel()
<-streamDone <-streamDone
return err return err
} }
func streamLines(ctx context.Context, done chan<- struct{}, func streamLines(done chan<- struct{}, logger Logger,
logger Logger, stdout, stderr <-chan string, stdout, stderr <-chan string,
) { ) {
defer close(done) defer close(done)
var line string
for { for {
select { select {
case <-ctx.Done(): case line, ok := <-stdout:
return if ok {
case line = <-stdout: logger.Info(line)
logger.Info(line) break
case line = <-stderr: }
logger.Error(line) if stderr == nil {
return
}
stdout = nil
case line, ok := <-stderr:
if ok {
logger.Error(line)
break
}
if stdout == nil {
return
}
stderr = nil
} }
} }
} }
+6 -1
View File
@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/openvpn" "github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/provider/utils"
@@ -68,7 +69,11 @@ func modifyConfig(lines []string, connection models.Connection,
} }
// Add values // Add values
modified = append(modified, "proto "+connection.Protocol) protocol := connection.Protocol
if protocol == constants.TCP {
protocol = "tcp-client"
}
modified = append(modified, "proto "+protocol)
modified = append(modified, fmt.Sprintf("remote %s %d", connection.IP, connection.Port)) modified = append(modified, fmt.Sprintf("remote %s %d", connection.IP, connection.Port))
modified = append(modified, "dev "+settings.Interface) modified = append(modified, "dev "+settings.Interface)
modified = append(modified, "mute-replay-warnings") modified = append(modified, "mute-replay-warnings")
+8 -5
View File
@@ -17,11 +17,14 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
Ciphers: []string{ Ciphers: []string{
openvpn.AES256gcm, openvpn.AES256cbc, openvpn.AES128gcm, openvpn.AES256gcm, openvpn.AES256cbc, openvpn.AES128gcm,
}, },
Auth: openvpn.SHA512, Auth: openvpn.SHA512,
CAs: []string{"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"}, //nolint:lll CAs: []string{
Cert: "MIIDTjCCAregAwIBAgIDKzZvMA0GCSqGSIb3DQEBCwUAMIGFMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UEChMMRm9ydC1GdW5zdG9uMRgwFgYDVQQDEw9Gb3J0LUZ1bnN0b24gQ0ExITAfBgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjAgFw0xNjExMDMwMzA2MThaGA8yMDY2MTEwMzAzMDYxOFowgYoxCzAJBgNVBAYTAlZHMQwwCgYDVQQIDANCVkkxEzARBgNVBAoMCkV4cHJlc3NWUE4xEzARBgNVBAsMCkV4cHJlc3NWUE4xHDAaBgNVBAMME2V4cHJlc3N2cG5fY3VzdG9tZXIxJTAjBgkqhkiG9w0BCQEWFnN1cHBvcnRAZXhwcmVzc3Zwbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrOYt/KOi2uMDGev3pXg8j1SO4J/4EVWDF7vJcKr2jrZlqD/zuAFx2W1YWvwumPO6PKH4PU9621aNdiumaUkv/RplCfznnnxqobhJuTE2oA+rS1bOq+9OhHwF9jgNXNVk+XX4d0toST5uGE6Z3OdmPBur8o5AlCf78PDSAwpFOw5HrgLqOEU4hTweC1/czX2VsvsHv22HRI6JMZgP8gGQii/p9iukqfaJvGdPciL5p1QRBUQIi8P8pNvEp1pVIpxYj7/LOUqb2DxFvgmp2v1IQ0Yu88SWsFk84+xAYHzfkLyS31Sqj5uLRBnJqx3fIlOihQ50GI72fwPMwo+OippvVAgMBAAGjPzA9MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgSwMB0GA1UdDgQWBBSkBM1TCX9kBgFsv2RmOzudMXa9njANBgkqhkiG9w0BAQsFAAOBgQA+2e4b+33zFmA+1ZQ46kWkfiB+fEeDyMwMLeYYyDS2d8mZhNZKdOw7dy4Ifz9Vqzp4aKuQ6j61c6k1UaQQL0tskqWVzslSFvs9NZyUAJLLdGUc5TT2MiLwiXQwd4UvH6bGeePdhvB4+ZbW7VMD7TE8hZhjhAL4F6yAP1EQvg3LDA==", //nolint:lll "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", //nolint:lll
RSAKey: "MIIEpAIBAAKCAQEAqzmLfyjotrjAxnr96V4PI9UjuCf+BFVgxe7yXCq9o62Zag/87gBcdltWFr8Lpjzujyh+D1PettWjXYrpmlJL/0aZQn85558aqG4SbkxNqAPq0tWzqvvToR8BfY4DVzVZPl1+HdLaEk+bhhOmdznZjwbq/KOQJQn+/Dw0gMKRTsOR64C6jhFOIU8Hgtf3M19lbL7B79th0SOiTGYD/IBkIov6fYrpKn2ibxnT3Ii+adUEQVECIvD/KTbxKdaVSKcWI+/yzlKm9g8Rb4Jqdr9SENGLvPElrBZPOPsQGB835C8kt9Uqo+bi0QZyasd3yJTooUOdBiO9n8DzMKPjoqab1QIDAQABAoIBAHgsekC0SKi+AOcNOZqJ3pxqophE0V7fQX2KWGXhxZnUZMFxGTc936deMYzjZ1y0lUa6x8cgOUcfqHol3hDmw9oWBckLHGv5Wi9umdb6DOLoZO62+FQATSdfaJ9jheq2Ub2YxsRN0apaXzB6KDKz0oM0+sZ4Udn9Kw6DfuIELRIWwEx4w0v3gKW7YLC4Jkc4AwLkPK03xEA/qImfkCmaMPLhrgVQt+IFfP8bXzL7CCC04rNU/IS8pyjex+iUolnQZlbXntF7Bm4V2mz0827ZVqrgAb/hEQRlsTW3rRkVh+rrdoUE7BCZRTFmRCbLoShjN6XuSf4sAus8ch4UEN12gN0CgYEA4o/tSvij1iPaXLmt4KOEuxqmSGB8MLKhFde8lBbNdrDgxiIH9bH7khKx15XRTX0qLDbs8b2/UJygZG0Aa1kIBqZTXTgeMAuxPRTesALJPdqQ/ROnbJcdFkI7gllrAG8VB0fH4wTRsRd0vWEB6YlCdE107u6LEsLAHxOj9Q5819cCgYEAwXjx9RkQ2qITBx5Ewib8YsltA0n3cmRomPicLlsnKV5DfvyCLpFIsZ1h3f9dUpfxRLwzp8wcoLiq9cCoOGdu1udw/yBTqmhaXWhUK/g77f9Ze2ZB1OEhuyKLYJ1vW/h/Z/a1aPCMxZqsDTPCePsuO8Cez5gqs8LjM3W7EyzRxDMCgYEAvhHrDFt975fSiLoJgo0MPIAGAnBXn+8sLwv3m/FpW+rWF8LTFK/Fku12H5wDpNOdvswxijkauIE+GiJMGMLvdcyx4WHECaC1h73reJRNykOEIZ0Md5BrCZJ1JEzp9Mo8RQhWTEFtvfkkqgApP4g0pSeaMx0StaGG1kt+4IbP+68CgYBrZdQKlquAck/Vt7u7eyDHRcE5/ilaWtqlb/xizz7h++3D5C/v4b5UumTFcyg+3RGVclPKZcfOgDSGzzeSd/hTW46iUTOgeOUQzQVMkzPRXdoyYgVRQtgSpY5xR3O1vjAbahwx8LZ0SvQPMBhYSDbV/Isr+fBacWjl/AipEEwxeQKBgQDdrAEnVlOFoCLw4sUjsPoxkLjhTAgI7CYk5NNxX67Rnj0tp+Y49+sGUhl5sCGfMKkLShiON5P2oxZa+B0aPtQjsdnsFPa1uaZkK4c++SS6AetzYRpVDLmLp7/1CulE0z3O0sBekpwiuaqLJ9ZccC81g4+2j8j6c50rIAct3hxIxw==", //nolint:lll "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", //nolint:lll
TLSAuth: "48d9999bd71095b10649c7cb471c1051b1afdece597cea06909b99303a18c67401597b12c04a787e98cdb619ee960d90a0165529dc650f3a5c6fbe77c91c137dcf55d863fcbe314df5f0b45dbe974d9bde33ef5b4803c3985531c6c23ca6906d6cd028efc8585d1b9e71003566bd7891b9cc9212bcba510109922eed87f5c8e66d8e59cbd82575261f02777372b2cd4ca5214c4a6513ff26dd568f574fd40d6cd450fc788160ff68434ce2bf6afb00e710a3198538f14c4d45d84ab42637872e778a6b35a124e700920879f1d003ba93dccdb953cdf32bea03f365760b0ed8002098d4ce20d045b45a83a8432cc737677aed27125592a7148d25c87fdbe0a3f6", //nolint:lll },
Cert: "MIIDTjCCAregAwIBAgIDKzZvMA0GCSqGSIb3DQEBCwUAMIGFMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UEChMMRm9ydC1GdW5zdG9uMRgwFgYDVQQDEw9Gb3J0LUZ1bnN0b24gQ0ExITAfBgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjAgFw0xNjExMDMwMzA2MThaGA8yMDY2MTEwMzAzMDYxOFowgYoxCzAJBgNVBAYTAlZHMQwwCgYDVQQIDANCVkkxEzARBgNVBAoMCkV4cHJlc3NWUE4xEzARBgNVBAsMCkV4cHJlc3NWUE4xHDAaBgNVBAMME2V4cHJlc3N2cG5fY3VzdG9tZXIxJTAjBgkqhkiG9w0BCQEWFnN1cHBvcnRAZXhwcmVzc3Zwbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrOYt/KOi2uMDGev3pXg8j1SO4J/4EVWDF7vJcKr2jrZlqD/zuAFx2W1YWvwumPO6PKH4PU9621aNdiumaUkv/RplCfznnnxqobhJuTE2oA+rS1bOq+9OhHwF9jgNXNVk+XX4d0toST5uGE6Z3OdmPBur8o5AlCf78PDSAwpFOw5HrgLqOEU4hTweC1/czX2VsvsHv22HRI6JMZgP8gGQii/p9iukqfaJvGdPciL5p1QRBUQIi8P8pNvEp1pVIpxYj7/LOUqb2DxFvgmp2v1IQ0Yu88SWsFk84+xAYHzfkLyS31Sqj5uLRBnJqx3fIlOihQ50GI72fwPMwo+OippvVAgMBAAGjPzA9MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgSwMB0GA1UdDgQWBBSkBM1TCX9kBgFsv2RmOzudMXa9njANBgkqhkiG9w0BAQsFAAOBgQA+2e4b+33zFmA+1ZQ46kWkfiB+fEeDyMwMLeYYyDS2d8mZhNZKdOw7dy4Ifz9Vqzp4aKuQ6j61c6k1UaQQL0tskqWVzslSFvs9NZyUAJLLdGUc5TT2MiLwiXQwd4UvH6bGeePdhvB4+ZbW7VMD7TE8hZhjhAL4F6yAP1EQvg3LDA==", //nolint:lll
RSAKey: "MIIEpAIBAAKCAQEAqzmLfyjotrjAxnr96V4PI9UjuCf+BFVgxe7yXCq9o62Zag/87gBcdltWFr8Lpjzujyh+D1PettWjXYrpmlJL/0aZQn85558aqG4SbkxNqAPq0tWzqvvToR8BfY4DVzVZPl1+HdLaEk+bhhOmdznZjwbq/KOQJQn+/Dw0gMKRTsOR64C6jhFOIU8Hgtf3M19lbL7B79th0SOiTGYD/IBkIov6fYrpKn2ibxnT3Ii+adUEQVECIvD/KTbxKdaVSKcWI+/yzlKm9g8Rb4Jqdr9SENGLvPElrBZPOPsQGB835C8kt9Uqo+bi0QZyasd3yJTooUOdBiO9n8DzMKPjoqab1QIDAQABAoIBAHgsekC0SKi+AOcNOZqJ3pxqophE0V7fQX2KWGXhxZnUZMFxGTc936deMYzjZ1y0lUa6x8cgOUcfqHol3hDmw9oWBckLHGv5Wi9umdb6DOLoZO62+FQATSdfaJ9jheq2Ub2YxsRN0apaXzB6KDKz0oM0+sZ4Udn9Kw6DfuIELRIWwEx4w0v3gKW7YLC4Jkc4AwLkPK03xEA/qImfkCmaMPLhrgVQt+IFfP8bXzL7CCC04rNU/IS8pyjex+iUolnQZlbXntF7Bm4V2mz0827ZVqrgAb/hEQRlsTW3rRkVh+rrdoUE7BCZRTFmRCbLoShjN6XuSf4sAus8ch4UEN12gN0CgYEA4o/tSvij1iPaXLmt4KOEuxqmSGB8MLKhFde8lBbNdrDgxiIH9bH7khKx15XRTX0qLDbs8b2/UJygZG0Aa1kIBqZTXTgeMAuxPRTesALJPdqQ/ROnbJcdFkI7gllrAG8VB0fH4wTRsRd0vWEB6YlCdE107u6LEsLAHxOj9Q5819cCgYEAwXjx9RkQ2qITBx5Ewib8YsltA0n3cmRomPicLlsnKV5DfvyCLpFIsZ1h3f9dUpfxRLwzp8wcoLiq9cCoOGdu1udw/yBTqmhaXWhUK/g77f9Ze2ZB1OEhuyKLYJ1vW/h/Z/a1aPCMxZqsDTPCePsuO8Cez5gqs8LjM3W7EyzRxDMCgYEAvhHrDFt975fSiLoJgo0MPIAGAnBXn+8sLwv3m/FpW+rWF8LTFK/Fku12H5wDpNOdvswxijkauIE+GiJMGMLvdcyx4WHECaC1h73reJRNykOEIZ0Md5BrCZJ1JEzp9Mo8RQhWTEFtvfkkqgApP4g0pSeaMx0StaGG1kt+4IbP+68CgYBrZdQKlquAck/Vt7u7eyDHRcE5/ilaWtqlb/xizz7h++3D5C/v4b5UumTFcyg+3RGVclPKZcfOgDSGzzeSd/hTW46iUTOgeOUQzQVMkzPRXdoyYgVRQtgSpY5xR3O1vjAbahwx8LZ0SvQPMBhYSDbV/Isr+fBacWjl/AipEEwxeQKBgQDdrAEnVlOFoCLw4sUjsPoxkLjhTAgI7CYk5NNxX67Rnj0tp+Y49+sGUhl5sCGfMKkLShiON5P2oxZa+B0aPtQjsdnsFPa1uaZkK4c++SS6AetzYRpVDLmLp7/1CulE0z3O0sBekpwiuaqLJ9ZccC81g4+2j8j6c50rIAct3hxIxw==", //nolint:lll
TLSAuth: "48d9999bd71095b10649c7cb471c1051b1afdece597cea06909b99303a18c67401597b12c04a787e98cdb619ee960d90a0165529dc650f3a5c6fbe77c91c137dcf55d863fcbe314df5f0b45dbe974d9bde33ef5b4803c3985531c6c23ca6906d6cd028efc8585d1b9e71003566bd7891b9cc9212bcba510109922eed87f5c8e66d8e59cbd82575261f02777372b2cd4ca5214c4a6513ff26dd568f574fd40d6cd450fc788160ff68434ce2bf6afb00e710a3198538f14c4d45d84ab42637872e778a6b35a124e700920879f1d003ba93dccdb953cdf32bea03f365760b0ed8002098d4ce20d045b45a83a8432cc737677aed27125592a7148d25c87fdbe0a3f6", //nolint:lll
MssFix: 1200, MssFix: 1200,
FastIO: true, FastIO: true,
Fragment: 1300, Fragment: 1300,
@@ -108,7 +108,6 @@ func hardcodedServers() (servers []models.Server) {
{Country: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"}, {Country: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
{Country: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"}, {Country: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
{Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"}, {Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"},
{Country: "Pakistan", Hostname: "pakistan-ca-version-2.expressnetw.com"},
{Country: "Panama", Hostname: "panama-ca-version-2.expressnetw.com"}, {Country: "Panama", Hostname: "panama-ca-version-2.expressnetw.com"},
{Country: "Peru", Hostname: "peru-ca-version-2.expressnetw.com"}, {Country: "Peru", Hostname: "peru-ca-version-2.expressnetw.com"},
{Country: "Philippines (via Singapore)", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"}, {Country: "Philippines (via Singapore)", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"},
+1 -1
View File
@@ -9,7 +9,7 @@ import (
func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
connection models.Connection, err error, connection models.Connection, err error,
) { ) {
defaults := utils.NewConnectionDefaults(0, 1194, 0) //nolint:mnd defaults := utils.NewConnectionDefaults(1194, 1194, 0) //nolint:mnd
return utils.GetConnection(p.Name(), return utils.GetConnection(p.Name(),
p.storage, selection, defaults, ipv6Supported, p.randSource) p.storage, selection, defaults, ipv6Supported, p.randSource)
} }
+3 -4
View File
@@ -2,6 +2,7 @@ package privado
import ( import (
"math/rand" "math/rand"
"net/http"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/common"
@@ -15,14 +16,12 @@ type Provider struct {
} }
func New(storage common.Storage, randSource rand.Source, func New(storage common.Storage, randSource rand.Source,
ipFetcher common.IPFetcher, unzipper common.Unzipper, client *http.Client, updaterWarner common.Warner,
updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
) *Provider { ) *Provider {
return &Provider{ return &Provider{
storage: storage, storage: storage,
randSource: randSource, randSource: randSource,
Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver), Fetcher: updater.New(client, updaterWarner),
} }
} }
@@ -1,50 +0,0 @@
package updater
import (
"net/netip"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
)
type hostToServer map[string]models.Server
func (hts hostToServer) add(host string) {
server, ok := hts[host]
if ok {
return
}
server.VPN = vpn.OpenVPN
server.Hostname = host
server.UDP = true
hts[host] = server
}
func (hts hostToServer) toHostsSlice() (hosts []string) {
hosts = make([]string, 0, len(hts))
for host := range hts {
hosts = append(hosts, host)
}
return hosts
}
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]netip.Addr) {
for host, IPs := range hostToIPs {
server := hts[host]
server.IPs = IPs
hts[host] = server
}
for host, server := range hts {
if len(server.IPs) == 0 {
delete(hts, host)
}
}
}
func (hts hostToServer) toServersSlice() (servers []models.Server) {
servers = make([]models.Server, 0, len(hts))
for _, server := range hts {
servers = append(servers, server)
}
return servers
}
@@ -1,31 +0,0 @@
package updater
import (
"context"
"net/netip"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/publicip/api"
)
func setLocationInfo(ctx context.Context, fetcher common.IPFetcher, servers []models.Server) (err error) {
// Get public IP address information
ipsToGetInfo := make([]netip.Addr, 0, len(servers))
for _, server := range servers {
ipsToGetInfo = append(ipsToGetInfo, server.IPs...)
}
ipsInfo, err := api.FetchMultiInfo(ctx, fetcher, ipsToGetInfo)
if err != nil {
return err
}
for i := range servers {
ipInfo := ipsInfo[i]
servers[i].Country = ipInfo.Country
servers[i].Region = ipInfo.Region
servers[i].City = ipInfo.City
}
return nil
}
@@ -1,26 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 0.1
maxDuration = 30 * time.Second
maxNoNew = 2
maxFails = 2
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
+39 -50
View File
@@ -2,77 +2,66 @@ package updater
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http"
"net/netip"
"sort" "sort"
"strings"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/updater/openvpn"
) )
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,
) { ) {
if !u.ipFetcher.CanFetchAnyIP() { const url = "https://privadovpn.com/apps/servers_export.json"
return nil, fmt.Errorf("%w: %s", common.ErrIPFetcherUnsupported, u.ipFetcher.String()) request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
}
const url = "https://privadovpn.com/apps/ovpn_configs.zip"
contents, err := u.unzipper.FetchAndExtract(ctx, url)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("creating request: %w", err)
} else if len(contents) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(contents), minServers)
} }
hts := make(hostToServer) response, err := u.client.Do(request)
for fileName, content := range contents {
if !strings.HasSuffix(fileName, ".ovpn") {
continue // not an OpenVPN file
}
host, warning, err := openvpn.ExtractHost(content)
if warning != "" {
u.warner.Warn(warning)
}
if err != nil {
// treat error as warning and go to next file
u.warner.Warn(err.Error() + " in " + fileName)
continue
}
hts.add(host)
}
if len(hts) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(hts), minServers)
}
hosts := hts.toHostsSlice()
resolveSettings := parallelResolverSettings(hosts)
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
} }
if len(hostToIPs) < minServers { var data struct {
return nil, fmt.Errorf("%w: %d and expected at least %d", Servers []struct {
common.ErrNotEnoughServers, len(servers), minServers) Country string `json:"country"`
City string `json:"city"`
Hostname string `json:"hostname"`
IP netip.Addr `json:"ip"`
} `json:"servers"`
}
decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&data)
if err != nil {
_ = response.Body.Close()
return nil, fmt.Errorf("decoding JSON response: %w", err)
} }
hts.adaptWithIPs(hostToIPs) err = response.Body.Close()
if err != nil {
return nil, fmt.Errorf("closing response body: %w", err)
}
servers = hts.toServersSlice() if len(data.Servers) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(data.Servers), minServers)
}
if err := setLocationInfo(ctx, u.ipFetcher, servers); err != nil { servers = make([]models.Server, len(data.Servers))
return nil, err for i, server := range data.Servers {
servers[i] = models.Server{
VPN: vpn.OpenVPN,
Country: server.Country,
City: server.City,
Hostname: server.Hostname,
IPs: []netip.Addr{server.IP},
UDP: true,
}
} }
sort.Sort(models.SortableServers(servers)) sort.Sort(models.SortableServers(servers))
+7 -11
View File
@@ -1,23 +1,19 @@
package updater package updater
import ( import (
"net/http"
"github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/common"
) )
type Updater struct { type Updater struct {
ipFetcher common.IPFetcher client *http.Client
unzipper common.Unzipper warner common.Warner
parallelResolver common.ParallelResolver
warner common.Warner
} }
func New(ipFetcher common.IPFetcher, unzipper common.Unzipper, func New(client *http.Client, warner common.Warner) *Updater {
warner common.Warner, parallelResolver common.ParallelResolver,
) *Updater {
return &Updater{ return &Updater{
ipFetcher: ipFetcher, client: client,
unzipper: unzipper, warner: warner,
parallelResolver: parallelResolver,
warner: warner,
} }
} }
@@ -13,12 +13,12 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support
// Set port defaults depending on encryption preset. // Set port defaults depending on encryption preset.
var defaults utils.ConnectionDefaults var defaults utils.ConnectionDefaults
switch *selection.OpenVPN.PIAEncPreset { switch *selection.OpenVPN.PIAEncPreset {
case presets.None, presets.Normal: case presets.Normal:
defaults.OpenVPNTCPPort = 502 defaults.OpenVPNTCPPort = 502
defaults.OpenVPNUDPPort = 1198 defaults.OpenVPNUDPPort = 1198
case presets.Strong: case presets.Strong:
defaults.OpenVPNTCPPort = 501 defaults.OpenVPNTCPPort = 8443
defaults.OpenVPNUDPPort = 1197 defaults.OpenVPNUDPPort = 8080
} }
return utils.GetConnection(p.Name(), return utils.GetConnection(p.Name(),
@@ -24,16 +24,10 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
) )
switch *settings.PIAEncPreset { switch *settings.PIAEncPreset {
case presets.Normal: case presets.Normal:
providerSettings.Ciphers = []string{openvpn.AES128cbc} providerSettings.Ciphers = []string{openvpn.AES128gcm}
providerSettings.Auth = openvpn.SHA1
providerSettings.CAs = []string{caNormalPreset}
case presets.None:
providerSettings.Ciphers = []string{"none"}
providerSettings.Auth = "none"
providerSettings.CAs = []string{caNormalPreset} providerSettings.CAs = []string{caNormalPreset}
default: // strong default: // strong
providerSettings.Ciphers = []string{openvpn.AES256cbc} providerSettings.Ciphers = []string{openvpn.AES256gcm}
providerSettings.Auth = openvpn.SHA256
providerSettings.CAs = []string{"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"} //nolint:lll providerSettings.CAs = []string{"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"} //nolint:lll
} }
@@ -13,6 +13,7 @@ import (
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -39,7 +40,6 @@ func (p *Provider) PortForward(ctx context.Context,
} }
serverName := objects.ServerName serverName := objects.ServerName
apiIP := buildAPIIPAddress(objects.Gateway)
logger := objects.Logger logger := objects.Logger
if !objects.CanPortForward { if !objects.CanPortForward {
@@ -68,22 +68,41 @@ func (p *Provider) PortForward(ctx context.Context,
} }
} }
p.apiIP, err = findAPIIP(ctx, privateIPClient, objects.Gateway)
if err != nil {
return nil, fmt.Errorf("finding API IP address: %w", err)
}
if !dataFound || expired { if !dataFound || expired {
client := objects.Client client := objects.Client
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, apiIP, data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, p.apiIP,
p.portForwardPath, objects.Username, objects.Password) p.portForwardPath, objects.Username, objects.Password)
if err != nil { if err != nil {
return nil, fmt.Errorf("refreshing port forward data: %w", err) return nil, fmt.Errorf("refreshing port forward data: %w", err)
} }
durationToExpiration = data.Expiration.Sub(p.timeNow()) durationToExpiration = data.Expiration.Sub(p.timeNow())
} }
logger.Info("Port forwarded data expires in " + format.FriendlyDuration(durationToExpiration))
// First time binding // First time binding
if err := bindPort(ctx, privateIPClient, apiIP, data); err != nil { for ctx.Err() == nil {
return nil, fmt.Errorf("binding port: %w", err) err = bindPort(ctx, privateIPClient, p.apiIP, data)
if err == nil {
break
} else if !errors.Is(err, errPortBusy) {
return nil, fmt.Errorf("binding port: %w", err)
}
logger.Warn("refreshing port forward data and trying again because " + err.Error())
client := objects.Client
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, p.apiIP,
p.portForwardPath, objects.Username, objects.Password)
if err != nil {
return nil, fmt.Errorf("refreshing port forward data: %w", err)
}
durationToExpiration = data.Expiration.Sub(p.timeNow())
} }
logger.Info("Port forwarded data expires in " + format.FriendlyDuration(durationToExpiration))
return []uint16{data.Port}, nil return []uint16{data.Port}, nil
} }
@@ -99,8 +118,6 @@ func (p *Provider) KeepPortForward(ctx context.Context,
panic("gateway is not set") panic("gateway is not set")
} }
apiIP := buildAPIIPAddress(objects.Gateway)
privateIPClient, err := newHTTPClient(objects.ServerName) privateIPClient, err := newHTTPClient(objects.ServerName)
if err != nil { if err != nil {
return fmt.Errorf("creating custom HTTP client: %w", err) return fmt.Errorf("creating custom HTTP client: %w", err)
@@ -128,7 +145,7 @@ func (p *Provider) KeepPortForward(ctx context.Context,
} }
return ctx.Err() return ctx.Err()
case <-keepAliveTimer.C: case <-keepAliveTimer.C:
err = bindPort(ctx, privateIPClient, apiIP, data) err = bindPort(ctx, privateIPClient, p.apiIP, data)
if err != nil { if err != nil {
return fmt.Errorf("binding port: %w", err) return fmt.Errorf("binding port: %w", err)
} }
@@ -140,15 +157,53 @@ func (p *Provider) KeepPortForward(ctx context.Context,
} }
} }
func buildAPIIPAddress(gateway netip.Addr) (api netip.Addr) { var errAPIIPNotFound = errors.New("API IP address not found")
func findAPIIP(ctx context.Context, client *http.Client, gateway netip.Addr) (
apiIP netip.Addr, err error,
) {
if gateway.Is6() { if gateway.Is6() {
panic("IPv6 gateway not supported") panic("IPv6 gateway not supported")
} }
gatewayBytes := gateway.As4() gatewayBytes := gateway.As4()
gatewayBytes[2] = 128 gatewayBytes[3] = 1 // x.y.z.1
gatewayBytes[3] = 1
return netip.AddrFrom4(gatewayBytes) gatewayBytes[2] = 128 // x.y.128.1
oldAPIIP := netip.AddrFrom4(gatewayBytes)
gatewayBytes[2] = 0 // x.y.0.1 - new API IP reported by some users
newAPIIP := netip.AddrFrom4(gatewayBytes)
possibleIPs := []netip.Addr{oldAPIIP, newAPIIP}
errs := make([]error, 0, len(possibleIPs))
for _, ip := range possibleIPs {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
url := url.URL{
Scheme: "https",
Host: net.JoinHostPort(ip.String(), "19999"),
Path: "/ping",
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
errs = append(errs, fmt.Errorf("trying IP %s: %w", ip, err))
continue
}
response, err := client.Do(request)
if err != nil {
errs = append(errs, fmt.Errorf("trying IP %s: %w", ip, err))
continue
}
_ = response.Body.Close()
return ip, nil
}
return netip.Addr{}, fmt.Errorf("%w: %w", errAPIIPNotFound, errors.Join(errs...))
} }
func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client, func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client,
@@ -260,6 +315,12 @@ func fetchToken(ctx context.Context, client *http.Client,
url.QueryEscape(password): "<password>", url.QueryEscape(password): "<password>",
} }
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 10 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
form := url.Values{} form := url.Values{}
form.Add("username", username) form.Add("username", username)
form.Add("password", password) form.Add("password", password)
@@ -304,6 +365,11 @@ func fetchPortForwardData(ctx context.Context, client *http.Client, apiIP netip.
) { ) {
errSubstitutions := map[string]string{url.QueryEscape(token): "<token>"} errSubstitutions := map[string]string{url.QueryEscape(token): "<token>"}
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
queryParams := make(url.Values) queryParams := make(url.Values)
queryParams.Add("token", token) queryParams.Add("token", token)
url := url.URL{ url := url.URL{
@@ -350,9 +416,23 @@ func fetchPortForwardData(ctx context.Context, client *http.Client, apiIP netip.
return port, data.Signature, expiration, err return port, data.Signature, expiration, err
} }
var ErrBadResponse = errors.New("bad response received") var (
ErrBadResponse = errors.New("bad response received")
errPortBusy = errors.New("port is busy")
)
var (
regexPortBusy = regexp.MustCompile(`^port \d+ is busy\. `)
regexNumber = regexp.MustCompile(`\d+`)
)
func bindPort(ctx context.Context, client *http.Client, apiIPAddress netip.Addr, data piaPortForwardData) (err error) { func bindPort(ctx context.Context, client *http.Client, apiIPAddress netip.Addr, data piaPortForwardData) (err error) {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
payload, err := packPayload(data.Port, data.Token, data.Expiration) payload, err := packPayload(data.Port, data.Token, data.Expiration)
if err != nil { if err != nil {
return fmt.Errorf("serializing payload: %w", err) return fmt.Errorf("serializing payload: %w", err)
@@ -384,7 +464,9 @@ func bindPort(ctx context.Context, client *http.Client, apiIPAddress netip.Addr,
} }
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode != http.StatusOK { switch response.StatusCode {
case http.StatusOK, http.StatusConflict:
default:
return makeNOKStatusError(response, errSubstitutions) return makeNOKStatusError(response, errSubstitutions)
} }
@@ -397,11 +479,24 @@ func bindPort(ctx context.Context, client *http.Client, apiIPAddress netip.Addr,
return fmt.Errorf("decoding response: from %s: %w", bindPortURL.String(), err) return fmt.Errorf("decoding response: from %s: %w", bindPortURL.String(), err)
} }
if responseData.Status != "OK" { switch response.StatusCode {
return fmt.Errorf("%w: %s: %s", ErrBadResponse, responseData.Status, responseData.Message) case http.StatusOK:
if responseData.Status != "OK" {
return fmt.Errorf("%w: status %q and message %q", ErrBadResponse, responseData.Status, responseData.Message)
}
return nil
case http.StatusConflict:
portIsBusy := regexPortBusy.FindString(responseData.Message)
if portIsBusy == "" {
return fmt.Errorf("%w response received with unexpected message %q not matching regex %q",
errPortBusy, responseData.Message, regexPortBusy.String())
}
portStr := regexNumber.FindString(portIsBusy)
rest := strings.TrimPrefix(responseData.Message, portIsBusy)
return fmt.Errorf("%w: %s - %s", errPortBusy, portStr, rest)
default:
panic("unreachable code")
} }
return nil
} }
// replaceInErr is used to remove sensitive information from errors. // replaceInErr is used to remove sensitive information from errors.
@@ -1,7 +1,6 @@
package presets package presets
const ( const (
None = "none"
Normal = "normal" Normal = "normal"
Strong = "strong" Strong = "strong"
) )
@@ -3,6 +3,7 @@ package privateinternetaccess
import ( import (
"math/rand" "math/rand"
"net/http" "net/http"
"net/netip"
"time" "time"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
@@ -17,6 +18,7 @@ type Provider struct {
common.Fetcher common.Fetcher
// Port forwarding // Port forwarding
portForwardPath string portForwardPath string
apiIP netip.Addr
} }
func New(storage common.Storage, randSource rand.Source, func New(storage common.Storage, randSource rand.Source,
@@ -36,7 +36,7 @@ type serverData struct {
func fetchAPI(ctx context.Context, client *http.Client) ( func fetchAPI(ctx context.Context, client *http.Client) (
data apiData, err error, data apiData, err error,
) { ) {
const url = "https://serverlist.piaservers.net/vpninfo/servers/v6" const url = "https://serverlist.piaservers.net/vpninfo/servers/v7"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
@@ -77,6 +77,9 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
func addData(regions []regionData, nts nameToServer) (change bool) { func addData(regions []regionData, nts nameToServer) (change bool) {
for _, region := range regions { for _, region := range regions {
if region.Offline {
continue
}
for _, server := range region.Servers.UDP { for _, server := range region.Servers.UDP {
const tcp, udp = false, true const tcp, udp = false, true
if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) { if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) {
@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"regexp" "regexp"
"strconv" "strconv"
"time"
"github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/provider/utils"
@@ -23,6 +24,12 @@ var ErrPortForwardedNotFound = errors.New("port forwarded not found")
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) ( func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
ports []uint16, err error, ports []uint16, err error,
) { ) {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 10 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
url := "https://connect.pvdatanet.com/v3/Api/port?ip[]=" + objects.InternalIP.String() url := "https://connect.pvdatanet.com/v3/Api/port?ip[]=" + objects.InternalIP.String()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
@@ -326,6 +326,7 @@ func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie coo
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err) return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
} }
c.setHeaders(request, unauthCookie) c.setHeaders(request, unauthCookie)
request.Header.Set("Content-Type", "application/json")
response, err := c.httpClient.Do(request) response, err := c.httpClient.Do(request)
if err != nil { if err != nil {
@@ -438,6 +439,7 @@ func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
return cookie{}, fmt.Errorf("creating request: %w", err) return cookie{}, fmt.Errorf("creating request: %w", err)
} }
c.setHeaders(request, unauthCookie) c.setHeaders(request, unauthCookie)
request.Header.Set("Content-Type", "application/json")
response, err := c.httpClient.Do(request) response, err := c.httpClient.Do(request)
if err != nil { if err != nil {
@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
"time"
) )
// getMostRecentStableTag finds the most recent proton-account stable tag version, // getMostRecentStableTag finds the most recent proton-account stable tag version,
@@ -18,6 +19,12 @@ func getMostRecentStableTag(ctx context.Context, client *http.Client) (version s
page := 1 page := 1
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`) regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
for ctx.Err() == nil { for ctx.Err() == nil {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
url := "https://api.github.com/repos/ProtonMail/WebClients/tags?per_page=30&page=" + fmt.Sprint(page) 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) request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+1 -1
View File
@@ -71,7 +71,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
providers.Mullvad: mullvad.New(storage, randSource, client), providers.Mullvad: mullvad.New(storage, randSource, client),
providers.Nordvpn: nordvpn.New(storage, randSource, client, updaterWarner), providers.Nordvpn: nordvpn.New(storage, randSource, client, updaterWarner),
providers.Perfectprivacy: perfectprivacy.New(storage, randSource, unzipper, updaterWarner), providers.Perfectprivacy: perfectprivacy.New(storage, randSource, unzipper, updaterWarner),
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver), providers.Privado: privado.New(storage, randSource, client, updaterWarner),
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, *credentials.ProtonEmail, *credentials.ProtonPassword), providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword),
+20 -9
View File
@@ -64,7 +64,11 @@ func OpenVPNConfig(provider OpenVPNProviderSettings,
lines.add("suppress-timestamps") // do not log timestamps, the Gluetun logger takes care of it lines.add("suppress-timestamps") // do not log timestamps, the Gluetun logger takes care of it
lines.add("dev", settings.Interface) lines.add("dev", settings.Interface)
lines.add("verb", fmt.Sprint(*settings.Verbosity)) lines.add("verb", fmt.Sprint(*settings.Verbosity))
lines.add("proto", connection.Protocol) protocol := connection.Protocol
if protocol == constants.TCP {
protocol = "tcp-client"
}
lines.add("proto", protocol)
lines.add("remote", connection.IP.String(), fmt.Sprint(connection.Port)) lines.add("remote", connection.IP.String(), fmt.Sprint(connection.Port))
if *settings.User != "" { if *settings.User != "" {
@@ -175,8 +179,8 @@ func OpenVPNConfig(provider OpenVPNProviderSettings,
lines.add("setenv", envKey, envValue) lines.add("setenv", envKey, envValue)
} }
for _, ca := range provider.CAs { if len(provider.CAs) > 0 {
lines.addLines(WrapOpenvpnCA(ca)) lines.addLines(WrapOpenvpnCAs(provider.CAs))
} }
if provider.CRLVerify != "" { if provider.CRLVerify != "" {
lines.addLines(WrapOpenvpnCRLVerify(provider.CRLVerify)) lines.addLines(WrapOpenvpnCRLVerify(provider.CRLVerify))
@@ -268,13 +272,20 @@ func defaultStringSlice(value, defaultValue []string) (
} }
func WrapOpenvpnCA(certificate string) (lines []string) { func WrapOpenvpnCA(certificate string) (lines []string) {
return []string{ return WrapOpenvpnCAs([]string{certificate})
"<ca>", }
"-----BEGIN CERTIFICATE-----",
certificate, func WrapOpenvpnCAs(certificates []string) (lines []string) {
"-----END CERTIFICATE-----", lines = append(lines, "<ca>")
"</ca>", for _, certificate := range certificates {
lines = append(lines,
"-----BEGIN CERTIFICATE-----",
certificate,
"-----END CERTIFICATE-----",
)
} }
lines = append(lines, "</ca>")
return lines
} }
func WrapOpenvpnCert(clientCertificate string) (lines []string) { func WrapOpenvpnCert(clientCertificate string) (lines []string) {
+38
View File
@@ -0,0 +1,38 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWrapOpenvpnCAs(t *testing.T) {
t.Parallel()
lines := WrapOpenvpnCAs([]string{"cert1", "cert2"})
assert.Equal(t, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
"cert1",
"-----END CERTIFICATE-----",
"-----BEGIN CERTIFICATE-----",
"cert2",
"-----END CERTIFICATE-----",
"</ca>",
}, lines)
}
func TestWrapOpenvpnCA(t *testing.T) {
t.Parallel()
lines := WrapOpenvpnCA("cert1")
assert.Equal(t, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
"cert1",
"-----END CERTIFICATE-----",
"</ca>",
}, lines)
}
+1 -1
View File
@@ -14,7 +14,7 @@ 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,
) { ) {
const url = "https://support.vyprvpn.com/hc/article_attachments/360052617332/Vypr_OpenVPN_20200320.zip" const url = "https://support.vyprvpn.com/hc/article_attachments/44585865394189"
contents, err := u.unzipper.FetchAndExtract(ctx, url) contents, err := u.unzipper.FetchAndExtract(ctx, url)
if err != nil { if err != nil {
return nil, err return nil, err
+7
View File
@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"strings" "strings"
"time"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
@@ -40,6 +41,12 @@ func (c *cloudflare) Token() (token string) {
func (c *cloudflare) FetchInfo(ctx context.Context, ip netip.Addr) ( func (c *cloudflare) FetchInfo(ctx context.Context, ip netip.Addr) (
result models.PublicIP, err error, result models.PublicIP, err error,
) { ) {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
urlBase := "https://speed.cloudflare.com" urlBase := "https://speed.cloudflare.com"
url := urlBase + "/meta" url := urlBase + "/meta"
if ip.IsValid() { if ip.IsValid() {
+7
View File
@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"strings" "strings"
"time"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
) )
@@ -44,6 +45,12 @@ func (e *echoip) Token() string {
func (e *echoip) FetchInfo(ctx context.Context, ip netip.Addr) ( func (e *echoip) FetchInfo(ctx context.Context, ip netip.Addr) (
result models.PublicIP, err error, result models.PublicIP, err error,
) { ) {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
url := e.url + "/json" url := e.url + "/json"
if ip.IsValid() { if ip.IsValid() {
url += "?ip=" + ip.String() url += "?ip=" + ip.String()
+7
View File
@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"strings" "strings"
"time"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
@@ -44,6 +45,12 @@ func (i *ip2Location) Token() string {
func (i *ip2Location) FetchInfo(ctx context.Context, ip netip.Addr) ( func (i *ip2Location) FetchInfo(ctx context.Context, ip netip.Addr) (
result models.PublicIP, err error, result models.PublicIP, err error,
) { ) {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
url := "https://api.ip2location.io/" url := "https://api.ip2location.io/"
if ip.IsValid() { if ip.IsValid() {
url += "?ip=" + ip.String() url += "?ip=" + ip.String()
+7
View File
@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"strings" "strings"
"time"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
@@ -42,6 +43,12 @@ func (i *ipInfo) Token() string {
func (i *ipInfo) FetchInfo(ctx context.Context, ip netip.Addr) ( func (i *ipInfo) FetchInfo(ctx context.Context, ip netip.Addr) (
result models.PublicIP, err error, result models.PublicIP, err error,
) { ) {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
url := "https://ipinfo.io/" url := "https://ipinfo.io/"
switch { switch {
case ip.Is6(): case ip.Is6():
@@ -3,6 +3,7 @@ package auth
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"slices"
) )
func New(settings Settings, debugLogger DebugLogger) ( func New(settings Settings, debugLogger DebugLogger) (
@@ -52,6 +53,17 @@ type authHandler struct {
} }
func (h *authHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { func (h *authHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
methods, ok := validRoutes[request.URL.Path]
if !ok {
h.logger.Debugf("url path %s is not a valid route", request.URL.Path)
http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
} else if !slices.Contains(methods, request.Method) {
h.logger.Debugf("method %s is not valid for url path %s", request.Method, request.URL.Path)
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
route := request.Method + " " + request.URL.Path route := request.Method + " " + request.URL.Path
roles := h.routeToRoles[route] roles := h.routeToRoles[route]
if len(roles) == 0 { if len(roles) == 0 {
@@ -32,13 +32,13 @@ func Test_authHandler_ServeHTTP(t *testing.T) {
}, },
makeLogger: func(ctrl *gomock.Controller) *MockDebugLogger { makeLogger: func(ctrl *gomock.Controller) *MockDebugLogger {
logger := NewMockDebugLogger(ctrl) logger := NewMockDebugLogger(ctrl)
logger.EXPECT().Debugf("no authentication role defined for route %s", "GET /b") logger.EXPECT().Debugf("url path %s is not a valid route", "/b")
return logger return logger
}, },
requestMethod: http.MethodGet, requestMethod: http.MethodGet,
requestPath: "/b", requestPath: "/b",
statusCode: http.StatusUnauthorized, statusCode: http.StatusNotFound,
responseBody: "Unauthorized\n", responseBody: "Not Found\n",
}, },
"authorized_unprotected_by_default": { "authorized_unprotected_by_default": {
settings: Settings{ settings: Settings{
@@ -64,17 +64,17 @@ func Test_authHandler_ServeHTTP(t *testing.T) {
"authorized_none": { "authorized_none": {
settings: Settings{ settings: Settings{
Roles: []Role{ Roles: []Role{
{Name: "role1", Auth: AuthNone, Routes: []string{"GET /a"}}, {Name: "role1", Auth: AuthNone, Routes: []string{"GET /v1/portforward"}},
}, },
}, },
makeLogger: func(ctrl *gomock.Controller) *MockDebugLogger { makeLogger: func(ctrl *gomock.Controller) *MockDebugLogger {
logger := NewMockDebugLogger(ctrl) logger := NewMockDebugLogger(ctrl)
logger.EXPECT().Debugf("access to route %s authorized for role %s", logger.EXPECT().Debugf("access to route %s authorized for role %s",
"GET /a", "role1") "GET /v1/portforward", "role1")
return logger return logger
}, },
requestMethod: http.MethodGet, requestMethod: http.MethodGet,
requestPath: "/a", requestPath: "/v1/portforward",
statusCode: http.StatusOK, statusCode: http.StatusOK,
}, },
} }
+51 -35
View File
@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"slices" "slices"
"strings"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
@@ -38,22 +39,27 @@ func (s *Settings) SetDefaultRole(jsonRole string) error {
return fmt.Errorf("validating default role: %w", err) return fmt.Errorf("validating default role: %w", err)
} }
authenticatedRoutes := make(map[string]struct{}, len(validRoutes)) maxRoutes := countValidRoutes()
authenticatedRoutes := make(map[string]struct{}, maxRoutes)
for _, role := range s.Roles { for _, role := range s.Roles {
for _, route := range role.Routes { for _, route := range role.Routes {
authenticatedRoutes[route] = struct{}{} authenticatedRoutes[route] = struct{}{}
} }
} }
if len(authenticatedRoutes) == len(validRoutes) { if len(authenticatedRoutes) == maxRoutes {
return nil return nil
} }
unauthenticatedRoutes := make([]string, 0, len(validRoutes)) var unauthenticatedRoutes []string
for route := range validRoutes { for urlPath, methods := range validRoutes {
_, authenticated := authenticatedRoutes[route] for _, method := range methods {
if !authenticated { route := method + " " + urlPath
unauthenticatedRoutes = append(unauthenticatedRoutes, route) _, authenticated := authenticatedRoutes[route]
if !authenticated {
unauthenticatedRoutes = append(unauthenticatedRoutes, route)
}
} }
} }
@@ -126,11 +132,12 @@ type Role struct {
} }
var ( var (
ErrMethodNotSupported = errors.New("authentication method not supported") ErrMethodNotSupported = errors.New("authentication method not supported")
ErrAPIKeyEmpty = errors.New("api key is empty") ErrAPIKeyEmpty = errors.New("api key is empty")
ErrBasicUsernameEmpty = errors.New("username is empty") ErrBasicUsernameEmpty = errors.New("username is empty")
ErrBasicPasswordEmpty = errors.New("password is empty") ErrBasicPasswordEmpty = errors.New("password is empty")
ErrRouteNotSupported = errors.New("route not supported by the control server") ErrRoutePathNotSupported = errors.New("route path not supported by the control server")
ErrRouteMethodNotSupported = errors.New("route method not supported for the path")
) )
func (r Role) Validate() (err error) { func (r Role) Validate() (err error) {
@@ -149,38 +156,47 @@ func (r Role) Validate() (err error) {
} }
for i, route := range r.Routes { for i, route := range r.Routes {
_, ok := validRoutes[route] const maxRouteFields = 2
parts := strings.SplitN(route, " ", maxRouteFields)
method, path := parts[0], parts[1]
methods, ok := validRoutes[path]
if !ok { if !ok {
return fmt.Errorf("route %d of %d: %w: %s", return fmt.Errorf("route %d of %d: %w: %s",
i+1, len(r.Routes), ErrRouteNotSupported, route) i+1, len(r.Routes), ErrRoutePathNotSupported, path)
} else if !slices.Contains(methods, method) {
return fmt.Errorf("route %d of %d: %w: %s for path %s",
i+1, len(r.Routes), ErrRouteMethodNotSupported, method, path)
} }
} }
return nil return nil
} }
// validRoutes maps URL paths to allowed HTTP methods.
// WARNING: do not mutate programmatically. // WARNING: do not mutate programmatically.
var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals var validRoutes = map[string][]string{ //nolint:gochecknoglobals
http.MethodGet + " /openvpn/actions/restart": {}, "/openvpn/actions/restart": {http.MethodGet},
http.MethodGet + " /openvpn/portforwarded": {}, "/openvpn/portforwarded": {http.MethodGet},
http.MethodGet + " /openvpn/settings": {}, "/openvpn/settings": {http.MethodGet},
http.MethodGet + " /unbound/actions/restart": {}, "/unbound/actions/restart": {http.MethodGet},
http.MethodGet + " /updater/restart": {}, "/updater/restart": {http.MethodGet},
http.MethodGet + " /v1/version": {}, "/v1/version": {http.MethodGet},
http.MethodGet + " /v1/vpn/status": {}, "/v1/vpn/status": {http.MethodGet, http.MethodPut},
http.MethodPut + " /v1/vpn/status": {}, "/v1/vpn/settings": {http.MethodGet, http.MethodPut},
http.MethodGet + " /v1/vpn/settings": {}, "/v1/openvpn/status": {http.MethodGet, http.MethodPut},
http.MethodPut + " /v1/vpn/settings": {}, "/v1/openvpn/portforwarded": {http.MethodGet},
http.MethodGet + " /v1/openvpn/status": {}, "/v1/openvpn/settings": {http.MethodGet},
http.MethodPut + " /v1/openvpn/status": {}, "/v1/dns/status": {http.MethodGet, http.MethodPut},
http.MethodGet + " /v1/openvpn/portforwarded": {}, "/v1/updater/status": {http.MethodGet, http.MethodPut},
http.MethodGet + " /v1/openvpn/settings": {}, "/v1/publicip/ip": {http.MethodGet},
http.MethodGet + " /v1/dns/status": {}, "/v1/portforward": {http.MethodGet, http.MethodPut},
http.MethodPut + " /v1/dns/status": {}, }
http.MethodGet + " /v1/updater/status": {},
http.MethodPut + " /v1/updater/status": {}, func countValidRoutes() (count int) {
http.MethodGet + " /v1/publicip/ip": {}, for _, methods := range validRoutes {
http.MethodGet + " /v1/portforward": {}, count += len(methods)
}
return count
} }
func (r Role) ToLinesNode() (node *gotree.Node) { func (r Role) ToLinesNode() (node *gotree.Node) {
+3 -8
View File
@@ -34,14 +34,9 @@ func (h *portForwardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *portForwardHandler) getPortForwarded(w http.ResponseWriter) { func (h *portForwardHandler) getPortForwarded(w http.ResponseWriter) {
ports := h.portForward.GetPortsForwarded() ports := h.portForward.GetPortsForwarded()
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
var data any data := portsWrapper{Ports: ports}
switch len(ports) { if len(ports) > 0 {
case 0: data.Port = ports[0] // TODO v4 remove
data = portWrapper{Port: 0} // TODO v4 change to portsWrapper
case 1:
data = portWrapper{Port: ports[0]} // TODO v4 change to portsWrapper
default:
data = portsWrapper{Ports: ports}
} }
err := encoder.Encode(data) err := encoder.Encode(data)
+1 -4
View File
@@ -25,11 +25,8 @@ func (sw *statusWrapper) getStatus() (status models.LoopStatus, err error) {
} }
} }
type portWrapper struct { // TODO v4 remove
Port uint16 `json:"port"`
}
type portsWrapper struct { type portsWrapper struct {
Port uint16 `json:"port"` // TODO v4 remove
Ports []uint16 `json:"ports"` Ports []uint16 `json:"ports"`
} }
+8959 -6953
View File
File diff suppressed because it is too large Load Diff
-46
View File
@@ -1,46 +0,0 @@
//go:build linux || darwin
package tun
import (
"errors"
"fmt"
"os"
"syscall"
)
var (
ErrTUNInfo = errors.New("cannot get syscall stat info of TUN file")
ErrTUNBadRdev = errors.New("TUN file has an unexpected rdev")
)
// Check checks the tunnel device specified by path is present and accessible.
func (t *Tun) Check(path string) error {
f, err := os.OpenFile(path, os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("TUN device is not available: %w", err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return fmt.Errorf("getting stat information for TUN file: %w", err)
}
sys, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return fmt.Errorf("%w", ErrTUNInfo)
}
const expectedRdev = 2760 // corresponds to major 10 and minor 200
if sys.Rdev != expectedRdev {
return fmt.Errorf("%w: %d instead of expected %d",
ErrTUNBadRdev, sys.Rdev, expectedRdev)
}
if err := f.Close(); err != nil {
return fmt.Errorf("closing TUN device: %w", err)
}
return nil
}
-7
View File
@@ -1,7 +0,0 @@
//go:build !linux && !darwin
package tun
func (t *Tun) Check(path string) error {
panic("not implemented")
}
-50
View File
@@ -1,50 +0,0 @@
//go:build linux || darwin
package tun
import (
"fmt"
"math"
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
// Create creates a TUN device at the path specified.
func (t *Tun) Create(path string) (err error) {
parentDir := filepath.Dir(path)
err = os.MkdirAll(parentDir, 0o751) //nolint:mnd
if err != nil {
return err
}
const (
major = 10
minor = 200
)
dev := unix.Mkdev(major, minor)
if dev > math.MaxInt {
panic("dev is too high")
}
err = unix.Mknod(path, unix.S_IFCHR, int(dev))
if err != nil {
return fmt.Errorf("creating TUN device file node: %w", err)
}
fd, err := unix.Open(path, 0, 0)
if err != nil {
if err.Error() == "operation not permitted" {
err = fmt.Errorf("%w (did you specify --device /dev/net/tun to your container command?)", err)
}
return fmt.Errorf("unix opening TUN device file: %w", err)
}
const nonBlocking = true
err = unix.SetNonblock(fd, nonBlocking)
if err != nil {
return fmt.Errorf("setting non block to TUN device file descriptor: %w", err)
}
return nil
}
-8
View File
@@ -1,8 +0,0 @@
//go:build !linux && !darwin
package tun
// Create creates a TUN device at the path specified.
func (t *Tun) Create(path string) error {
panic("not implemented")
}
+96 -3
View File
@@ -1,7 +1,100 @@
//go:build linux || darwin
package tun package tun
type Tun struct{} import (
"errors"
"fmt"
"math"
"os"
"path/filepath"
"syscall"
func New() *Tun { "golang.org/x/sys/unix"
return &Tun{} )
func Setup() error {
const tunDevice = "/dev/net/tun"
err := check(tunDevice)
switch {
case err == nil:
return nil
case errors.Is(err, os.ErrNotExist):
err = create(tunDevice)
if err != nil {
return fmt.Errorf("creating TUN device: %w", err)
}
return nil
default:
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
}
}
// check checks the tunnel device specified by path is present and accessible.
func check(path string) error {
f, err := os.OpenFile(path, os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("TUN device is not available: %w", err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return fmt.Errorf("getting stat information for TUN file: %w", err)
}
sys, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return errors.New("cannot get syscall stat info of TUN file") //nolint:err113
}
const expectedRdev = 2760 // corresponds to major 10 and minor 200
if sys.Rdev != expectedRdev {
return fmt.Errorf("TUN file has an unexpected rdev: %d instead of expected %d", //nolint:err113
sys.Rdev, expectedRdev)
}
if err := f.Close(); err != nil {
return fmt.Errorf("closing TUN device: %w", err)
}
return nil
}
// create creates a TUN device at the path specified.
func create(path string) (err error) {
parentDir := filepath.Dir(path)
err = os.MkdirAll(parentDir, 0o751) //nolint:mnd
if err != nil {
return err
}
const (
major = 10
minor = 200
)
dev := unix.Mkdev(major, minor)
if dev > math.MaxInt {
panic("dev is too high")
}
err = unix.Mknod(path, unix.S_IFCHR, int(dev))
if err != nil {
return fmt.Errorf("creating TUN device file node: %w", err)
}
fd, err := unix.Open(path, 0, 0)
if err != nil {
if err.Error() == "operation not permitted" {
err = fmt.Errorf("%w (did you specify --device /dev/net/tun to your container command?)", err)
}
return fmt.Errorf("unix opening TUN device file: %w", err)
}
const nonBlocking = true
err = unix.SetNonblock(fd, nonBlocking)
if err != nil {
return fmt.Errorf("setting non block to TUN device file descriptor: %w", err)
}
return nil
} }
+6 -8
View File
@@ -10,20 +10,18 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func Test_Tun(t *testing.T) { func Test_Setup(t *testing.T) {
t.Parallel() t.Parallel()
path := getTempPath(t) path := getTempPath(t)
tun := New()
defer func() { defer func() {
err := os.RemoveAll(path) err := os.RemoveAll(path)
require.NoError(t, err) require.NoError(t, err)
}() }()
// No file check fail // No file check fail
err := tun.Check(path) err := check(path)
require.Error(t, err) require.Error(t, err)
expectedMessage := "TUN device is not available: open " + path + ": no such file or directory" expectedMessage := "TUN device is not available: open " + path + ": no such file or directory"
require.Equal(t, expectedMessage, err.Error()) require.Equal(t, expectedMessage, err.Error())
@@ -35,13 +33,13 @@ func Test_Tun(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Simple file check fail // Simple file check fail
err = tun.Check(path) err = check(path)
require.Error(t, err) require.Error(t, err)
expectedMessage = "TUN file has an unexpected rdev: 0 instead of expected 2760" expectedMessage = "TUN file has an unexpected rdev: 0 instead of expected 2760"
require.Equal(t, expectedMessage, err.Error()) require.Equal(t, expectedMessage, err.Error())
// Create TUN device fail as file exists // Create TUN device fail as file exists
err = tun.Create(path) err = create(path)
require.Error(t, err) require.Error(t, err)
require.EqualError(t, err, "creating TUN device file node: file exists") require.EqualError(t, err, "creating TUN device file node: file exists")
@@ -50,7 +48,7 @@ func Test_Tun(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create TUN device success // Create TUN device success
err = tun.Create(path) err = create(path)
if err != nil && strings.HasSuffix(err.Error(), "operation not permitted") { if err != nil && strings.HasSuffix(err.Error(), "operation not permitted") {
t.Skip("You do not have root privileges to create a TUN device, skipping test") t.Skip("You do not have root privileges to create a TUN device, skipping test")
return return
@@ -58,7 +56,7 @@ func Test_Tun(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Check TUN device success // Check TUN device success
err = tun.Check(path) err = check(path)
require.NoError(t, err) require.NoError(t, err)
} }
+12
View File
@@ -0,0 +1,12 @@
//go:build !linux && !darwin
package tun
import (
"fmt"
"runtime"
)
func Setup() error {
return fmt.Errorf("not implemented for %s", runtime.GOOS)
}
+1 -1
View File
@@ -25,7 +25,7 @@ func ExtractProto(b []byte) (tcp, udp bool, err error) {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
s = strings.ToLower(s) s = strings.ToLower(s)
switch s { switch s {
case "tcp", "tcp4", "tcp6": case "tcp", "tcp4", "tcp6", "tcp-client":
return true, false, nil return true, false, nil
case "udp", "udp4", "udp6": case "udp", "udp4", "udp6":
return false, true, nil return false, true, nil
+10
View File
@@ -0,0 +1,10 @@
package resolver
import (
"context"
"net"
)
type Dialer interface {
Dial(ctx context.Context, network, address string) (net.Conn, error)
}
+2 -7
View File
@@ -1,17 +1,12 @@
package resolver package resolver
import ( import (
"context"
"net" "net"
) )
func newResolver(resolverAddress string) *net.Resolver { func newResolver(d Dialer) *net.Resolver {
d := net.Dialer{}
resolverAddress = net.JoinHostPort(resolverAddress, "53")
return &net.Resolver{ return &net.Resolver{
PreferGo: true, PreferGo: true,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { Dial: d.Dial,
return d.DialContext(ctx, "udp", resolverAddress)
},
} }
} }
+2 -2
View File
@@ -11,9 +11,9 @@ type Parallel struct {
repeatResolver *Repeat repeatResolver *Repeat
} }
func NewParallelResolver(resolverAddress string) *Parallel { func NewParallelResolver(dialer Dialer) *Parallel {
return &Parallel{ return &Parallel{
repeatResolver: NewRepeat(resolverAddress), repeatResolver: NewRepeat(dialer),
} }
} }
+2 -2
View File
@@ -14,9 +14,9 @@ type Repeat struct {
resolver *net.Resolver resolver *net.Resolver
} }
func NewRepeat(resolverAddress string) *Repeat { func NewRepeat(dialer Dialer) *Repeat {
return &Repeat{ return &Repeat{
resolver: newResolver(resolverAddress), resolver: newResolver(dialer),
} }
} }
+12
View File
@@ -28,6 +28,12 @@ type githubCommit struct {
var errHTTPStatusCode = errors.New("bad response HTTP status code") var errHTTPStatusCode = errors.New("bad response HTTP status code")
func getGithubReleases(ctx context.Context, client *http.Client) (releases []githubRelease, err error) { func getGithubReleases(ctx context.Context, client *http.Client) (releases []githubRelease, err error) {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
const url = "https://api.github.com/repos/qdm12/gluetun/releases" const url = "https://api.github.com/repos/qdm12/gluetun/releases"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
@@ -53,6 +59,12 @@ func getGithubReleases(ctx context.Context, client *http.Client) (releases []git
} }
func getGithubCommits(ctx context.Context, client *http.Client) (commits []githubCommit, err error) { func getGithubCommits(ctx context.Context, client *http.Client) (commits []githubCommit, err error) {
// Define a timeout since the default client has a large timeout and we don't
// want to wait too long.
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
const url = "https://api.github.com/repos/qdm12/gluetun/commits" const url = "https://api.github.com/repos/qdm12/gluetun/commits"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
+2 -1
View File
@@ -101,7 +101,8 @@ type CmdStarter interface {
} }
type HealthChecker interface { type HealthChecker interface {
SetConfig(tlsDialAddrs []string, icmpTargetIPs []netip.Addr, smallCheckType string) SetConfig(tlsDialAddrs []string, icmpTargetIPs []netip.Addr,
smallCheckType string, startupOnFail bool)
Start(ctx context.Context) (runError <-chan error, err error) Start(ctx context.Context) (runError <-chan error, err error)
Stop() error Stop() error
} }
+7
View File
@@ -43,6 +43,7 @@ type Loop struct {
start <-chan struct{} start <-chan struct{}
running chan<- models.LoopStatus running chan<- models.LoopStatus
userTrigger bool userTrigger bool
healthDone <-chan struct{}
// Internal constant values // Internal constant values
backoffTime time.Duration backoffTime time.Duration
} }
@@ -68,6 +69,11 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped) statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
state := state.New(statusManager, vpnSettings) state := state.New(statusManager, vpnSettings)
// Initialize healthDone channel to a closed channel so that the first time
// we call <-l.healthDone it does not block
healthDone := make(chan struct{})
close(healthDone)
return &Loop{ return &Loop{
statusManager: statusManager, statusManager: statusManager,
state: state, state: state,
@@ -95,6 +101,7 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1
stop: stop, stop: stop,
stopped: stopped, stopped: stopped,
userTrigger: true, userTrigger: true,
healthDone: healthDone,
backoffTime: defaultBackoffTime, backoffTime: defaultBackoffTime,
} }
} }
+6
View File
@@ -8,6 +8,7 @@ import (
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/tun"
) )
// setupOpenVPN sets OpenVPN up using the configurators and settings given. // setupOpenVPN sets OpenVPN up using the configurators and settings given.
@@ -17,6 +18,11 @@ func setupOpenVPN(ctx context.Context, fw Firewall,
settings settings.VPN, ipv6Supported bool, starter CmdStarter, settings settings.VPN, ipv6Supported bool, starter CmdStarter,
logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, err error, logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, err error,
) { ) {
err = tun.Setup()
if err != nil {
return nil, models.Connection{}, fmt.Errorf("setting up tun device: %w", err)
}
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported) connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
if err != nil { if err != nil {
return nil, models.Connection{}, fmt.Errorf("finding a valid server connection: %w", err) return nil, models.Connection{}, fmt.Errorf("finding a valid server connection: %w", err)
+12 -3
View File
@@ -31,12 +31,13 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
} }
} }
<-l.healthDone // make sure the health checker is stopped before restarting it
icmpTargetIPs := l.healthSettings.ICMPTargetIPs icmpTargetIPs := l.healthSettings.ICMPTargetIPs
if len(icmpTargetIPs) == 1 && icmpTargetIPs[0].IsUnspecified() { if len(icmpTargetIPs) == 1 && icmpTargetIPs[0].IsUnspecified() {
icmpTargetIPs = []netip.Addr{data.serverIP} icmpTargetIPs = []netip.Addr{data.serverIP}
} }
l.healthChecker.SetConfig(l.healthSettings.TargetAddresses, icmpTargetIPs, l.healthChecker.SetConfig(l.healthSettings.TargetAddresses, icmpTargetIPs,
l.healthSettings.SmallCheckType) l.healthSettings.SmallCheckType, !*l.healthSettings.RestartVPN)
healthErrCh, err := l.healthChecker.Start(ctx) healthErrCh, err := l.healthChecker.Start(ctx)
l.healthServer.SetError(err) l.healthServer.SetError(err)
@@ -54,7 +55,12 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
// Start collecting health errors asynchronously, since // Start collecting health errors asynchronously, since
// we should not wait for the code below to complete // we should not wait for the code below to complete
// to start monitoring health and auto-healing. // to start monitoring health and auto-healing.
go l.collectHealthErrors(ctx, loopCtx, healthErrCh) // We keep track of when this goroutine is done with the healthDone
// channel to avoid a race condition where the health checker is stopped
// after being reconfigured and restarted, instead of before.
healthDone := make(chan struct{})
l.healthDone = healthDone
go l.collectHealthErrors(ctx, loopCtx, healthDone, healthErrCh)
if *l.dnsLooper.GetSettings().ServerEnabled { if *l.dnsLooper.GetSettings().ServerEnabled {
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running) _, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
@@ -86,7 +92,10 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
} }
} }
func (l *Loop) collectHealthErrors(ctx, loopCtx context.Context, healthErrCh <-chan error) { func (l *Loop) collectHealthErrors(ctx, loopCtx context.Context,
done chan<- struct{}, healthErrCh <-chan error,
) {
defer close(done)
var previousHealthErr error var previousHealthErr error
for { for {
select { select {
+12
View File
@@ -7,6 +7,7 @@ import (
"net" "net"
"github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/netlink"
gtun "github.com/qdm12/gluetun/internal/tun"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/device"
@@ -37,15 +38,18 @@ func (w *Wireguard) Run(ctx context.Context, waitError chan<- error, ready chan<
kernelSupported := w.netlink.IsWireguardSupported() kernelSupported := w.netlink.IsWireguardSupported()
setupFunction := setupUserSpace setupFunction := setupUserSpace
userspace := false
switch w.settings.Implementation { switch w.settings.Implementation {
case "auto": //nolint:goconst case "auto": //nolint:goconst
if !kernelSupported { if !kernelSupported {
w.logger.Info("Using userspace implementation since Kernel support does not exist") w.logger.Info("Using userspace implementation since Kernel support does not exist")
userspace = true
break break
} }
w.logger.Info("Using available kernelspace implementation") w.logger.Info("Using available kernelspace implementation")
setupFunction = setupKernelSpace setupFunction = setupKernelSpace
case "userspace": case "userspace":
userspace = true
case "kernelspace": case "kernelspace":
if !kernelSupported { if !kernelSupported {
waitError <- fmt.Errorf("%w", ErrKernelSupport) waitError <- fmt.Errorf("%w", ErrKernelSupport)
@@ -56,6 +60,14 @@ func (w *Wireguard) Run(ctx context.Context, waitError chan<- error, ready chan<
panic(fmt.Sprintf("unknown implementation %q", w.settings.Implementation)) panic(fmt.Sprintf("unknown implementation %q", w.settings.Implementation))
} }
if userspace {
err := gtun.Setup()
if err != nil {
waitError <- fmt.Errorf("setting up userspace tun device: %w", err)
return
}
}
client, err := wgctrl.New() client, err := wgctrl.New()
if err != nil { if err != nil {
waitError <- fmt.Errorf("%w: %s", ErrWgctrlOpen, err) waitError <- fmt.Errorf("%w: %s", ErrWgctrlOpen, err)