mirror of
https://github.com/qdm12/gluetun.git
synced 2026-07-29 13:56:42 +02:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44143aa3ba | |||
| 56e5cad66b | |||
| c0073c9567 | |||
| cccd7c4c1b | |||
| 4ddf7c9a57 | |||
| a4df514e4b | |||
| 35847bb3b2 | |||
| d36d0f84d7 | |||
| e68b70ff8b | |||
| 73c3ce23da | |||
| 6857210429 | |||
| 0f7f35d6dc | |||
| ec71f1881b | |||
| 44f2df6f87 | |||
| 650c61e93c | |||
| 2f98539848 | |||
| 5c02e2106c | |||
| e6815054d9 | |||
| 9fe34266f0 | |||
| cdfd644c04 | |||
| 1c722b0f80 | |||
| 2e204e11b6 | |||
| c745f61156 | |||
| 4301196c21 | |||
| ef48826e48 | |||
| 3fca8b3dfe | |||
| 8ab97bb4b4 | |||
| 8269fad3b2 | |||
| 16870d6385 | |||
| d41081f4b3 | |||
| 5fbe277f9c | |||
| c5b20a3b06 | |||
| 1b871496ea | |||
| 05795ad2d1 | |||
| f9a86f48aa | |||
| 3a39236fa8 | |||
| 998ae46e97 | |||
| c4e65f8cc7 | |||
| a20795df9e | |||
| 78929cd6e1 | |||
| 75c2404179 | |||
| bcbc8c41f2 | |||
| 3da0c21c82 | |||
| 009b8e45b7 | |||
| 05dc21ed61 | |||
| 270dce9acb | |||
| 23cf3231f0 | |||
| 7f22fb3276 | |||
| 6909a0c123 | |||
| 3e1f48932a | |||
| 50744852c5 | |||
| 09e52bc685 | |||
| 857fe425ec |
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -29,6 +29,7 @@ type Logger interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
Errorf(format string, args ...any)
|
||||
}
|
||||
|
||||
type Cmder interface {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,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,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,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,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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package tun
|
||||
|
||||
func (t *Tun) Check(path string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Dialer interface {
|
||||
Dial(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,7 +9,7 @@ type NetLinker interface {
|
||||
Router
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
IsWireguardSupported() bool
|
||||
}
|
||||
|
||||
type Router interface {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user