Compare commits

...

53 Commits

Author SHA1 Message Date
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
Quentin McGaw 7f22fb3276 fix(protonvpn): support port 51820 for UDP OpenVPN 2026-02-11 14:13:34 +00:00
Quentin McGaw 6909a0c123 fix(healthcheck): prevent race condition and fix #3096 (#3123) 2026-02-11 14:12:20 +00:00
Quentin McGaw 3e1f48932a fix(openvpn): only log openvpn version corresponding to OPENVPN_VERSION 2026-02-11 14:12:08 +00:00
Chris Duck 50744852c5 fix(protonvpn): update OpenVPN settings (#3120) 2026-02-11 14:11:57 +00:00
Quentin McGaw 09e52bc685 fix(httpproxy): remove info log when no Proxy-Authorization header is present 2026-02-11 14:11:46 +00:00
Quentin McGaw 857fe425ec fix(wireguard): fix detection of kernelspace wireguard 2026-02-11 14:11:36 +00:00
94 changed files with 10080 additions and 7677 deletions
+9 -4
View File
@@ -68,7 +68,7 @@ jobs:
verify-private:
if: |
github.repository == 'qdm12/gluetun' &&
github.repository == 'passteque/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
@@ -115,7 +115,7 @@ jobs:
publish:
if: |
github.repository == 'qdm12/gluetun' &&
github.repository == 'passteque/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
@@ -127,6 +127,7 @@ jobs:
contents: read
packages: write
runs-on: ubuntu-latest
environment: secrets
steps:
- uses: actions/checkout@v6
@@ -161,11 +162,15 @@ jobs:
with:
registry: ghcr.io
username: qdm12
password: ${{ github.token }}
password: ${{ secrets.GHCR_PAT }}
- name: Short commit
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
uses: docker/build-push-action@v6
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
config-file: .github/workflows/configs/mlc-config.json
- 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:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
+21 -27
View File
@@ -15,6 +15,8 @@ import (
_ "time/tzdata"
_ "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/cli"
"github.com/qdm12/gluetun/internal/command"
@@ -22,6 +24,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/constants"
copenvpn "github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/healthcheck"
@@ -38,7 +41,6 @@ import (
"github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun"
updater "github.com/qdm12/gluetun/internal/updater/loop"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
@@ -75,7 +77,6 @@ func main() {
logger := log.New(log.SetLevel(log.LevelInfo))
args := os.Args
tun := tun.New()
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
netLinker := netlink.New(netLinkDebugLogger)
cli := cli.New()
@@ -95,7 +96,7 @@ func main() {
errorCh := make(chan error)
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
@@ -142,7 +143,7 @@ var errCommandUnknown = errors.New("command is unknown")
//nolint:gocognit,gocyclo,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger log.LoggerInterface, reader *reader.Reader,
tun Tun, netLinker netLinker, cmder RunStarter,
netLinker netLinker, cmder RunStarter,
cli clier,
) error {
if len(args) > 1 { // cli operation
@@ -262,18 +263,21 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
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}
// Create configurators
alpineConf := alpine.New()
ovpnConf := openvpn.New(
logger.New(log.SetComponent("openvpn configurator")),
cmder, puid, pgid)
ovpnVersion := ovpnConf.Version26
if allSettings.VPN.OpenVPN.Version == copenvpn.Openvpn25 {
ovpnVersion = ovpnConf.Version25
}
err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
{name: "OpenVPN", getVersion: ovpnVersion},
{name: "IPtables", getVersion: firewallConf.Version},
})
if err != nil {
@@ -331,19 +335,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
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 _, defaultRoute := range defaultRoutes {
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
@@ -423,10 +414,18 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go healthcheckServer.Run(healthServerCtx, healthServerDone)
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"))
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
parallelResolver := resolver.NewParallelResolver(dohDialer)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
@@ -549,7 +548,7 @@ type netLinker interface {
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
IsWireguardSupported() bool
IsIPv6Supported() (ok bool, err error)
PatchLoggerLevel(level log.Level)
}
@@ -592,11 +591,6 @@ type clier interface {
GenKey(args []string) error
}
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
}
type RunStarter interface {
Run(cmd *exec.Cmd) (output string, err error)
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/pgzip v1.2.6
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/goshutdown v0.3.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/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc10 h1:IyeNEYXfhBsaE1dwxx5eAqdAz1HS98dT+8c7xoKODa0=
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 h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
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/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
+20 -3
View File
@@ -10,6 +10,8 @@ import (
"strings"
"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/constants"
"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 {
options := settings.Updater{}
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.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false,
"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
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
"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
}
if dnsServer != "" {
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
}
if !endUserMode && !maintainerMode {
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)
}
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
httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
parallelResolver := resolver.NewParallelResolver(dnsDialer)
nameTokenPairs := []api.NameToken{
{Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)},
+18 -21
View File
@@ -9,8 +9,9 @@ import (
)
// Start launches a command and streams stdout and stderr to channels.
// All the channels returned are ready only and won't be closed
// if the command fails later.
// stdoutLines and stderrLines channels will be closed when there is no more
// 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) (
stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error,
@@ -21,7 +22,6 @@ func (c *Cmder) Start(cmd *exec.Cmd) (
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error,
) {
stop := make(chan struct{})
stdoutReady := make(chan struct{})
stdoutLinesCh := make(chan string)
stdoutDone := make(chan struct{})
@@ -33,43 +33,47 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
if err != nil {
return nil, nil, nil, err
}
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
go streamToChannel(stdoutReady, stdoutDone, stdout, stdoutLinesCh)
stderr, err := cmd.StderrPipe()
if err != nil {
_ = stdout.Close()
close(stop)
<-stdoutDone
close(stdoutLinesCh)
return nil, nil, nil, err
}
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
err = cmd.Start()
if err != nil {
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone
close(stdoutLinesCh)
_ = stderr.Close()
<-stderrDone
close(stderrLinesCh)
return nil, nil, nil, err
}
waitErrorCh := make(chan error)
go func() {
err := cmd.Wait()
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone
close(stdoutLinesCh)
_ = stdout.Close()
<-stderrDone
close(stderrLinesCh)
_ = stderr.Close()
waitErrorCh <- err
}()
<-stdoutReady
<-stderrReady
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
}
func streamToChannel(ready chan<- struct{},
stop <-chan struct{}, done chan<- struct{},
func streamToChannel(ready chan<- struct{}, done chan<- struct{},
stream io.Reader, lines chan<- string,
) {
defer close(done)
@@ -89,12 +93,5 @@ func streamToChannel(ready chan<- struct{},
if err == nil || errors.Is(err, os.ErrClosed) {
return
}
// ignore the error if it is stopped.
select {
case <-stop:
return
default:
lines <- "stream error: " + err.Error()
}
lines <- "stream error: " + err.Error()
}
+42 -24
View File
@@ -89,30 +89,48 @@ func Test_start(t *testing.T) {
require.NoError(t, err)
var stdoutIndex, stderrIndex int
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)
collectAndCheckChannels(t, stdoutLines, stderrLines, waitError,
testCase.stdout, testCase.stderr, testCase.waitErr)
})
}
}
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.Ipvanish,
providers.Perfectprivacy,
providers.Privado,
providers.Vyprvpn,
) {
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:
// no custom port allowed
case providers.Expressvpn, providers.Fastestvpn,
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
providers.Privado, providers.Purevpn,
providers.Giganews, providers.Ipvanish,
providers.Nordvpn, providers.Purevpn,
providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s",
@@ -99,12 +98,15 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
case providers.Perfectprivacy:
allowedTCP = []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:
allowedTCP = []uint16{80, 110, 443}
allowedTCP = []uint16{80, 110, 443, 501, 502, 8443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060, 51820}
case providers.SlickVPN:
allowedTCP = []uint16{443, 8080, 8888}
allowedUDP = []uint16{443, 8080, 8888}
@@ -130,7 +132,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{
presets.None,
presets.Normal,
presets.Strong,
}
@@ -21,10 +21,6 @@ type Updater struct {
// updater. It cannot be nil in the internal state.
// TODO change to value and add Enabled field.
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
// find per provider, compared to the total current
// number of servers. It defaults to 0.8.
@@ -76,7 +72,6 @@ func (u Updater) Validate() (err error) {
func (u *Updater) copy() (copied Updater) {
return Updater{
Period: gosettings.CopyPointer(u.Period),
DNSAddress: u.DNSAddress,
MinRatio: u.MinRatio,
Providers: gosettings.CopySlice(u.Providers),
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
@@ -89,7 +84,6 @@ func (u *Updater) copy() (copied Updater) {
// settings.
func (u *Updater) overrideWith(other Updater) {
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.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
@@ -98,7 +92,6 @@ func (u *Updater) overrideWith(other Updater) {
func (u *Updater) SetDefaults(vpnProvider string) {
u.Period = gosettings.DefaultPointer(u.Period, 0)
u.DNSAddress = gosettings.DefaultComparable(u.DNSAddress, "1.1.1.1:53")
if u.MinRatio == 0 {
const defaultMinRatio = 0.8
@@ -125,7 +118,6 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server data updater settings:")
node.Appendf("Update period: %s", *u.Period)
node.Appendf("DNS address: %s", u.DNSAddress)
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
if slices.Contains(u.Providers, providers.Protonvpn) {
@@ -142,11 +134,6 @@ func (u *Updater) read(r *reader.Reader) (err error) {
return err
}
u.DNSAddress, err = readUpdaterDNSAddress()
if err != nil {
return err
}
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
if err != nil {
return err
@@ -166,12 +153,3 @@ func (u *Updater) read(r *reader.Reader) (err error) {
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
// or the defaults for nordvpn will not work.
for _, addressString := range addressStrings {
if !strings.ContainsRune(addressString, '/') {
addressString = strings.TrimSpace(addressString)
if addressString == "" {
continue
} else if !strings.ContainsRune(addressString, '/') {
addressString += "/32"
}
addressString = strings.TrimSpace(addressString)
address, err := netip.ParsePrefix(addressString)
if err != nil {
return fmt.Errorf("parsing address: %w", err)
@@ -3,10 +3,10 @@ package files
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"regexp"
"strings"
"gopkg.in/ini.v1"
)
@@ -82,12 +82,12 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
if endpoint != nil {
parts := strings.Split(*endpoint, ":")
endpointIP = &parts[0]
const partsWithPort = 2
if len(parts) >= partsWithPort {
endpointPort = new(string)
*endpointPort = strings.Join(parts[1:], ":")
host, port, err := net.SplitHostPort(*endpoint)
if err == nil {
endpointIP = &host
endpointPort = &port
} else {
endpointIP = endpoint
}
}
@@ -179,6 +179,12 @@ Endpoint = 1.2.3.4:51820`,
endpointIP: ptrTo("1.2.3.4"),
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 {
-6
View File
@@ -2,7 +2,6 @@ package dns
import (
"context"
"errors"
"github.com/qdm12/dns/v2/pkg/nameserver"
"github.com/qdm12/gluetun/internal/constants"
@@ -53,11 +52,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
if ctx.Err() != nil {
return
}
if !errors.Is(err, errUpdateBlockLists) {
const fallback = true
l.useUnencryptedDNS(fallback)
}
l.logAndWait(ctx, err)
settings = l.GetSettings()
}
+10 -6
View File
@@ -2,24 +2,28 @@ package dns
import (
"context"
"errors"
"fmt"
"net/netip"
"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/server"
)
var errUpdateBlockLists = errors.New("cannot update filter block lists")
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 {
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)
if err != nil {
+2 -2
View File
@@ -30,8 +30,9 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
lastTick = l.timeNow()
status := l.GetStatus()
settings := l.GetSettings()
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.logger.Error(err.Error())
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.Running)
settings := l.GetSettings()
timer.Reset(*settings.UpdatePeriod)
case <-l.updateTicker:
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/middlewares/filter/update"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (l *Loop) updateFiles(ctx context.Context) (err error) {
settings := l.GetSettings()
func (l *Loop) updateFiles(ctx context.Context, settings settings.DNS) (err error) {
l.logger.Info("downloading hostnames and IP block lists")
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
@@ -37,7 +36,6 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
IPPrefixes: result.BlockedIPPrefixes,
}
updateSettings.BlockHostnames(result.BlockedHostnames)
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
err = l.filter.Update(updateSettings)
if err != nil {
return fmt.Errorf("updating filter: %w", err)
+5 -6
View File
@@ -10,12 +10,11 @@ import (
)
type Config struct {
runner CmdRunner
logger Logger
iptablesMutex sync.Mutex
ip6tablesMutex sync.Mutex
defaultRoutes []routing.DefaultRoute
localNetworks []routing.LocalNetwork
runner CmdRunner
logger Logger
iptablesMutex sync.Mutex
defaultRoutes []routing.DefaultRoute
localNetworks []routing.LocalNetwork
// Fixed state
ipTables string
+2 -2
View File
@@ -36,8 +36,8 @@ func (c *Config) runIP6tablesInstruction(ctx context.Context, instruction string
if c.ip6Tables == "" {
return nil
}
c.ip6tablesMutex.Lock() // only one ip6tables command at once
defer c.ip6tablesMutex.Unlock()
c.iptablesMutex.Lock() // only one ip6tables command at once
defer c.iptablesMutex.Unlock()
if isDeleteMatchInstruction(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,
) error {
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",
appendOrDelete(remove), connection.IP, defaultInterface, protocol,
protocol, connection.Port)
@@ -218,6 +215,9 @@ func (c *Config) redirectPort(ctx context.Context, intf string,
interfaceFlag = ""
}
c.iptablesMutex.Lock()
defer c.iptablesMutex.Unlock()
err = c.runIptablesInstructions(ctx, []string{
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
+47 -19
View File
@@ -8,7 +8,6 @@ import (
"net"
"net/netip"
"strings"
"sync"
"time"
"github.com/qdm12/gluetun/internal/healthcheck/dns"
@@ -23,7 +22,7 @@ type Checker struct {
logger Logger
icmpTargetIPs []netip.Addr
smallCheckType string
configMutex sync.Mutex
startupOnFail bool
icmpNotPermitted *bool
@@ -45,26 +44,41 @@ func NewChecker(logger Logger) *Checker {
}
}
// SetConfig sets the TCP+TLS dial addresses, the ICMP echo IP address
// to target and the desired small check type (dns or icmp).
// SetConfig sets the following:
// - 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].
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.icmpTargetIPs = icmpTargets
c.smallCheckType = smallCheckType
c.startupOnFail = startupOnFail
}
// Start starts the checker by first running a blocking 6s-timed TCP+TLS check,
// and, on success, starts the periodic checks in a separate goroutine:
// Start starts the [Checker] which behaves differently according to its
// 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 "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) {
if len(c.tlsDialAddrs) == 0 || len(c.icmpTargetIPs) == 0 || c.smallCheckType == "" {
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()
// 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)
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{})
@@ -90,8 +114,6 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
smallCheckTimer := time.NewTimer(smallCheckPeriod)
const fullCheckPeriod = 5 * time.Minute
fullCheckTimer := time.NewTimer(fullCheckPeriod)
runErrorCh := make(chan error)
runError = runErrorCh
go func() {
defer close(done)
close(ready)
@@ -106,14 +128,22 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
if err != nil {
err = fmt.Errorf("small periodic check: %w", err)
}
runErrorCh <- err
select {
case <-ctx.Done():
continue
case runErrorCh <- err:
}
smallCheckTimer.Reset(smallCheckPeriod)
case <-fullCheckTimer.C:
err := c.fullPeriodicCheck(ctx)
if err != nil {
err = fmt.Errorf("full periodic check: %w", err)
}
runErrorCh <- err
select {
case <-ctx.Done():
continue
case runErrorCh <- err:
}
fullCheckTimer.Reset(fullCheckPeriod)
}
}
@@ -132,10 +162,8 @@ func (c *Checker) Stop() error {
}
func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
c.configMutex.Lock()
icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs))
copy(icmpTargetIPs, c.icmpTargetIPs)
c.configMutex.Unlock()
tryTimeouts := []time.Duration{
5 * time.Second,
5 * time.Second,
+4
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"time"
)
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 {
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
-1
View File
@@ -13,7 +13,6 @@ func (h *handler) isAuthorized(responseWriter http.ResponseWriter, request *http
}
basicAuth := request.Header.Get("Proxy-Authorization")
if basicAuth == "" {
h.logger.Info("Proxy-Authorization header not found from " + request.RemoteAddr)
responseWriter.Header().Set("Proxy-Authenticate", `Basic realm="Access to Gluetun over HTTP"`)
responseWriter.WriteHeader(http.StatusProxyAuthRequired)
return false
+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")
func getModulesInfo() (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, ", "))
}
func getModulesInfo(modulesPath string) (modulesInfo map[string]moduleInfo, err error) {
dependencyFilepath := filepath.Join(modulesPath, "modules.dep")
dependencyFile, err := os.Open(dependencyFilepath)
if err != nil {
@@ -109,6 +80,39 @@ func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) {
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 {
file, err := os.Open(filepath.Join(modulesDirPath, "modules.builtin"))
if err != nil {
+39 -2
View File
@@ -1,12 +1,49 @@
package mod
import (
"errors"
"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 {
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 {
return fmt.Errorf("getting modules information: %w", err)
}
+11 -1
View File
@@ -1,8 +1,18 @@
package netlink
import "net/netip"
import (
"net/netip"
"github.com/qdm12/log"
)
func makeNetipPrefix(n byte) netip.Prefix {
const bits = 24
return netip.PrefixFrom(netip.AddrFrom4([4]byte{n, n, n, 0}), bits)
}
type noopLogger struct{}
func (l *noopLogger) Debug(_ string) {}
func (l *noopLogger) Debugf(_ string, _ ...any) {}
func (l *noopLogger) Patch(_ ...log.Option) {}
+9 -26
View File
@@ -3,52 +3,35 @@
package netlink
import (
"fmt"
"github.com/qdm12/gluetun/internal/mod"
"github.com/vishvananda/netlink"
)
func (n *NetLink) IsWireguardSupported() (ok bool, err error) {
func (n *NetLink) IsWireguardSupported() bool {
// Check for Wireguard family without loading the wireguard module.
// Some kernels have the wireguard module built-in, and don't have a
// modules directory, such as WSL2 kernels.
ok, err = hasWireguardFamily()
if err != nil {
return false, fmt.Errorf("checking for wireguard family: %w", err)
}
ok := hasWireguardFamily()
if ok {
return true, nil
return true
}
// Try loading the wireguard module, since some systems do not load
// it after a boot. If this fails, wireguard is assumed to not be supported.
n.debugLogger.Debugf("wireguard family not found, trying to load wireguard kernel module")
err = mod.Probe("wireguard")
err := mod.Probe("wireguard")
if err != nil {
n.debugLogger.Debugf("failed loading wireguard kernel module: %s", err)
return false, nil
return false
}
n.debugLogger.Debugf("wireguard kernel module loaded successfully")
// Re-check if the Wireguard family is now available, after loading
// the wireguard kernel module.
ok, err = hasWireguardFamily()
if err != nil {
return false, fmt.Errorf("checking for wireguard family: %w", err)
}
return ok, nil
return hasWireguardFamily()
}
func hasWireguardFamily() (ok bool, err error) {
families, err := netlink.GenlFamilyList()
if err != nil {
return false, fmt.Errorf("listing gen 1 families: %w", err)
}
for _, family := range families {
if family.Name == "wireguard" {
return true, nil
}
}
return false, nil
func hasWireguardFamily() bool {
_, err := netlink.GenlFamilyGet("wireguard")
return err == nil
}
+5 -7
View File
@@ -4,17 +4,15 @@ package netlink
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_NetLink_IsWireguardSupported(t *testing.T) {
t.Skip() // TODO unskip once the data race problem with netlink.GenlFamilyList() is fixed
t.Parallel()
netLink := &NetLink{}
ok, err := netLink.IsWireguardSupported()
require.NoError(t, err)
netLink := &NetLink{
debugLogger: &noopLogger{},
}
ok := netLink.IsWireguardSupported()
if ok { // cannot assert since this depends on kernel
t.Log("wireguard is supported")
} else {
+24 -15
View File
@@ -55,6 +55,8 @@ func extractDataFromLines(lines []string) (
func extractDataFromLine(line string) (
ip netip.Addr, port uint16, protocol string, err error,
) {
line = strings.TrimSpace(line)
switch {
case strings.HasPrefix(line, "proto "):
protocol, err = extractProto(line)
@@ -81,10 +83,7 @@ func extractDataFromLine(line string) (
return ip, 0, "", nil
}
var (
errProtoLineFieldsCount = errors.New("proto line has not 2 fields as expected")
errProtocolNotSupported = errors.New("network protocol not supported")
)
var errProtoLineFieldsCount = errors.New("proto line has not 2 fields as expected")
func extractProto(line string) (protocol string, err error) {
fields := strings.Fields(line)
@@ -92,13 +91,25 @@ func extractProto(line string) (protocol string, err error) {
return "", fmt.Errorf("%w: %s", errProtoLineFieldsCount, line)
}
switch fields[1] {
case "tcp", "tcp4", "tcp6", "tcp-client", "udp", "udp4", "udp6":
default:
return "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[1])
}
return parseProto(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 (
@@ -136,11 +147,9 @@ func extractRemote(line string) (ip netip.Addr, port uint16,
}
if n > 3 { //nolint:mnd
switch fields[3] {
case "tcp", "udp":
protocol = fields[3]
default:
return netip.Addr{}, 0, "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[3])
protocol, err = parseProto(fields[3])
if err != nil {
return netip.Addr{}, 0, "", fmt.Errorf("parsing protocol from remote line: %w", err)
}
}
+10 -2
View File
@@ -62,6 +62,14 @@ func Test_extractDataFromLines(t *testing.T) {
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 {
@@ -105,7 +113,7 @@ func Test_extractDataFromLine(t *testing.T) {
},
"tcp-client": {
line: "proto tcp-client",
protocol: "tcp-client",
protocol: constants.TCP,
},
"extract remote error": {
line: "remote bad",
@@ -239,7 +247,7 @@ func Test_extractRemote(t *testing.T) {
},
"invalid protocol": {
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": {
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
}
streamCtx, streamCancel := context.WithCancel(context.Background())
streamDone := make(chan struct{})
go streamLines(streamCtx, streamDone, r.logger,
go streamLines(streamDone, r.logger,
stdoutLines, stderrLines, ready)
select {
case <-ctx.Done():
<-waitError
streamCancel()
<-streamDone
errCh <- ctx.Err()
case err := <-waitError:
streamCancel()
<-streamDone
errCh <- err
}
+20 -9
View File
@@ -1,26 +1,37 @@
package openvpn
import (
"context"
"strings"
)
func streamLines(ctx context.Context, done chan<- struct{},
func streamLines(done chan<- struct{},
logger Logger, stdout, stderr <-chan string,
tunnelReady chan<- struct{},
) {
defer close(done)
var line string
for {
var line string
var ok bool
errLine := false
select {
case <-ctx.Done():
return
case line = <-stdout:
case line = <-stderr:
errLine = true
case line, ok = <-stdout:
if ok {
break
}
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)
if line == "" {
+1
View File
@@ -29,6 +29,7 @@ type Logger interface {
Info(s string)
Warn(s string)
Error(s string)
Errorf(format string, args ...any)
}
type Cmder interface {
+11 -2
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"sync"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/portforward/service"
@@ -85,6 +86,8 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
defer close(runDone)
var serviceRunError <-chan error
var retryAfter <-chan time.Time
const retryDelay = 5 * time.Second
for {
updateReceived := false
select {
@@ -103,10 +106,12 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
l.settingsMutex.Unlock()
case err := <-serviceRunError:
l.logger.Error(err.Error())
case <-retryAfter:
// Retry starting the service after a delay
retryAfter = nil
}
firstRun := serviceRunError == nil
if !firstRun {
if l.service != nil {
err := l.service.Stop()
if err != nil {
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)
}
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)
}
}
}
+24 -14
View File
@@ -18,7 +18,11 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
}
portsString := strings.Join(portStrings, ",")
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
commandString = strings.ReplaceAll(commandString, "{{PORT}}", portStrings[0])
var firstPort string
if len(portStrings) > 0 {
firstPort = portStrings[0]
}
commandString = strings.ReplaceAll(commandString, "{{PORT}}", firstPort)
commandString = strings.ReplaceAll(commandString, "{{VPN_INTERFACE}}", vpnInterface)
args, err := command.Split(commandString)
if err != nil {
@@ -31,31 +35,37 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
return err
}
streamCtx, streamCancel := context.WithCancel(context.Background())
streamDone := make(chan struct{})
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
go streamLines(streamDone, logger, stdout, stderr)
err = <-waitError
streamCancel()
<-streamDone
return err
}
func streamLines(ctx context.Context, done chan<- struct{},
logger Logger, stdout, stderr <-chan string,
func streamLines(done chan<- struct{}, logger Logger,
stdout, stderr <-chan string,
) {
defer close(done)
var line string
for {
select {
case <-ctx.Done():
return
case line = <-stdout:
logger.Info(line)
case line = <-stderr:
logger.Error(line)
case line, ok := <-stdout:
if ok {
logger.Info(line)
}
if stderr == nil {
return
}
stdout = nil
case line, ok := <-stderr:
if ok {
logger.Error(line)
}
if stdout == nil {
return
}
stderr = nil
}
}
}
+6 -1
View File
@@ -7,6 +7,7 @@ import (
"strings"
"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/models"
"github.com/qdm12/gluetun/internal/provider/utils"
@@ -68,7 +69,11 @@ func modifyConfig(lines []string, connection models.Connection,
}
// 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, "dev "+settings.Interface)
modified = append(modified, "mute-replay-warnings")
+8 -5
View File
@@ -17,11 +17,14 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
Ciphers: []string{
openvpn.AES256gcm, openvpn.AES256cbc, openvpn.AES128gcm,
},
Auth: openvpn.SHA512,
CAs: []string{"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"}, //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
Auth: openvpn.SHA512,
CAs: []string{
"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", //nolint:lll
"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", //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,
FastIO: true,
Fragment: 1300,
@@ -108,7 +108,6 @@ func hardcodedServers() (servers []models.Server) {
{Country: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
{Country: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
{Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"},
{Country: "Pakistan", Hostname: "pakistan-ca-version-2.expressnetw.com"},
{Country: "Panama", Hostname: "panama-ca-version-2.expressnetw.com"},
{Country: "Peru", Hostname: "peru-ca-version-2.expressnetw.com"},
{Country: "Philippines (via Singapore)", 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) (
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(),
p.storage, selection, defaults, ipv6Supported, p.randSource)
}
+3 -4
View File
@@ -2,6 +2,7 @@ package privado
import (
"math/rand"
"net/http"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
@@ -15,14 +16,12 @@ type Provider struct {
}
func New(storage common.Storage, randSource rand.Source,
ipFetcher common.IPFetcher, unzipper common.Unzipper,
updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
client *http.Client, updaterWarner common.Warner,
) *Provider {
return &Provider{
storage: storage,
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 (
"context"
"encoding/json"
"fmt"
"net/http"
"net/netip"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/updater/openvpn"
)
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
servers []models.Server, err error,
) {
if !u.ipFetcher.CanFetchAnyIP() {
return nil, fmt.Errorf("%w: %s", common.ErrIPFetcherUnsupported, u.ipFetcher.String())
}
const url = "https://privadovpn.com/apps/ovpn_configs.zip"
contents, err := u.unzipper.FetchAndExtract(ctx, url)
const url = "https://privadovpn.com/apps/servers_export.json"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
} else if len(contents) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(contents), minServers)
return nil, fmt.Errorf("creating request: %w", err)
}
hts := make(hostToServer)
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)
}
response, err := u.client.Do(request)
if err != nil {
return nil, err
}
if len(hostToIPs) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
var data struct {
Servers []struct {
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 {
return nil, err
servers = make([]models.Server, len(data.Servers))
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))
+7 -11
View File
@@ -1,23 +1,19 @@
package updater
import (
"net/http"
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
ipFetcher common.IPFetcher
unzipper common.Unzipper
parallelResolver common.ParallelResolver
warner common.Warner
client *http.Client
warner common.Warner
}
func New(ipFetcher common.IPFetcher, unzipper common.Unzipper,
warner common.Warner, parallelResolver common.ParallelResolver,
) *Updater {
func New(client *http.Client, warner common.Warner) *Updater {
return &Updater{
ipFetcher: ipFetcher,
unzipper: unzipper,
parallelResolver: parallelResolver,
warner: warner,
client: client,
warner: warner,
}
}
@@ -13,12 +13,12 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Support
// Set port defaults depending on encryption preset.
var defaults utils.ConnectionDefaults
switch *selection.OpenVPN.PIAEncPreset {
case presets.None, presets.Normal:
case presets.Normal:
defaults.OpenVPNTCPPort = 502
defaults.OpenVPNUDPPort = 1198
case presets.Strong:
defaults.OpenVPNTCPPort = 501
defaults.OpenVPNUDPPort = 1197
defaults.OpenVPNTCPPort = 8443
defaults.OpenVPNUDPPort = 8080
}
return utils.GetConnection(p.Name(),
@@ -24,16 +24,10 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
)
switch *settings.PIAEncPreset {
case presets.Normal:
providerSettings.Ciphers = []string{openvpn.AES128cbc}
providerSettings.Auth = openvpn.SHA1
providerSettings.CAs = []string{caNormalPreset}
case presets.None:
providerSettings.Ciphers = []string{"none"}
providerSettings.Auth = "none"
providerSettings.Ciphers = []string{openvpn.AES128gcm}
providerSettings.CAs = []string{caNormalPreset}
default: // strong
providerSettings.Ciphers = []string{openvpn.AES256cbc}
providerSettings.Auth = openvpn.SHA256
providerSettings.Ciphers = []string{openvpn.AES256gcm}
providerSettings.CAs = []string{"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"} //nolint:lll
}
@@ -13,6 +13,7 @@ import (
"net/netip"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -39,7 +40,6 @@ func (p *Provider) PortForward(ctx context.Context,
}
serverName := objects.ServerName
apiIP := buildAPIIPAddress(objects.Gateway)
logger := objects.Logger
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 {
client := objects.Client
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, apiIP,
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))
// First time binding
if err := bindPort(ctx, privateIPClient, apiIP, data); err != nil {
return nil, fmt.Errorf("binding port: %w", err)
for ctx.Err() == nil {
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
}
@@ -99,8 +118,6 @@ func (p *Provider) KeepPortForward(ctx context.Context,
panic("gateway is not set")
}
apiIP := buildAPIIPAddress(objects.Gateway)
privateIPClient, err := newHTTPClient(objects.ServerName)
if err != nil {
return fmt.Errorf("creating custom HTTP client: %w", err)
@@ -128,7 +145,7 @@ func (p *Provider) KeepPortForward(ctx context.Context,
}
return ctx.Err()
case <-keepAliveTimer.C:
err = bindPort(ctx, privateIPClient, apiIP, data)
err = bindPort(ctx, privateIPClient, p.apiIP, data)
if err != nil {
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() {
panic("IPv6 gateway not supported")
}
gatewayBytes := gateway.As4()
gatewayBytes[2] = 128
gatewayBytes[3] = 1
return netip.AddrFrom4(gatewayBytes)
gatewayBytes[3] = 1 // x.y.z.1
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,
@@ -260,6 +315,12 @@ func fetchToken(ctx context.Context, client *http.Client,
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.Add("username", username)
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>"}
// 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.Add("token", token)
url := url.URL{
@@ -350,9 +416,23 @@ func fetchPortForwardData(ctx context.Context, client *http.Client, apiIP netip.
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) {
// 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)
if err != nil {
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()
if response.StatusCode != http.StatusOK {
switch response.StatusCode {
case http.StatusOK, http.StatusConflict:
default:
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)
}
if responseData.Status != "OK" {
return fmt.Errorf("%w: %s: %s", ErrBadResponse, responseData.Status, responseData.Message)
switch response.StatusCode {
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.
@@ -1,7 +1,6 @@
package presets
const (
None = "none"
Normal = "normal"
Strong = "strong"
)
@@ -3,6 +3,7 @@ package privateinternetaccess
import (
"math/rand"
"net/http"
"net/netip"
"time"
"github.com/qdm12/gluetun/internal/constants/providers"
@@ -17,6 +18,7 @@ type Provider struct {
common.Fetcher
// Port forwarding
portForwardPath string
apiIP netip.Addr
}
func New(storage common.Storage, randSource rand.Source,
@@ -36,7 +36,7 @@ type serverData struct {
func fetchAPI(ctx context.Context, client *http.Client) (
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)
if err != nil {
@@ -77,6 +77,9 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
func addData(regions []regionData, nts nameToServer) (change bool) {
for _, region := range regions {
if region.Offline {
continue
}
for _, server := range region.Servers.UDP {
const tcp, udp = false, true
if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) {
@@ -9,6 +9,7 @@ import (
"net/http"
"regexp"
"strconv"
"time"
"github.com/qdm12/gluetun/internal/provider/common"
"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) (
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()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
+2 -3
View File
@@ -15,7 +15,6 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
RemoteCertTLS: true,
AuthUserPass: true,
Ciphers: []string{
openvpn.AES256cbc,
openvpn.AES256gcm,
},
Auth: openvpn.SHA512,
@@ -23,8 +22,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
TunMTUExtra: 32,
RenegDisabled: true,
KeyDirection: "1",
CAs: []string{"MIIFozCCA4ugAwIBAgIBATANBgkqhkiG9w0BAQ0FADBAMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMUHJvdG9uVlBOIEFHMRowGAYDVQQDExFQcm90b25WUE4gUm9vdCBDQTAeFw0xNzAyMTUxNDM4MDBaFw0yNzAyMTUxNDM4MDBaMEAxCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxQcm90b25WUE4gQUcxGjAYBgNVBAMTEVByb3RvblZQTiBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt+BsSsZg7+AuqTq7vDbPzfygtl9f8fLJqO4amsyOXlI7pquL5IsEZhpWyJIIvYybqS4s1/T7BbvHPLVEwlrq8A5DBIXcfuXrBbKoYkmpICGc2u1KYVGOZ9A+PH9z4Tr6OXFfXRnsbZToie8t2Xjv/dZDdUDAqeW89I/mXg3k5x08m2nfGCQDm4gCanN1r5MT7ge56z0MkY3FFGCOqRwspIEUzu1ZqGSTkG1eQiOYIrdOF5cc7n2APyvBIcfvp/W3cpTOEmEBJ7/14RnXnHo0fcx61Inx/6ZxzKkW8BMdGGQF3tF6u2M0FjVN0lLH9S0ul1TgoOS56yEJ34hrJSRTqHuar3t/xdCbKFZjyXFZFNsXVvgJu34CNLrHHTGJj9jiUfFnxWQYMo9UNUd4a3PPG1HnbG7LAjlvj5JlJ5aqO5gshdnqb9uIQeR2CdzcCJgklwRGCyDT1pm7eoivWV19YBd81vKulLzgPavu3kRRe83yl29It2hwQ9FMs5w6ZV/X6ciTKo3etkX9nBD9ZzJPsGQsBUy7CzO1jK4W01+u3ItmQS+1s4xtcFxdFY8o/q1zoqBlxpe5MQIWN6QalryiET74gMHE/S5WrPlsq/gehxsdgc6GDUXG4dk8vn6OUMa6wb5wRO3VXGEc67IYm4mDFTYiPvLaFOxtndlUWuCruKcCAwEAAaOBpzCBpDAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBSDkIaYhLVZTwyLNTetNB2qV0gkVDBoBgNVHSMEYTBfgBSDkIaYhLVZTwyLNTetNB2qV0gkVKFEpEIwQDELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFByb3RvblZQTiBBRzEaMBgGA1UEAxMRUHJvdG9uVlBOIFJvb3QgQ0GCAQEwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBDQUAA4ICAQCYr7LpvnfZXBCxVIVc2ea1fjxQ6vkTj0zMhtFs3qfeXpMRf+g1NAh4vv1UIwLsczilMt87SjpJ25pZPyS3O+/VlI9ceZMvtGXdMGfXhTDp//zRoL1cbzSHee9tQlmEm1tKFxB0wfWd/inGRjZxpJCTQh8oc7CTziHZufS+Jkfpc4Rasr31fl7mHhJahF1j/ka/OOWmFbiHBNjzmNWPQInJm+0ygFqij5qs51OEvubR8yh5Mdq4TNuWhFuTxpqoJ87VKaSOx/Aefca44Etwcj4gHb7LThidw/kyzysZiWjyrbfX/31RX7QanKiMk2RDtgZaWi/lMfsl5O+6E2lJ1vo4xv9pW8225B5XeAeXHCfjV/vrrCFqeCprNF6a3Tn/LX6VNy3jbeC+167QagBOaoDA01XPOx7OdhsbGd7cJ5VkgyycZgLnT9zrChgwjx59JQosFEG1DsaAgHfpEl/N3YPJh68N7fwN41Cjzsk39v6iZdfuet/sP7oiP5/gLmA/CIPNhdIYxaojbLjFPkftVjVPn49RqwqzJJPRN8BOyb94yhQ7KO4F3IcLT/y/dsWitY0ZH4lCnAVV/v2YjWAWS3OWyC8BFx/Jmc3WDK/yPwECUcPgHIeXiRjHnJt0Zcm23O2Q3RphpU+1SO3XixsXpOVOYP6rJIXW9bMZA1gTTlpi7A=="}, //nolint:lll
TLSAuth: "6acef03f62675b4b1bbd03e53b187727423cea742242106cb2916a8a4c8297563d22c7e5cef430b1103c6f66eb1fc5b375a672f158e2e2e936c3faa48b035a6de17beaac23b5f03b10b868d53d03521d8ba115059da777a60cbfd7b2c9c5747278a15b8f6e68a3ef7fd583ec9f398c8bd4735dab40cbd1e3c62a822e97489186c30a0b48c7c38ea32ceb056d3fa5a710e10ccc7a0ddb363b08c3d2777a3395e10c0b6080f56309192ab5aacd4b45f55da61fc77af39bd81a19218a79762c33862df55785075f37d8c71dc8a42097ee43344739a0dd48d03025b0450cf1fb5e8caeb893d9a96d1f15519bb3c4dcb40ee316672ea16c012664f8a9f11255518deb", //nolint:lll
CAs: []string{"MIIFnTCCA4WgAwIBAgIUCI574SM3Lyh47GyNl0WAOYrqb5QwDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCQ0gxHzAdBgNVBAoMFlByb3RvbiBUZWNobm9sb2dpZXMgQUcxEjAQBgNVBAsMCVByb3RvblZQTjEaMBgGA1UEAwwRUHJvdG9uVlBOIFJvb3QgQ0EwHhcNMTkxMDE3MDgwNjQxWhcNMzkxMDEyMDgwNjQxWjBeMQswCQYDVQQGEwJDSDEfMB0GA1UECgwWUHJvdG9uIFRlY2hub2xvZ2llcyBBRzESMBAGA1UECwwJUHJvdG9uVlBOMRowGAYDVQQDDBFQcm90b25WUE4gUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMkUT7zMUS5C+NjQ7YoGpVFlfbN9HFgG4JiKfHB8QxnPPRgyTi0zVOAj1ImsRilauY8Ddm5dQtd8qcApoz6oCx5cFiiSQG2uyhS/59Zl5wqIkw1o+CgwZgeWkq04lcrxhhfPgJZRFjrYVezy/Z2Ssd18s3/FFNQ+2iV1KC2Kz8eSPr50u+l9vEKsKiNGkJTdlWjoDKZM2C15i/h8Smi+PdJlx7WMTtYoVC1Fzq0raCPDQl18kspu11b6d8ECPWghKcDIIKuA0r0nGqF1GvH1AmbC/xUaNrKgz9AfioZLMP/l22tVG3KKM1ku0eYHX7NzNHgkM2JKnBBannImQQBGTAcvvUlnfF3AHx4vzx7HahpBz8ebThx2uv+vzu8lCVEcKjQObGwLbAONJN2enug8hwSSZQv7tz7onDQWlYh0El5fnkrEQGbukNnSyOqTwfobvBllIPzBqdO38eZFA0YTlH9plYjIjPjGl931lFAA3G9t0x7nxAauLXN5QVp1yoF1tzXc5kN0SFAasM9VtVEOSMaGHLKhF+IMyVX8h5IuIRC8u5O672r7cHS+Dtx87LjxypqNhmbf1TWyLJSoh0qYhMr+BbO7+N6zKRIZPI5bMXc8Be2pQwbSA4ZrDvSjFC9yDXmSuZTyVo6Bqi/KCUZeaXKof68oNxVYeGowNeQdg/znAgMBAAGjUzBRMB0GA1UdDgQWBBR44WtTuEKCaPPUltYEHZoyhJo+4TAfBgNVHSMEGDAWgBR44WtTuEKCaPPUltYEHZoyhJo+4TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBBmzCQlHxOJ6izys3TVpaze+rUkA9GejgsB2DZXIcm4Lj/SNzQsPlZRu4S0IZV253dbE1DoWlHanw5lnXwx8iU82X7jdm/5uZOwj2NqSqTbTn0WLAC6khEKKe5bPTf18UOcwN82Le3AnkwcNAaBO5/TzFQVgnVedXr2g6rmpp9gdedeEl9acB7xqfYfkrmijqYMm+xeG2rXaanch3HjweMDuZdT/Ub5G6oir0KowftlA1ytjXRg+X+yWymTpF/zGLYfSodWWjMKhpzZtRJZ+9B0pWXUyY7SuCj5T5SMIAux3NQQ46wSbHRolIlwh7zD7kBgkyLe7ByLvGFKa2Vw4PuWjqYwrRbFjb2+EKAwPu6VTWz/QQTU8oJewGFipw94Bi61zuaPvF1qZCHgYhVojRy6KcqncX2Hx9hjfVxspBZDrVH6uofCmd99GmVu+qizybWQTrPaubfc/a2jJIbXc2bRQjYj/qmjE3hTlmO3k7VEP6i8CLhEl+dX75aZw9StkqjdpIApYwX6XNDqVuGzfeTXXclk4N4aDPwPFM/Yo/eKnvlNlKbljWdMYkfx8r37aOHpchH34cv0Jb5Im+1H07ywnshXNfUhRazOpubJRHnbjDuBwWS1/Vwp5AJ+QHsPXhJdl3qHc1szJZVJb3VyAWvG/bWApKfFuZX18tiI4N0EA=="}, //nolint:lll
TLSCrypt: "6acef03f62675b4b1bbd03e53b187727423cea742242106cb2916a8a4c8297563d22c7e5cef430b1103c6f66eb1fc5b375a672f158e2e2e936c3faa48b035a6de17beaac23b5f03b10b868d53d03521d8ba115059da777a60cbfd7b2c9c5747278a15b8f6e68a3ef7fd583ec9f398c8bd4735dab40cbd1e3c62a822e97489186c30a0b48c7c38ea32ceb056d3fa5a710e10ccc7a0ddb363b08c3d2777a3395e10c0b6080f56309192ab5aacd4b45f55da61fc77af39bd81a19218a79762c33862df55785075f37d8c71dc8a42097ee43344739a0dd48d03025b0450cf1fb5e8caeb893d9a96d1f15519bb3c4dcb40ee316672ea16c012664f8a9f11255518deb", //nolint:lll
UDPLines: []string{
"fast-io",
},
@@ -326,6 +326,7 @@ func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie coo
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
}
c.setHeaders(request, unauthCookie)
request.Header.Set("Content-Type", "application/json")
response, err := c.httpClient.Do(request)
if err != nil {
@@ -438,6 +439,7 @@ func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
return cookie{}, fmt.Errorf("creating request: %w", err)
}
c.setHeaders(request, unauthCookie)
request.Header.Set("Content-Type", "application/json")
response, err := c.httpClient.Do(request)
if err != nil {
@@ -8,6 +8,7 @@ import (
"net/http"
"regexp"
"strings"
"time"
)
// 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
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
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)
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.Nordvpn: nordvpn.New(storage, randSource, client, 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.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
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("dev", settings.Interface)
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))
if *settings.User != "" {
@@ -175,8 +179,8 @@ func OpenVPNConfig(provider OpenVPNProviderSettings,
lines.add("setenv", envKey, envValue)
}
for _, ca := range provider.CAs {
lines.addLines(WrapOpenvpnCA(ca))
if len(provider.CAs) > 0 {
lines.addLines(WrapOpenvpnCAs(provider.CAs))
}
if provider.CRLVerify != "" {
lines.addLines(WrapOpenvpnCRLVerify(provider.CRLVerify))
@@ -268,13 +272,20 @@ func defaultStringSlice(value, defaultValue []string) (
}
func WrapOpenvpnCA(certificate string) (lines []string) {
return []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
certificate,
"-----END CERTIFICATE-----",
"</ca>",
return WrapOpenvpnCAs([]string{certificate})
}
func WrapOpenvpnCAs(certificates []string) (lines []string) {
lines = append(lines, "<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) {
+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) (
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)
if err != nil {
return nil, err
+7
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/netip"
"strings"
"time"
"github.com/qdm12/gluetun/internal/constants"
"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) (
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"
url := urlBase + "/meta"
if ip.IsValid() {
+7
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/netip"
"strings"
"time"
"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) (
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"
if ip.IsValid() {
url += "?ip=" + ip.String()
+7
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/netip"
"strings"
"time"
"github.com/qdm12/gluetun/internal/constants"
"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) (
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/"
if ip.IsValid() {
url += "?ip=" + ip.String()
+7
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/netip"
"strings"
"time"
"github.com/qdm12/gluetun/internal/constants"
"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) (
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/"
switch {
case ip.Is6():
-15
View File
@@ -63,21 +63,6 @@ func (mr *MockNetLinkerMockRecorder) AddrReplace(arg0, arg1 interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrReplace", reflect.TypeOf((*MockNetLinker)(nil).AddrReplace), arg0, arg1)
}
// IsWireguardSupported mocks base method.
func (m *MockNetLinker) IsWireguardSupported() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsWireguardSupported")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsWireguardSupported indicates an expected call of IsWireguardSupported.
func (mr *MockNetLinkerMockRecorder) IsWireguardSupported() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsWireguardSupported", reflect.TypeOf((*MockNetLinker)(nil).IsWireguardSupported))
}
// LinkAdd mocks base method.
func (m *MockNetLinker) LinkAdd(arg0 netlink.Link) (int, error) {
m.ctrl.T.Helper()
-1
View File
@@ -12,7 +12,6 @@ type NetLinker interface {
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
}
type Addresser interface {
@@ -3,6 +3,7 @@ package auth
import (
"fmt"
"net/http"
"slices"
)
func New(settings Settings, debugLogger DebugLogger) (
@@ -52,6 +53,17 @@ type authHandler struct {
}
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
roles := h.routeToRoles[route]
if len(roles) == 0 {
@@ -32,13 +32,13 @@ func Test_authHandler_ServeHTTP(t *testing.T) {
},
makeLogger: func(ctrl *gomock.Controller) *MockDebugLogger {
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
},
requestMethod: http.MethodGet,
requestPath: "/b",
statusCode: http.StatusUnauthorized,
responseBody: "Unauthorized\n",
statusCode: http.StatusNotFound,
responseBody: "Not Found\n",
},
"authorized_unprotected_by_default": {
settings: Settings{
@@ -64,17 +64,17 @@ func Test_authHandler_ServeHTTP(t *testing.T) {
"authorized_none": {
settings: Settings{
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 {
logger := NewMockDebugLogger(ctrl)
logger.EXPECT().Debugf("access to route %s authorized for role %s",
"GET /a", "role1")
"GET /v1/portforward", "role1")
return logger
},
requestMethod: http.MethodGet,
requestPath: "/a",
requestPath: "/v1/portforward",
statusCode: http.StatusOK,
},
}
+51 -35
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"slices"
"strings"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate"
@@ -38,22 +39,27 @@ func (s *Settings) SetDefaultRole(jsonRole string) error {
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 _, route := range role.Routes {
authenticatedRoutes[route] = struct{}{}
}
}
if len(authenticatedRoutes) == len(validRoutes) {
if len(authenticatedRoutes) == maxRoutes {
return nil
}
unauthenticatedRoutes := make([]string, 0, len(validRoutes))
for route := range validRoutes {
_, authenticated := authenticatedRoutes[route]
if !authenticated {
unauthenticatedRoutes = append(unauthenticatedRoutes, route)
var unauthenticatedRoutes []string
for urlPath, methods := range validRoutes {
for _, method := range methods {
route := method + " " + urlPath
_, authenticated := authenticatedRoutes[route]
if !authenticated {
unauthenticatedRoutes = append(unauthenticatedRoutes, route)
}
}
}
@@ -126,11 +132,12 @@ type Role struct {
}
var (
ErrMethodNotSupported = errors.New("authentication method not supported")
ErrAPIKeyEmpty = errors.New("api key is empty")
ErrBasicUsernameEmpty = errors.New("username is empty")
ErrBasicPasswordEmpty = errors.New("password is empty")
ErrRouteNotSupported = errors.New("route not supported by the control server")
ErrMethodNotSupported = errors.New("authentication method not supported")
ErrAPIKeyEmpty = errors.New("api key is empty")
ErrBasicUsernameEmpty = errors.New("username is empty")
ErrBasicPasswordEmpty = errors.New("password is empty")
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) {
@@ -149,38 +156,47 @@ func (r Role) Validate() (err error) {
}
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 {
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
}
// validRoutes maps URL paths to allowed HTTP methods.
// WARNING: do not mutate programmatically.
var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
http.MethodGet + " /openvpn/actions/restart": {},
http.MethodGet + " /openvpn/portforwarded": {},
http.MethodGet + " /openvpn/settings": {},
http.MethodGet + " /unbound/actions/restart": {},
http.MethodGet + " /updater/restart": {},
http.MethodGet + " /v1/version": {},
http.MethodGet + " /v1/vpn/status": {},
http.MethodPut + " /v1/vpn/status": {},
http.MethodGet + " /v1/vpn/settings": {},
http.MethodPut + " /v1/vpn/settings": {},
http.MethodGet + " /v1/openvpn/status": {},
http.MethodPut + " /v1/openvpn/status": {},
http.MethodGet + " /v1/openvpn/portforwarded": {},
http.MethodGet + " /v1/openvpn/settings": {},
http.MethodGet + " /v1/dns/status": {},
http.MethodPut + " /v1/dns/status": {},
http.MethodGet + " /v1/updater/status": {},
http.MethodPut + " /v1/updater/status": {},
http.MethodGet + " /v1/publicip/ip": {},
http.MethodGet + " /v1/portforward": {},
var validRoutes = map[string][]string{ //nolint:gochecknoglobals
"/openvpn/actions/restart": {http.MethodGet},
"/openvpn/portforwarded": {http.MethodGet},
"/openvpn/settings": {http.MethodGet},
"/unbound/actions/restart": {http.MethodGet},
"/updater/restart": {http.MethodGet},
"/v1/version": {http.MethodGet},
"/v1/vpn/status": {http.MethodGet, http.MethodPut},
"/v1/vpn/settings": {http.MethodGet, http.MethodPut},
"/v1/openvpn/status": {http.MethodGet, http.MethodPut},
"/v1/openvpn/portforwarded": {http.MethodGet},
"/v1/openvpn/settings": {http.MethodGet},
"/v1/dns/status": {http.MethodGet, http.MethodPut},
"/v1/updater/status": {http.MethodGet, http.MethodPut},
"/v1/publicip/ip": {http.MethodGet},
"/v1/portforward": {http.MethodGet, http.MethodPut},
}
func countValidRoutes() (count int) {
for _, methods := range validRoutes {
count += len(methods)
}
return count
}
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) {
ports := h.portForward.GetPortsForwarded()
encoder := json.NewEncoder(w)
var data any
switch len(ports) {
case 0:
data = portWrapper{Port: 0} // TODO v4 change to portsWrapper
case 1:
data = portWrapper{Port: ports[0]} // TODO v4 change to portsWrapper
default:
data = portsWrapper{Ports: ports}
data := portsWrapper{Ports: ports}
if len(ports) > 0 {
data.Port = ports[0] // TODO v4 remove
}
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 {
Port uint16 `json:"port"` // TODO v4 remove
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
type Tun struct{}
import (
"errors"
"fmt"
"math"
"os"
"path/filepath"
"syscall"
func New() *Tun {
return &Tun{}
"golang.org/x/sys/unix"
)
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"
)
func Test_Tun(t *testing.T) {
func Test_Setup(t *testing.T) {
t.Parallel()
path := getTempPath(t)
tun := New()
defer func() {
err := os.RemoveAll(path)
require.NoError(t, err)
}()
// No file check fail
err := tun.Check(path)
err := check(path)
require.Error(t, err)
expectedMessage := "TUN device is not available: open " + path + ": no such file or directory"
require.Equal(t, expectedMessage, err.Error())
@@ -35,13 +33,13 @@ func Test_Tun(t *testing.T) {
require.NoError(t, err)
// Simple file check fail
err = tun.Check(path)
err = check(path)
require.Error(t, err)
expectedMessage = "TUN file has an unexpected rdev: 0 instead of expected 2760"
require.Equal(t, expectedMessage, err.Error())
// Create TUN device fail as file exists
err = tun.Create(path)
err = create(path)
require.Error(t, err)
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)
// Create TUN device success
err = tun.Create(path)
err = create(path)
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")
return
@@ -58,7 +56,7 @@ func Test_Tun(t *testing.T) {
require.NoError(t, err)
// Check TUN device success
err = tun.Check(path)
err = check(path)
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.ToLower(s)
switch s {
case "tcp", "tcp4", "tcp6":
case "tcp", "tcp4", "tcp6", "tcp-client":
return true, false, nil
case "udp", "udp4", "udp6":
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
import (
"context"
"net"
)
func newResolver(resolverAddress string) *net.Resolver {
d := net.Dialer{}
resolverAddress = net.JoinHostPort(resolverAddress, "53")
func newResolver(d Dialer) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
return d.DialContext(ctx, "udp", resolverAddress)
},
Dial: d.Dial,
}
}
+2 -2
View File
@@ -11,9 +11,9 @@ type Parallel struct {
repeatResolver *Repeat
}
func NewParallelResolver(resolverAddress string) *Parallel {
func NewParallelResolver(dialer Dialer) *Parallel {
return &Parallel{
repeatResolver: NewRepeat(resolverAddress),
repeatResolver: NewRepeat(dialer),
}
}
+2 -2
View File
@@ -14,9 +14,9 @@ type Repeat struct {
resolver *net.Resolver
}
func NewRepeat(resolverAddress string) *Repeat {
func NewRepeat(dialer Dialer) *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")
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"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, 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) {
// 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"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
+3 -2
View File
@@ -61,7 +61,7 @@ type NetLinker interface {
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
IsWireguardSupported() bool
}
type Router interface {
@@ -101,7 +101,8 @@ type CmdStarter 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)
Stop() error
}
+7
View File
@@ -43,6 +43,7 @@ type Loop struct {
start <-chan struct{}
running chan<- models.LoopStatus
userTrigger bool
healthDone <-chan struct{}
// Internal constant values
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)
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{
statusManager: statusManager,
state: state,
@@ -95,6 +101,7 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1
stop: stop,
stopped: stopped,
userTrigger: true,
healthDone: healthDone,
backoffTime: defaultBackoffTime,
}
}
+6
View File
@@ -8,6 +8,7 @@ import (
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/tun"
)
// 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,
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)
if err != nil {
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
if len(icmpTargetIPs) == 1 && icmpTargetIPs[0].IsUnspecified() {
icmpTargetIPs = []netip.Addr{data.serverIP}
}
l.healthChecker.SetConfig(l.healthSettings.TargetAddresses, icmpTargetIPs,
l.healthSettings.SmallCheckType)
l.healthSettings.SmallCheckType, !*l.healthSettings.RestartVPN)
healthErrCh, err := l.healthChecker.Start(ctx)
l.healthServer.SetError(err)
@@ -54,7 +55,12 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
// Start collecting health errors asynchronously, since
// we should not wait for the code below to complete
// 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 {
_, _ = 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
for {
select {
+1 -1
View File
@@ -9,7 +9,7 @@ type NetLinker interface {
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
IsWireguardSupported() bool
}
type Router interface {
+2 -3
View File
@@ -49,12 +49,11 @@ func (mr *MockNetLinkerMockRecorder) AddrReplace(arg0, arg1 interface{}) *gomock
}
// IsWireguardSupported mocks base method.
func (m *MockNetLinker) IsWireguardSupported() (bool, error) {
func (m *MockNetLinker) IsWireguardSupported() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsWireguardSupported")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
return ret0
}
// IsWireguardSupported indicates an expected call of IsWireguardSupported.
+13 -6
View File
@@ -7,6 +7,7 @@ import (
"net"
"github.com/qdm12/gluetun/internal/netlink"
gtun "github.com/qdm12/gluetun/internal/tun"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
@@ -16,7 +17,6 @@ import (
)
var (
ErrDetectKernel = errors.New("cannot detect Kernel support")
ErrCreateTun = errors.New("cannot create TUN device")
ErrAddLink = errors.New("cannot add Wireguard link")
ErrFindLink = errors.New("cannot find link")
@@ -35,22 +35,21 @@ var (
// See https://git.zx2c4.com/wireguard-go/tree/main.go
func (w *Wireguard) Run(ctx context.Context, waitError chan<- error, ready chan<- struct{}) {
kernelSupported, err := w.netlink.IsWireguardSupported()
if err != nil {
waitError <- fmt.Errorf("%w: %s", ErrDetectKernel, err)
return
}
kernelSupported := w.netlink.IsWireguardSupported()
setupFunction := setupUserSpace
userspace := false
switch w.settings.Implementation {
case "auto": //nolint:goconst
if !kernelSupported {
w.logger.Info("Using userspace implementation since Kernel support does not exist")
userspace = true
break
}
w.logger.Info("Using available kernelspace implementation")
setupFunction = setupKernelSpace
case "userspace":
userspace = true
case "kernelspace":
if !kernelSupported {
waitError <- fmt.Errorf("%w", ErrKernelSupport)
@@ -61,6 +60,14 @@ func (w *Wireguard) Run(ctx context.Context, waitError chan<- error, ready chan<
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()
if err != nil {
waitError <- fmt.Errorf("%w: %s", ErrWgctrlOpen, err)