mirror of
https://github.com/qdm12/gluetun.git
synced 2026-07-28 21:36:28 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f21596cf4 |
@@ -1,3 +1,2 @@
|
||||
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
||||
RUN apk add wireguard-tools htop openssl tcpdump iptables
|
||||
RUN apk add nodejs npm && npm install markdownlint-cli2 --global && apk del npm
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
# AGENTS
|
||||
|
||||
Guidance for coding agents working in this repository.
|
||||
|
||||
## Scope and priorities
|
||||
|
||||
- Keep changes minimal and targeted. Feel free to do light refactors that are relevant to the modifications.
|
||||
- Breaking changes:
|
||||
- Do not introduce breaking usage behavior (cli flags, environment variables, etc.) unless explicitly agreed.
|
||||
- Do not introduce breaking changes for the Go API in the `pkg/` directory.
|
||||
- If a compatibility break seems beneficial, stop and ask for confirmation before implementing it.
|
||||
- Update or add tests when behavior changes.
|
||||
|
||||
## Go coding conventions
|
||||
|
||||
### General guidelines
|
||||
|
||||
- Use explicit, descriptive variable names by default.
|
||||
- Notable bad examples: `req`, `resp`, `cfg`, `v`
|
||||
- Allowed short-name exceptions:
|
||||
- indexes such as `i`, `j`
|
||||
- `ctx` for `context.Context`
|
||||
- `t` for `*testing.T` and `b` for `*testing.B`
|
||||
- `ctrl` for `*gomock.Controller`
|
||||
- `err` for `error`, `errs` for `[]error`
|
||||
- `wg` for `*sync.WaitGroup`
|
||||
- Avoid using global variables except for:
|
||||
- exported sentinel errors that are used outside the package boundaries
|
||||
- regular expressions defined with `regexp.MustCompile`
|
||||
- variables set by the build pipeline, such as `Version` and `BuildDate`
|
||||
- Constants
|
||||
- Prefer defining them inline in a function if it's only used in that function, rather than at the package level.
|
||||
- Each one should be defined right above where it's used, instead of having multiple defined at the same place in a `const ()` block
|
||||
- If one is only used in a single production code function, define it right above it so it's more local for readability.
|
||||
- Do not define constants when constants exist in other packages, for example `http.StatusBadRequest` or `log.LevelDebug`.
|
||||
- Structs
|
||||
- Prefer defining them inline in a function if it's only used in that function, rather than at the package level.
|
||||
- Do not use the short if form, prefer the longer one
|
||||
- Follow modern Go, according to the Go version defined in go.mod. Prefer modern constructs when equivalent:
|
||||
- Example: use `for i := range 5` rather than `for i := 0; i < 5; i++`.
|
||||
- Example: use `new("string")` rather than helper wrappers such as `stringPtr("string")`.
|
||||
- Example: no need to pin variables in for loops when using them in goroutines or subtests.
|
||||
- Use `New(...) *Item` constructor per package. Each package should ideally only have one constructor, although this is not a strict rule. The constructor should return a pointer to the struct, and not an interface.
|
||||
- Always prefer using context-aware functions, for example:
|
||||
- `exec.CommandContext` rather than `exec.Command`
|
||||
- `http.NewRequestWithContext` rather than `http.NewRequest`
|
||||
- Never export a symbol unless absolutely necessary.
|
||||
- Always use the most restrictive builtin types. For example prefer `uint` over `int` if it's only zero or positive. Prefer `uint16` is the max value is 65535.
|
||||
- Prefer using builtin types whenever possible AND do not define single field structs unless necessary
|
||||
- Prefer splitting a code line only when it triggers the `lll` linter, do not split a command or arguments list for each element
|
||||
- Use `netip` types instead of `net` types whenever possible
|
||||
- Use constants instead of variables whenever possible, especially function-local inline constants.
|
||||
- Do not use `time.Sleep`, prefer using a `time.Timer` with a `select` statement also listening on a context cancelation
|
||||
- `panic`:
|
||||
- should only be used when a programming error is encountered and you should NOT return errors for programming errors (such as passing nil objects)
|
||||
- Its counterpart `recover` should not really be used, except for testing a panic in test code (or use `assert.PanicsWithValue`).
|
||||
|
||||
### Directory structure and file naming
|
||||
|
||||
- Executable main packages with a single `main()` function must be in the `cmd` directory.
|
||||
Prefer having top level logic and have a longer `main()` function rather than having an `internal/app` package.
|
||||
- Code lives by default in subpackages within the `internal` directory
|
||||
- Code needing to be imported by external Go modules must be in subpackages within the `pkg` directory
|
||||
- Example code especially using the `pkg` directory must be in `main` packages within the `examples` directory, each with a single `main.go` function.
|
||||
- If AND only if the repository is a Go library and not a Go application, you may have Go files at the root of the project to simplify import paths. Most of the code should still be in subpackages in the `internal` directory.
|
||||
- Interfaces should be defined in `interfaces.go` files for each package. If there are unexported interfaces which need to be mocked, which is rare, they should be defined in `interfaces_local.go` files.
|
||||
- Mock files are
|
||||
- `mocks_generate_test.go` which only contains `//go:generate` directives for generating mocks, and no actual code
|
||||
- `mocks_test.go` which contains the generated mocks from exported interfaces and no other code, and is ignored in coverage reports
|
||||
- `mocks_local_test.go` (rare) which contains the generated mocks from unexported interfaces and no other code, and is ignored in coverage reports
|
||||
- NEVER generate an exported mock in a non test file, prefer re-generating files across packages.
|
||||
- Package naming
|
||||
- Your package name should be the same as the directory containing it, **except for the `main` package**
|
||||
- Use single words for package names
|
||||
- Do not use generic names for package names such as `utils` or `helpers`
|
||||
- Package nesting
|
||||
- Try to avoid nesting packages by default
|
||||
- You can nest packages if you have different implementations for the same interface (e.g. a store interface)
|
||||
- You can nest packages if you start having a lot of Go files (more than 10) and it really does make sense to make subpackages
|
||||
|
||||
### Linting
|
||||
|
||||
The linter is `golangci-lint` with the configuration defined in `.golangci.yml`.
|
||||
|
||||
To exclude code from linting, prefer using, when absolutely necessary, command comments `//nolint:<linter>`.
|
||||
This allows the `nolintlint` linter to detect and report unnecessary `//nolint` comments later.
|
||||
You can notably use `//nolint:lll` and, for good valid reasons, `//nolint:gosec`. Sometimes `//nolint:mnd` when it just doesn't make sense to extract a constant such as `n = n << 4`
|
||||
Always prefer placing `//nolint` comments on the same code line where the error comes from, and not above a code block.
|
||||
|
||||
### Mocking
|
||||
|
||||
Mocking works with the `go.uber.org/mock` library, and the `mockgen` tool.
|
||||
|
||||
- Mocks from exported interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_test.go` files, using:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . InterfaceA,InterfaceB
|
||||
```
|
||||
|
||||
- Mocks from unexported interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_local_test.go` files. The source file for unexported interfaces is `interfaces_local.go`. The go generate command is similar to:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_local_test.go -package $GOPACKAGE -source interfaces_local.go
|
||||
```
|
||||
|
||||
- Mocks from external interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_<package-name>_test.go` files, using:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_<package-name>_test.go -package $GOPACKAGE module-name InterfaceA,InterfaceB
|
||||
```
|
||||
|
||||
- Generated mocks usage in tests:
|
||||
- Define mocks in the subtest, not in the parent test. You can also have a function returning the mocks as a field of the test case struct, which takes in the subtest `*testing.T` as argument, and call it in the subtest to get the mocks.
|
||||
- **Never** use `gomock.Any()` as argument. Always use concrete, precise arguments. You might need to define a custom GoMock matcher for your argument in some very niche and corner cases.
|
||||
- **Never** use `.AnyTimes()` on mocks. Always define the number of times a certain mock call should be called, with `.Times(3)` for example.
|
||||
- **Always** set the `.Return(...)` on the mock if the function returns something.
|
||||
- Avoid using **mock helpers** functions, prefer a bit of repetition than tight coupling and dependency
|
||||
|
||||
### main.go
|
||||
|
||||
- Make the program OS signal aware, so it attempts a graceful shutdown when interruped. Force quit the program on a second interrupt signal.
|
||||
|
||||
### Formatting
|
||||
|
||||
The Go formatter used is gofumpt.
|
||||
|
||||
### Errors
|
||||
|
||||
- Always prefer wrapping errors with some context with `fmt.Errorf("doing this: %w", err)`
|
||||
- In rare cases, you can just use `return err` notably:
|
||||
- If the function is called **recursively**, since we don't wrap the wrapping multiple times for each recursion
|
||||
- If the current function only statement is the call to another function, for example:
|
||||
|
||||
```go
|
||||
func (s *Struct) Fetch() error {
|
||||
return fetch() // do not wrap the error
|
||||
}
|
||||
```
|
||||
|
||||
- When wrapping errors, use verbs ending in "ing" and no "failed to" or "cannot" to avoid redundancy. For example, use `fmt.Errorf("resolving host: %w", err)` rather than `fmt.Errorf("failed to resolve host: %w", err)`.
|
||||
- When wrapping an error, the context should NEVER contain variables injected as arguments in the function returning an error, to avoid repeating the same variable in multiple error messages.
|
||||
- Testing errors:
|
||||
- If the error does not wrap a sentinel error, use `assert.ErrorContains` to check for error messages, rather than `assert.EqualError`, to avoid having to update tests for minor changes in error messages. And use `assert.NoError` to check for no error.
|
||||
- If the error wraps a sentinel error, use `assert.ErrorIs` to check both for the sentinel error or an expected nil error. You can also check the error message with `assert.ErrorContains`
|
||||
|
||||
### User program settings
|
||||
|
||||
- For configuration structs, each field Go zero value (i.e. `0` for `int`, `nil` for `*string`) should be an INVALID value in the user sense. This is used to detect when a field is not set, in order to default it, merge it or override it. For example if `""` is not a valid value, the field should be of type `string`. Conversely, if `""` is a valid value, the field should be of type `*string` to distinguish between "not set" and "set to empty string". Notably, boolean fields are ALWAYS of type `*bool` for this reason, since both `true` and `false` are valid values.
|
||||
- Configuration reading and handling relies on the Go library github.com/qdm12/gosettings please use it whenever appropriate.
|
||||
- Do not wrap errors coming from `reader.Reader` methods, since they already contain the necessary context.
|
||||
- All keys passed to `reader.Reader` methods must be in environment variable format, i.e. uppercase with underscores. These get converted to lowercase and dashes for flags notably.
|
||||
- For each settings structs, define the following methods, which are usually unexported, but can be exported especially for the top level Settings struct, in this order:
|
||||
- `func (s *Settings) setDefaults()` whichs sets defaults (using `gosettings.Default*` functions) on unset fields
|
||||
- If the settings need to be patched at runtime, which is rarely the case, define `func (s *Settings) overrideWith(other Settings)` which overrides the settings with another settings struct, only for fields that are set in the other struct (using `gosettings.OverrideWith` functions).
|
||||
- `func (s Settings) validate() error` which validates the settings, and returns an error if anything is invalid
|
||||
- `func (s *Settings) read(r *reader.Reader) error` which reads the settings from a gosettings/reader.Reader (which can be from multiple sources, such as environment variables, cli flags, config files etc.)
|
||||
- `func (s Settings) String() string` which uses `toLinesNode().String()` to return a string representation of the settings
|
||||
- `func (s Settings) toLinesNode() *gotree.Node` which a github.com/qdm12/gotree `*Node` representing the settings
|
||||
|
||||
### Testing
|
||||
|
||||
- Use the github.com/stretchr/testify library for assertions
|
||||
- Most tests should be table tests with parallel subtests
|
||||
- Prefer map-based table tests of the form `map[string]struct{ ... }`, with the key as the test name.
|
||||
Use underscores in test names, not spaces, to keep `go test` output searchable.
|
||||
- Use `testCases` for the table variable name, and `testCase` for each iterated case value.
|
||||
- Run all tests in parallel:
|
||||
- call `t.Parallel()` in the top-level test
|
||||
- call `t.Parallel()` in each subtest
|
||||
|
||||
### Libraries to use
|
||||
|
||||
- Logging: `github.com/qdm12/log`
|
||||
- Splash information at program start: `github.com/qdm12/gosplash`
|
||||
- Long running services (i.e. health server, http prod server, backup loop etc.): `github.com/qdm12/goservices`
|
||||
- String tree structures: `github.com/qdm12/gotree`
|
||||
|
||||
### Extra rules
|
||||
|
||||
- Do not use `http.DefaultClient`, use a custom `*http.Client` with a fixed timeout and share with dependency injections.
|
||||
- Do not check for injected dependencies being `nil`, prefer to just panic on a nil pointer. By default it's fine to panic if a developer injects a dependency `nil`. `nil` does not mean use a default.
|
||||
|
||||
## Validation checklist
|
||||
|
||||
Run the following before finishing changes:
|
||||
|
||||
1. Go building `go build ./...`
|
||||
1. Go linting `golangci-lint run`
|
||||
1. Go unit tests `go test ./...`
|
||||
1. If a module is added or modified, run `go mod tidy`
|
||||
1. If an interface or mock command is modified, run `go generate -run mockgen ./...`
|
||||
|
||||
If a Markdown file is modified and `markdownlint-cli2` is available, run `markdownlint-cli2 "**/*.md"`
|
||||
|
||||
If a command is unavailable in the current environment, report it clearly and provide the exact command needed once available.
|
||||
+1
-2
@@ -164,8 +164,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
VPN_PORT_FORWARDING_PROVIDER= \
|
||||
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||
VPN_PORT_FORWARDING_LISTENING_PORTS=0 \
|
||||
VPN_PORT_FORWARDING_PORTS_COUNT=1 \
|
||||
VPN_PORT_FORWARDING_LISTENING_PORT=0 \
|
||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||
# PMTUD
|
||||
PMTUD_ICMP_ADDRESSES=1.1.1.1,8.8.8.8 \
|
||||
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/mdlayher/genetlink v1.3.2
|
||||
github.com/mdlayher/netlink v1.9.0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260310123525-76fabc2b3294
|
||||
github.com/qdm12/gosettings v0.4.4
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c
|
||||
@@ -26,6 +26,7 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/mod v0.33.0
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/text v0.35.0
|
||||
@@ -57,7 +58,6 @@ require (
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
|
||||
@@ -74,8 +74,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-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/dns/v2 v2.0.0-rc9.0.20260310123525-76fabc2b3294 h1:adkCP7N9mEHpsKSR/5LToF27qJo0yOufhT5zBdKpyrE=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260310123525-76fabc2b3294/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=
|
||||
|
||||
@@ -22,7 +22,7 @@ type BoringPoll struct {
|
||||
|
||||
// Internal signals and channels
|
||||
cancel context.CancelFunc
|
||||
done *sync.WaitGroup
|
||||
done <-chan struct{}
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ func (b *BoringPoll) Start() (runError <-chan error, err error) {
|
||||
const logEveryBytes = 100 * 1000 * 1000 // 100 IEC MB
|
||||
|
||||
var ready, done sync.WaitGroup
|
||||
b.done = &done
|
||||
ready.Add(len(b.urlToData))
|
||||
done.Add(len(b.urlToData))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -167,7 +166,7 @@ func (b *BoringPoll) Stop() error {
|
||||
return nil
|
||||
}
|
||||
b.cancel()
|
||||
b.done.Wait()
|
||||
<-b.done
|
||||
b.cancel = nil
|
||||
b.done = nil
|
||||
return nil
|
||||
|
||||
@@ -138,6 +138,46 @@ func defaultDNSProviders() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
for _, addrPort := range d.UpstreamPlainAddresses {
|
||||
if addrPort.Addr().Is4() {
|
||||
return addrPort.Addr()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ipv4 = findPlainIPv4InProviders(d.Providers)
|
||||
if ipv4.IsValid() {
|
||||
return ipv4
|
||||
}
|
||||
|
||||
// Either:
|
||||
// - all upstream plain addresses are IPv6 and no provider is set
|
||||
// - all providers set do not have a plaintext IPv4 address
|
||||
ipv4 = findPlainIPv4InProviders(defaultDNSProviders())
|
||||
if !ipv4.IsValid() {
|
||||
panic("no plaintext IPv4 address found in default DNS providers")
|
||||
}
|
||||
return ipv4
|
||||
}
|
||||
|
||||
func findPlainIPv4InProviders(providerNames []string) netip.Addr {
|
||||
providers := provider.NewProviders()
|
||||
for _, name := range providerNames {
|
||||
provider, err := providers.Get(name)
|
||||
if err != nil {
|
||||
// Settings should be validated before calling this function,
|
||||
// so an error happening here is a programming error.
|
||||
panic(err)
|
||||
}
|
||||
if len(provider.Plain.IPv4) > 0 {
|
||||
return provider.Plain.IPv4[0].Addr()
|
||||
}
|
||||
}
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
func (d DNS) String() string {
|
||||
return d.toLinesNode().String()
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
// Validate EncPreset
|
||||
if vpnProvider == providers.PrivateInternetAccess {
|
||||
validEncryptionPresets := []string{
|
||||
presets.None,
|
||||
presets.Normal,
|
||||
presets.Strong,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gosettings"
|
||||
@@ -39,28 +37,16 @@ type PortForwarding struct {
|
||||
// It can be the empty string to indicate to NOT run a command.
|
||||
// It cannot be nil in the internal state.
|
||||
DownCommand *string `json:"down_command"`
|
||||
// ListeningPorts are the ports traffic would be redirected to from the
|
||||
// forwarded ports. The redirection is disabled if it is the slice [0],
|
||||
// which is its default as well. If set and not [0], its length must match
|
||||
// the PortsCount value, such that each forwarded port is redirected to
|
||||
// the corresponding listening port.
|
||||
ListeningPorts []uint16 `json:"listening_port"`
|
||||
// PortsCount is the number of ports to forward. It is optional for ProtonVPN
|
||||
// and be between 1 and 5. For other providers, it must be set to 1 if port
|
||||
// forwarding is enabled.
|
||||
PortsCount uint16 `json:"ports_count"`
|
||||
// ListeningPort is the port traffic would be redirected to from the
|
||||
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||
// is its default as well.
|
||||
ListeningPort *uint16 `json:"listening_port"`
|
||||
// Username is only used for Private Internet Access port forwarding.
|
||||
Username string `json:"username"`
|
||||
// Password is only used for Private Internet Access port forwarding.
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrPortsCountTooHigh = errors.New("ports count too high")
|
||||
ErrListeningPortsLen = errors.New("listening ports length must be equal to ports count")
|
||||
ErrListeningPortZero = errors.New("listening port cannot be 0")
|
||||
)
|
||||
|
||||
func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
if !*p.Enabled {
|
||||
return nil
|
||||
@@ -89,36 +75,13 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
switch providerSelected {
|
||||
case providers.PrivateInternetAccess:
|
||||
const maxPortsCount = 1
|
||||
if providerSelected == providers.PrivateInternetAccess {
|
||||
switch {
|
||||
case p.PortsCount > maxPortsCount:
|
||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
|
||||
case p.Username == "":
|
||||
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
|
||||
case p.Password == "":
|
||||
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
|
||||
}
|
||||
case providers.Protonvpn:
|
||||
const maxPortsCount = 4
|
||||
if p.PortsCount > maxPortsCount {
|
||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
|
||||
}
|
||||
default:
|
||||
const maxPortsCount = 1
|
||||
if p.PortsCount > maxPortsCount {
|
||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
|
||||
switch {
|
||||
case len(p.ListeningPorts) != int(p.PortsCount):
|
||||
return fmt.Errorf("%w: %d != %d", ErrListeningPortsLen, len(p.ListeningPorts), p.PortsCount)
|
||||
case slices.Contains(p.ListeningPorts, 0):
|
||||
return fmt.Errorf("%w: in %v", ErrListeningPortZero, p.ListeningPorts)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -126,14 +89,14 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
|
||||
func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||
return PortForwarding{
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Provider: gosettings.CopyPointer(p.Provider),
|
||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||
ListeningPorts: gosettings.CopySlice(p.ListeningPorts),
|
||||
Username: p.Username,
|
||||
Password: p.Password,
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Provider: gosettings.CopyPointer(p.Provider),
|
||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||
Username: p.Username,
|
||||
Password: p.Password,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +106,7 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
||||
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
||||
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
||||
p.ListeningPorts = gosettings.OverrideWithSlice(p.ListeningPorts, other.ListeningPorts)
|
||||
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
||||
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||
}
|
||||
@@ -154,8 +117,7 @@ func (p *PortForwarding) setDefaults() {
|
||||
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
||||
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
||||
p.ListeningPorts = gosettings.DefaultSlice(p.ListeningPorts, []uint16{0}) // disabled
|
||||
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, 1)
|
||||
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
||||
}
|
||||
|
||||
func (p PortForwarding) String() string {
|
||||
@@ -169,14 +131,11 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
||||
|
||||
node = gotree.New("Automatic port forwarding settings:")
|
||||
|
||||
node.Appendf("Number of ports to be forwarded: %d", p.PortsCount)
|
||||
|
||||
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
|
||||
redirNode := node.Appendf("Redirection for listening ports:")
|
||||
for i, port := range p.ListeningPorts {
|
||||
redirNode.Appendf("Port #%d -> %d", i+1, port)
|
||||
}
|
||||
listeningPort := "disabled"
|
||||
if *p.ListeningPort != 0 {
|
||||
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
|
||||
}
|
||||
node.Appendf("Redirection listening port: %s", listeningPort)
|
||||
|
||||
if *p.Provider == "" {
|
||||
node.Appendf("Use port forwarding code for current provider")
|
||||
@@ -231,13 +190,7 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
|
||||
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
|
||||
reader.ForceLowercase(false))
|
||||
|
||||
p.ListeningPorts, err = r.CSVUint16("VPN_PORT_FORWARDING_LISTENING_PORTS",
|
||||
reader.RetroKeys("VPN_PORT_FORWARDING_LISTENING_PORT"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.PortsCount, err = r.Uint16("VPN_PORT_FORWARDING_PORTS_COUNT")
|
||||
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ type Logger interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
Errorf(format string, args ...any)
|
||||
}
|
||||
|
||||
type Cmder interface {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||
@@ -43,12 +42,11 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
|
||||
settings: Settings{
|
||||
VPNIsUp: ptrTo(false),
|
||||
Service: service.Settings{
|
||||
Enabled: settings.Enabled,
|
||||
Filepath: *settings.Filepath,
|
||||
UpCommand: *settings.UpCommand,
|
||||
DownCommand: *settings.DownCommand,
|
||||
ListeningPorts: settings.ListeningPorts,
|
||||
PortsCount: settings.PortsCount,
|
||||
Enabled: settings.Enabled,
|
||||
Filepath: *settings.Filepath,
|
||||
UpCommand: *settings.UpCommand,
|
||||
DownCommand: *settings.DownCommand,
|
||||
ListeningPort: *settings.ListeningPort,
|
||||
},
|
||||
},
|
||||
routing: routing,
|
||||
@@ -88,8 +86,6 @@ 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 {
|
||||
@@ -108,12 +104,10 @@ 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
|
||||
}
|
||||
|
||||
if l.service != nil {
|
||||
firstRun := serviceRunError == nil
|
||||
if !firstRun {
|
||||
err := l.service.Stop()
|
||||
if err != nil {
|
||||
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
||||
@@ -137,10 +131,6 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type Logger interface {
|
||||
type PortForwarder interface {
|
||||
Name() string
|
||||
PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||
internalToExternalPorts map[uint16]uint16, err error)
|
||||
ports []uint16, err error)
|
||||
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
||||
}
|
||||
|
||||
|
||||
@@ -69,11 +69,7 @@ func (s *Service) SetPortsForwarded(ctx context.Context, ports []uint16) (err er
|
||||
return fmt.Errorf("cleaning up: %w", err)
|
||||
}
|
||||
|
||||
internalToExternalPorts := make(map[uint16]uint16, len(ports))
|
||||
for _, port := range ports {
|
||||
internalToExternalPorts[port] = port
|
||||
}
|
||||
err = s.onNewPorts(ctx, internalToExternalPorts)
|
||||
err = s.onNewPorts(ctx, ports)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handling new ports: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gosettings"
|
||||
@@ -18,8 +17,7 @@ type Settings struct {
|
||||
Interface string // needed for PIA, PrivateVPN and ProtonVPN, tun0 for example
|
||||
ServerName string // needed for PIA
|
||||
CanPortForward bool // needed for PIA
|
||||
ListeningPorts []uint16
|
||||
PortsCount uint16
|
||||
ListeningPort uint16
|
||||
Username string // needed for PIA
|
||||
Password string // needed for PIA
|
||||
}
|
||||
@@ -33,8 +31,7 @@ func (s Settings) Copy() (copied Settings) {
|
||||
copied.Interface = s.Interface
|
||||
copied.ServerName = s.ServerName
|
||||
copied.CanPortForward = s.CanPortForward
|
||||
copied.ListeningPorts = gosettings.CopySlice(s.ListeningPorts)
|
||||
copied.PortsCount = s.PortsCount
|
||||
copied.ListeningPort = s.ListeningPort
|
||||
copied.Username = s.Username
|
||||
copied.Password = s.Password
|
||||
return copied
|
||||
@@ -49,8 +46,7 @@ func (s *Settings) OverrideWith(update Settings) {
|
||||
s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface)
|
||||
s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName)
|
||||
s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward)
|
||||
s.ListeningPorts = gosettings.OverrideWithSlice(s.ListeningPorts, update.ListeningPorts)
|
||||
s.PortsCount = gosettings.OverrideWithComparable(s.PortsCount, update.PortsCount)
|
||||
s.ListeningPort = gosettings.OverrideWithComparable(s.ListeningPort, update.ListeningPort)
|
||||
s.Username = gosettings.OverrideWithComparable(s.Username, update.Username)
|
||||
s.Password = gosettings.OverrideWithComparable(s.Password, update.Password)
|
||||
}
|
||||
@@ -62,10 +58,6 @@ var (
|
||||
ErrPasswordNotSet = errors.New("password not set")
|
||||
ErrFilepathNotSet = errors.New("file path not set")
|
||||
ErrInterfaceNotSet = errors.New("interface not set")
|
||||
ErrPortsCountZero = errors.New("ports count cannot be zero")
|
||||
ErrPortsCountTooHigh = errors.New("ports count too high")
|
||||
ErrListeningPortsLen = errors.New("listening ports length must be equal to ports count")
|
||||
ErrListeningPortZero = errors.New("listening port cannot be 0")
|
||||
)
|
||||
|
||||
func (s *Settings) Validate(forStartup bool) (err error) {
|
||||
@@ -86,12 +78,7 @@ func (s *Settings) Validate(forStartup bool) (err error) {
|
||||
return fmt.Errorf("%w", ErrPortForwarderNotSet)
|
||||
case s.Interface == "":
|
||||
return fmt.Errorf("%w", ErrInterfaceNotSet)
|
||||
case s.PortsCount == 0:
|
||||
return fmt.Errorf("%w", ErrPortsCountZero)
|
||||
}
|
||||
|
||||
switch s.PortForwarder.Name() {
|
||||
case providers.PrivateInternetAccess:
|
||||
case s.PortForwarder.Name() == providers.PrivateInternetAccess:
|
||||
switch {
|
||||
case s.ServerName == "":
|
||||
return fmt.Errorf("%w", ErrServerNameNotSet)
|
||||
@@ -100,26 +87,6 @@ func (s *Settings) Validate(forStartup bool) (err error) {
|
||||
case s.Password == "":
|
||||
return fmt.Errorf("%w", ErrPasswordNotSet)
|
||||
}
|
||||
case providers.Protonvpn:
|
||||
const maxPortsCount = 4
|
||||
if s.PortsCount > maxPortsCount {
|
||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, s.PortsCount, maxPortsCount)
|
||||
}
|
||||
default:
|
||||
const maxPortsCount = 1
|
||||
if s.PortsCount > maxPortsCount {
|
||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, s.PortsCount, maxPortsCount)
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Equal(s.ListeningPorts, []uint16{0}) {
|
||||
switch {
|
||||
case len(s.ListeningPorts) != int(s.PortsCount):
|
||||
return fmt.Errorf("%w: %d != %d", ErrListeningPortsLen, len(s.ListeningPorts), s.PortsCount)
|
||||
case slices.Contains(s.ListeningPorts, 0):
|
||||
return fmt.Errorf("%w: in %v", ErrListeningPortZero, s.ListeningPorts)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
@@ -43,9 +42,8 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
|
||||
CanPortForward: s.settings.CanPortForward,
|
||||
Username: s.settings.Username,
|
||||
Password: s.settings.Password,
|
||||
PortsCount: s.settings.PortsCount,
|
||||
}
|
||||
internalToExternalPorts, err := s.settings.PortForwarder.PortForward(ctx, obj)
|
||||
ports, err := s.settings.PortForwarder.PortForward(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("port forwarding for the first time: %w", err)
|
||||
}
|
||||
@@ -53,7 +51,7 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
|
||||
s.portMutex.Lock()
|
||||
defer s.portMutex.Unlock()
|
||||
|
||||
err = s.onNewPorts(ctx, internalToExternalPorts)
|
||||
err = s.onNewPorts(ctx, ports)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,60 +86,36 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
|
||||
return runErrorCh, nil
|
||||
}
|
||||
|
||||
func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[uint16]uint16) (err error) {
|
||||
autoRedirectionNeeded := false
|
||||
externalToInternalPorts := make(map[uint16]uint16, len(internalToExternalPorts))
|
||||
for internal, external := range internalToExternalPorts {
|
||||
externalToInternalPorts[external] = internal
|
||||
if internal != external {
|
||||
autoRedirectionNeeded = true
|
||||
}
|
||||
}
|
||||
func (s *Service) onNewPorts(ctx context.Context, ports []uint16) (err error) {
|
||||
slices.Sort(ports)
|
||||
|
||||
externalPorts := slices.Collect(maps.Keys(externalToInternalPorts))
|
||||
slices.Sort(externalPorts)
|
||||
s.logger.Info(portsToString(ports))
|
||||
|
||||
s.logger.Info(portsToString(externalPorts))
|
||||
|
||||
userRedirectionEnabled := !slices.Equal(s.settings.ListeningPorts, []uint16{0})
|
||||
for i, port := range externalPorts {
|
||||
internalPort := externalToInternalPorts[port]
|
||||
err = s.portAllower.SetAllowedPort(ctx, internalPort, s.settings.Interface)
|
||||
for _, port := range ports {
|
||||
err = s.portAllower.SetAllowedPort(ctx, port, s.settings.Interface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("allowing port in firewall: %w", err)
|
||||
}
|
||||
|
||||
var sourcePort, destinationPort uint16
|
||||
switch {
|
||||
case userRedirectionEnabled: // precedence over auto redirection
|
||||
sourcePort = externalToInternalPorts[port]
|
||||
destinationPort = s.settings.ListeningPorts[i]
|
||||
case autoRedirectionNeeded:
|
||||
sourcePort = externalToInternalPorts[port]
|
||||
destinationPort = port
|
||||
default:
|
||||
// No redirection needed, source and destination ports are the same.
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, sourcePort, destinationPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting port %d to %d in firewall: %w",
|
||||
sourcePort, destinationPort, err)
|
||||
if s.settings.ListeningPort != 0 {
|
||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, port, s.settings.ListeningPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting port in firewall: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = s.writePortForwardedFile(externalPorts)
|
||||
err = s.writePortForwardedFile(ports)
|
||||
if err != nil {
|
||||
_ = s.cleanup()
|
||||
return fmt.Errorf("writing port file: %w", err)
|
||||
}
|
||||
|
||||
s.ports = make([]uint16, len(internalToExternalPorts))
|
||||
copy(s.ports, externalPorts)
|
||||
s.ports = make([]uint16, len(ports))
|
||||
copy(s.ports, ports)
|
||||
|
||||
if s.settings.UpCommand != "" {
|
||||
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, externalPorts, s.settings.Interface)
|
||||
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports, s.settings.Interface)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("running up command: %w", err)
|
||||
s.logger.Error(err.Error())
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -39,14 +38,13 @@ func (s *Service) cleanup() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
redirectionWasEnabled := !slices.Equal(s.settings.ListeningPorts, []uint16{0})
|
||||
for _, port := range s.ports {
|
||||
err = s.portAllower.RemoveAllowedPort(context.Background(), port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("blocking previous port in firewall: %w", err)
|
||||
}
|
||||
|
||||
if redirectionWasEnabled {
|
||||
if s.settings.ListeningPort != 0 {
|
||||
ctx := context.Background()
|
||||
const listeningPort = 0 // 0 to clear the redirection
|
||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, port, listeningPort)
|
||||
|
||||
@@ -76,7 +76,6 @@ func modifyConfig(lines []string, connection models.Connection,
|
||||
modified = append(modified, "pull-filter ignore \"auth-token\"") // prevent auth failed loop
|
||||
modified = append(modified, "auth-retry nointeract")
|
||||
modified = append(modified, "suppress-timestamps")
|
||||
modified = append(modified, "handshake-window 10") // default is 60 seconds which is too long
|
||||
if *settings.User != "" {
|
||||
modified = append(modified, "auth-user-pass "+openvpn.AuthConf)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ func Test_modifyConfig(t *testing.T) {
|
||||
"pull-filter ignore \"auth-token\"",
|
||||
"auth-retry nointeract",
|
||||
"suppress-timestamps",
|
||||
"handshake-window 10",
|
||||
"auth-user-pass /etc/openvpn/auth.conf",
|
||||
"verb 0",
|
||||
"data-ciphers-fallback cipher",
|
||||
|
||||
@@ -10,17 +10,12 @@ import (
|
||||
// PortForward calculates and returns the VPN server side ports forwarded.
|
||||
func (p *Provider) PortForward(_ context.Context,
|
||||
objects utils.PortForwardObjects,
|
||||
) (internalToExternalPorts map[uint16]uint16, err error) {
|
||||
) (ports []uint16, err error) {
|
||||
if !objects.InternalIP.IsValid() {
|
||||
panic("internal ip is not set")
|
||||
}
|
||||
|
||||
ports := internalIPToPorts(objects.InternalIP)
|
||||
internalToExternalPorts = make(map[uint16]uint16, len(ports))
|
||||
for _, port := range ports {
|
||||
internalToExternalPorts[port] = port
|
||||
}
|
||||
return internalToExternalPorts, nil
|
||||
return internalIPToPorts(objects.InternalIP), nil
|
||||
}
|
||||
|
||||
func (p *Provider) KeepPortForward(ctx context.Context,
|
||||
|
||||
@@ -13,7 +13,7 @@ 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.Normal:
|
||||
case presets.None, presets.Normal:
|
||||
defaults.OpenVPNTCPPort = 502
|
||||
defaults.OpenVPNUDPPort = 1198
|
||||
case presets.Strong:
|
||||
|
||||
@@ -24,10 +24,16 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
)
|
||||
switch *settings.PIAEncPreset {
|
||||
case presets.Normal:
|
||||
providerSettings.Ciphers = []string{openvpn.AES128gcm}
|
||||
providerSettings.Ciphers = []string{openvpn.AES128cbc}
|
||||
providerSettings.Auth = openvpn.SHA1
|
||||
providerSettings.CAs = []string{caNormalPreset}
|
||||
case presets.None:
|
||||
providerSettings.Ciphers = []string{"none"}
|
||||
providerSettings.Auth = "none"
|
||||
providerSettings.CAs = []string{caNormalPreset}
|
||||
default: // strong
|
||||
providerSettings.Ciphers = []string{openvpn.AES256gcm}
|
||||
providerSettings.Ciphers = []string{openvpn.AES256cbc}
|
||||
providerSettings.Auth = openvpn.SHA256
|
||||
providerSettings.CAs = []string{"MIIHqzCCBZOgAwIBAgIJAJ0u+vODZJntMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzQwMzNaFw0zNDA0MTIxNzQwMzNaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVkhjumaqBbL8aSgj6xbX1QPTfTd1qHsAZd2B97m8Vw31c/2yQgZNf5qZY0+jOIHULNDe4R9TIvyBEbvnAg/OkPw8n/+ScgYOeH876VUXzjLDBnDb8DLr/+w9oVsuDeFJ9KV2UFM1OYX0SnkHnrYAN2QLF98ESK4NCSU01h5zkcgmQ+qKSfA9Ny0/UpsKPBFqsQ25NvjDWFhCpeqCHKUJ4Be27CDbSl7lAkBuHMPHJs8f8xPgAbHRXZOxVCpayZ2SNDfCwsnGWpWFoMGvdMbygngCn6jA/W1VSFOlRlfLuuGe7QFfDwA0jaLCxuWt/BgZylp7tAzYKR8lnWmtUCPm4+BtjyVDYtDCiGBD9Z4P13RFWvJHw5aapx/5W/CuvVyI7pKwvc2IT+KPxCUhH1XI8ca5RN3C9NoPJJf6qpg4g0rJH3aaWkoMRrYvQ+5PXXYUzjtRHImghRGd/ydERYoAZXuGSbPkm9Y/p2X8unLcW+F0xpJD98+ZI+tzSsI99Zs5wijSUGYr9/j18KHFTMQ8n+1jauc5bCCegN27dPeKXNSZ5riXFL2XX6BkY68y58UaNzmeGMiUL9BOV1iV+PMb7B7PYs7oFLjAhh0EdyvfHkrh/ZV9BEhtFa7yXp8XR0J6vz1YV9R6DYJmLjOEbhU8N0gc3tZm4Qz39lIIG6w3FDAgMBAAGjggFUMIIBUDAdBgNVHQ4EFgQUrsRtyWJftjpdRM0+925Y6Cl08SUwggEfBgNVHSMEggEWMIIBEoAUrsRtyWJftjpdRM0+925Y6Cl08SWhge6kgeswgegxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRlaW50ZXJuZXRhY2Nlc3MuY29tggkAnS7684Nkme0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOCAgEAJsfhsPk3r8kLXLxY+v+vHzbr4ufNtqnL9/1Uuf8NrsCtpXAoyZ0YqfbkWx3NHTZ7OE9ZRhdMP/RqHQE1p4N4Sa1nZKhTKasV6KhHDqSCt/dvEm89xWm2MVA7nyzQxVlHa9AkcBaemcXEiyT19XdpiXOP4Vhs+J1R5m8zQOxZlV1GtF9vsXmJqWZpOVPmZ8f35BCsYPvv4yMewnrtAC8PFEK/bOPeYcKN50bol22QYaZuLfpkHfNiFTnfMh8sl/ablPyNY7DUNiP5DRcMdIwmfGQxR5WEQoHL3yPJ42LkB5zs6jIm26DGNXfwura/mi105+ENH1CaROtRYwkiHb08U6qLXXJz80mWJkT90nr8Asj35xN2cUppg74nG3YVav/38P48T56hG1NHbYF5uOCske19F6wi9maUoto/3vEr0rnXJUp2KODmKdvBI7co245lHBABWikk8VfejQSlCtDBXn644ZMtAdoxKNfR2WTFVEwJiyd1Fzx0yujuiXDROLhISLQDRjVVAvawrAtLZWYK31bY7KlezPlQnl/D9Asxe85l8jO5+0LdJ6VyOs/Hd4w52alDW/MFySDZSfQHMTIc30hLBJ8OnCEIvluVQQ2UQvoW+no177N9L2Y+M9TcTA62ZyMXShHQGeh20rb4kK8f+iFX8NxtdHVSkxMEFSfDDyQ="} //nolint:lll
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ var ErrServerNameNotFound = errors.New("server name not found in servers")
|
||||
// PortForward obtains a VPN server side port forwarded from PIA.
|
||||
func (p *Provider) PortForward(ctx context.Context,
|
||||
objects utils.PortForwardObjects,
|
||||
) (internalToExternalPorts map[uint16]uint16, err error) {
|
||||
) (ports []uint16, err error) {
|
||||
switch {
|
||||
case objects.ServerName == "":
|
||||
panic("server name cannot be empty")
|
||||
@@ -39,6 +39,7 @@ func (p *Provider) PortForward(ctx context.Context,
|
||||
}
|
||||
|
||||
serverName := objects.ServerName
|
||||
apiIP := buildAPIIPAddress(objects.Gateway)
|
||||
logger := objects.Logger
|
||||
|
||||
if !objects.CanPortForward {
|
||||
@@ -67,14 +68,9 @@ 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, p.apiIP,
|
||||
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, apiIP,
|
||||
p.portForwardPath, objects.Username, objects.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("refreshing port forward data: %w", err)
|
||||
@@ -84,11 +80,11 @@ func (p *Provider) PortForward(ctx context.Context,
|
||||
logger.Info("Port forwarded data expires in " + format.FriendlyDuration(durationToExpiration))
|
||||
|
||||
// First time binding
|
||||
if err := bindPort(ctx, privateIPClient, p.apiIP, data); err != nil {
|
||||
if err := bindPort(ctx, privateIPClient, apiIP, data); err != nil {
|
||||
return nil, fmt.Errorf("binding port: %w", err)
|
||||
}
|
||||
|
||||
return map[uint16]uint16{data.Port: data.Port}, nil
|
||||
return []uint16{data.Port}, nil
|
||||
}
|
||||
|
||||
var ErrPortForwardedExpired = errors.New("port forwarded data expired")
|
||||
@@ -103,6 +99,8 @@ 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)
|
||||
@@ -130,7 +128,7 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
||||
}
|
||||
return ctx.Err()
|
||||
case <-keepAliveTimer.C:
|
||||
err = bindPort(ctx, privateIPClient, p.apiIP, data)
|
||||
err = bindPort(ctx, privateIPClient, apiIP, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("binding port: %w", err)
|
||||
}
|
||||
@@ -142,53 +140,15 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
func buildAPIIPAddress(gateway netip.Addr) (api netip.Addr) {
|
||||
if gateway.Is6() {
|
||||
panic("IPv6 gateway not supported")
|
||||
}
|
||||
|
||||
gatewayBytes := gateway.As4()
|
||||
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...))
|
||||
gatewayBytes[2] = 128
|
||||
gatewayBytes[3] = 1
|
||||
return netip.AddrFrom4(gatewayBytes)
|
||||
}
|
||||
|
||||
func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package presets
|
||||
|
||||
const (
|
||||
None = "none"
|
||||
Normal = "normal"
|
||||
Strong = "strong"
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ package privateinternetaccess
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
@@ -18,7 +17,6 @@ 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/v7"
|
||||
const url = "https://serverlist.piaservers.net/vpninfo/servers/v6"
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -77,9 +77,6 @@ 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) {
|
||||
|
||||
@@ -22,7 +22,7 @@ var ErrPortForwardedNotFound = errors.New("port forwarded not found")
|
||||
// PortForward obtains a VPN server side port forwarded from the PrivateVPN API.
|
||||
// It returns 0 if all ports are to forwarded on a dedicated server IP.
|
||||
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||
internalToExternalPorts map[uint16]uint16, err error,
|
||||
ports []uint16, err error,
|
||||
) {
|
||||
// Define a timeout since the default client has a large timeout and we don't
|
||||
// want to wait too long.
|
||||
@@ -75,8 +75,7 @@ func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObj
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing port: %w", err)
|
||||
}
|
||||
port := uint16(portUint64)
|
||||
return map[uint16]uint16{port: port}, nil
|
||||
return []uint16{uint16(portUint64)}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) KeepPortForward(ctx context.Context,
|
||||
|
||||
@@ -28,10 +28,10 @@ func Test_Provider_PortForward(t *testing.T) {
|
||||
cancel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
ctx context.Context
|
||||
objects utils.PortForwardObjects
|
||||
internalToExternalPorts map[uint16]uint16
|
||||
errMessage string
|
||||
ctx context.Context
|
||||
objects utils.PortForwardObjects
|
||||
ports []uint16
|
||||
errMessage string
|
||||
}{
|
||||
"canceled context": {
|
||||
ctx: canceledCtx,
|
||||
@@ -192,7 +192,7 @@ func Test_Provider_PortForward(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
internalToExternalPorts: map[uint16]uint16{61527: 61527},
|
||||
ports: []uint16{61527},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
@@ -203,7 +203,7 @@ func Test_Provider_PortForward(t *testing.T) {
|
||||
ports, err := provider.PortForward(testCase.ctx,
|
||||
testCase.objects)
|
||||
|
||||
assert.Equal(t, testCase.internalToExternalPorts, ports)
|
||||
assert.Equal(t, testCase.ports, ports)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,18 +13,17 @@ import (
|
||||
|
||||
var ErrServerPortForwardNotSupported = errors.New("server does not support port forwarding")
|
||||
|
||||
const nonSymmetricPortStart uint16 = 56789
|
||||
|
||||
// PortForward obtains a VPN server side port forwarded from ProtonVPN gateway.
|
||||
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||
internalToExternalPorts map[uint16]uint16, err error,
|
||||
ports []uint16, err error,
|
||||
) {
|
||||
if !objects.CanPortForward {
|
||||
return nil, fmt.Errorf("%w", ErrServerPortForwardNotSupported)
|
||||
}
|
||||
|
||||
client := natpmp.New()
|
||||
_, externalIPv4Address, err := client.ExternalAddress(ctx, objects.Gateway)
|
||||
_, externalIPv4Address, err := client.ExternalAddress(ctx,
|
||||
objects.Gateway)
|
||||
if err != nil {
|
||||
switch {
|
||||
case strings.HasSuffix(err.Error(), "connection refused"):
|
||||
@@ -40,37 +38,29 @@ func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObj
|
||||
|
||||
logger := objects.Logger
|
||||
|
||||
logger.Debug("gateway external IPv4 address is " + externalIPv4Address.String())
|
||||
const externalPort = 0
|
||||
logger.Info("gateway external IPv4 address is " + externalIPv4Address.String())
|
||||
const internalPort, externalPort = 0, 1
|
||||
const lifetime = 60 * time.Second
|
||||
|
||||
p.internalToExternalPorts = make(map[uint16]uint16, objects.PortsCount)
|
||||
for i := range objects.PortsCount {
|
||||
internalPort := nonSymmetricPortStart + i
|
||||
protoToInternalPort := map[string]uint16{
|
||||
"udp": 0,
|
||||
"tcp": 0,
|
||||
}
|
||||
protoToExternalPort := maps.Clone(protoToInternalPort)
|
||||
for protocol := range protoToExternalPort {
|
||||
_, assignedInternalPort, assignedExternalPort, assignedLifetime, err := client.AddPortMapping(
|
||||
ctx, objects.Gateway, protocol, internalPort, externalPort, lifetime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("adding %d/%d %s port mapping: %w",
|
||||
i+1, objects.PortsCount, strings.ToUpper(protocol), err)
|
||||
}
|
||||
checkLifetime(logger, strings.ToUpper(protocol), lifetime, assignedLifetime)
|
||||
checkInternalPort(logger, internalPort, assignedInternalPort)
|
||||
protoToInternalPort[protocol] = assignedInternalPort
|
||||
protoToExternalPort[protocol] = assignedExternalPort
|
||||
}
|
||||
|
||||
checkInternalPorts(logger, protoToInternalPort["udp"], protoToInternalPort["tcp"])
|
||||
checkExternalPorts(logger, protoToExternalPort["udp"], protoToExternalPort["tcp"])
|
||||
p.internalToExternalPorts[protoToInternalPort["tcp"]] = protoToExternalPort["tcp"]
|
||||
_, _, assignedUDPExternalPort, assignedLifetime, err := client.AddPortMapping(ctx, objects.Gateway, "udp",
|
||||
internalPort, externalPort, lifetime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("adding UDP port mapping: %w", err)
|
||||
}
|
||||
checkLifetime(logger, "UDP", lifetime, assignedLifetime)
|
||||
|
||||
return maps.Clone(p.internalToExternalPorts), nil
|
||||
_, _, assignedTCPExternalPort, assignedLifetime, err := client.AddPortMapping(ctx, objects.Gateway, "tcp",
|
||||
internalPort, externalPort, lifetime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("adding TCP port mapping: %w", err)
|
||||
}
|
||||
checkLifetime(logger, "TCP", lifetime, assignedLifetime)
|
||||
|
||||
checkExternalPorts(logger, assignedUDPExternalPort, assignedTCPExternalPort)
|
||||
|
||||
p.portForwarded = assignedTCPExternalPort
|
||||
|
||||
return []uint16{assignedTCPExternalPort}, nil
|
||||
}
|
||||
|
||||
func checkLifetime(logger utils.Logger, protocol string,
|
||||
@@ -83,20 +73,6 @@ func checkLifetime(logger utils.Logger, protocol string,
|
||||
}
|
||||
}
|
||||
|
||||
func checkInternalPort(logger utils.Logger, sent, received uint16) {
|
||||
if sent != received {
|
||||
logger.Warn(fmt.Sprintf("internal port assigned %d differs from requested internal port %d",
|
||||
sent, received))
|
||||
}
|
||||
}
|
||||
|
||||
func checkInternalPorts(logger utils.Logger, udpPort, tcpPort uint16) {
|
||||
if udpPort != tcpPort {
|
||||
logger.Warn(fmt.Sprintf("UDP internal port %d differs from TCP internal port %d",
|
||||
udpPort, tcpPort))
|
||||
}
|
||||
}
|
||||
|
||||
func checkExternalPorts(logger utils.Logger, udpPort, tcpPort uint16) {
|
||||
if udpPort != tcpPort {
|
||||
logger.Warn(fmt.Sprintf("UDP external port %d differs from TCP external port %d",
|
||||
@@ -104,10 +80,7 @@ func checkExternalPorts(logger utils.Logger, udpPort, tcpPort uint16) {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInternalPortChanged = errors.New("internal port changed")
|
||||
ErrExternalPortChanged = errors.New("external port changed")
|
||||
)
|
||||
var ErrExternalPortChanged = errors.New("external port changed")
|
||||
|
||||
func (p *Provider) KeepPortForward(ctx context.Context,
|
||||
objects utils.PortForwardObjects,
|
||||
@@ -123,28 +96,32 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
objects.Logger.Debug("refreshing forwarded ports since 45 seconds have elapsed")
|
||||
networkProtocols := [...]string{"udp", "tcp"}
|
||||
objects.Logger.Debug("refreshing port forward since 45 seconds have elapsed")
|
||||
networkProtocols := []string{"udp", "tcp"}
|
||||
const internalPort = 0
|
||||
const lifetime = 60 * time.Second
|
||||
for internalPort, externalPort := range p.internalToExternalPorts {
|
||||
for _, networkProtocol := range networkProtocols {
|
||||
_, assignedInternalPort, assignedExternalPort, assignedLiftetime, err := client.AddPortMapping(
|
||||
ctx, objects.Gateway, networkProtocol, internalPort, externalPort, lifetime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding port mapping: %w", err)
|
||||
}
|
||||
checkLifetime(logger, networkProtocol, lifetime, assignedLiftetime)
|
||||
if externalPort != assignedExternalPort {
|
||||
return fmt.Errorf("%w: %d changed to %d",
|
||||
ErrExternalPortChanged, externalPort, assignedExternalPort)
|
||||
} else if internalPort != assignedInternalPort {
|
||||
return fmt.Errorf("%w: %d (for external port %d) changed to %d",
|
||||
ErrInternalPortChanged, internalPort, externalPort, assignedInternalPort)
|
||||
}
|
||||
|
||||
for _, networkProtocol := range networkProtocols {
|
||||
_, _, assignedExternalPort, assignedLiftetime, err := client.AddPortMapping(ctx, objects.Gateway, networkProtocol,
|
||||
internalPort, p.portForwarded, lifetime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding port mapping: %w", err)
|
||||
}
|
||||
|
||||
if assignedLiftetime != lifetime {
|
||||
logger.Warn(fmt.Sprintf("assigned lifetime %s differs"+
|
||||
" from requested lifetime %s",
|
||||
assignedLiftetime, lifetime))
|
||||
}
|
||||
|
||||
if p.portForwarded != assignedExternalPort {
|
||||
return fmt.Errorf("%w: %d changed to %d",
|
||||
ErrExternalPortChanged, p.portForwarded, assignedExternalPort)
|
||||
}
|
||||
objects.Logger.Debug(fmt.Sprintf("port forwarded %d maintained", externalPort))
|
||||
}
|
||||
|
||||
objects.Logger.Debug(fmt.Sprintf("port forwarded %d maintained", p.portForwarded))
|
||||
|
||||
timer.Reset(refreshTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type Provider struct {
|
||||
storage common.Storage
|
||||
randSource rand.Source
|
||||
common.Fetcher
|
||||
internalToExternalPorts map[uint16]uint16
|
||||
portForwarded uint16
|
||||
}
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
|
||||
@@ -22,11 +22,12 @@ import (
|
||||
// oddities of Proton's authentication flow they want to keep hidden
|
||||
// from the public.
|
||||
type apiClient struct {
|
||||
apiURLBase string
|
||||
httpClient *http.Client
|
||||
appVersion string
|
||||
userAgent string
|
||||
generator *rand.ChaCha8
|
||||
apiURLBase string
|
||||
httpClient *http.Client
|
||||
appVersion string
|
||||
vpnGtkAppVersion string
|
||||
userAgent string
|
||||
generator *rand.ChaCha8
|
||||
}
|
||||
|
||||
// newAPIClient returns an [apiClient] with sane defaults matching Proton's
|
||||
@@ -46,17 +47,22 @@ func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClie
|
||||
}
|
||||
userAgent := userAgents[generator.Uint64()%uint64(len(userAgents))]
|
||||
|
||||
appVersion, err := getMostRecentStableTag(ctx, httpClient)
|
||||
appVersion, err := getMostRecentStableWebAccountTag(ctx, httpClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting most recent version for proton app: %w", err)
|
||||
return nil, fmt.Errorf("getting most recent version for web-account: %w", err)
|
||||
}
|
||||
vpnGtkAppVersion, err := getMostRecentStableVPNGtkAppTag(ctx, httpClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting most recent version for linux VPN GTK app: %w", err)
|
||||
}
|
||||
|
||||
return &apiClient{
|
||||
apiURLBase: "https://account.proton.me/api",
|
||||
httpClient: httpClient,
|
||||
appVersion: appVersion,
|
||||
userAgent: userAgent,
|
||||
generator: generator,
|
||||
apiURLBase: "https://account.proton.me/api",
|
||||
httpClient: httpClient,
|
||||
appVersion: appVersion,
|
||||
vpnGtkAppVersion: vpnGtkAppVersion,
|
||||
userAgent: userAgent,
|
||||
generator: generator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -66,10 +72,10 @@ var ErrCodeNotSuccess = errors.New("response code is not success")
|
||||
// to succeed without being blocked by their "security" measures.
|
||||
// See for example [getMostRecentStableTag] on how the app version must
|
||||
// be set to a recent version or they block your request. "SeCuRiTy"...
|
||||
func (c *apiClient) setHeaders(request *http.Request, cookie cookie) {
|
||||
func (c *apiClient) setHeaders(request *http.Request, cookie cookie, appVersion string) {
|
||||
request.Header.Set("Cookie", cookie.String())
|
||||
request.Header.Set("User-Agent", c.userAgent)
|
||||
request.Header.Set("x-pm-appversion", c.appVersion)
|
||||
request.Header.Set("x-pm-appversion", appVersion)
|
||||
request.Header.Set("x-pm-locale", "en_US")
|
||||
request.Header.Set("x-pm-uid", cookie.uid)
|
||||
}
|
||||
@@ -165,7 +171,7 @@ func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
|
||||
unauthCookie := cookie{
|
||||
sessionID: sessionID,
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
@@ -252,7 +258,7 @@ func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, acces
|
||||
uid: uid,
|
||||
sessionID: sessionID,
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
@@ -325,7 +331,7 @@ func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie coo
|
||||
if err != nil {
|
||||
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
@@ -438,7 +444,7 @@ func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
@@ -590,7 +596,9 @@ func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
c.setHeaders(request, cookie)
|
||||
// Note we use the vpnGtkAppVersion field given it produces an output of more servers
|
||||
c.setHeaders(request, cookie, c.vpnGtkAppVersion)
|
||||
request.Header.Set("x-pm-appversion", "linux-vpn@4.15.2")
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,15 +7,18 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// getMostRecentStableTag finds the most recent proton-account stable tag version,
|
||||
// getMostRecentStableWebAccountTag finds the most recent proton-account stable tag version,
|
||||
// in order to use it in the x-pm-appversion http request header. Because if we do
|
||||
// fall behind on versioning, Proton doesn't like it because they like to create
|
||||
// complications where there is no need for it. Hence this function.
|
||||
func getMostRecentStableTag(ctx context.Context, client *http.Client) (version string, err error) {
|
||||
func getMostRecentStableWebAccountTag(ctx context.Context, client *http.Client) (version string, err error) {
|
||||
page := 1
|
||||
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
||||
for ctx.Err() == nil {
|
||||
@@ -69,3 +72,45 @@ func getMostRecentStableTag(ctx context.Context, client *http.Client) (version s
|
||||
|
||||
return "", fmt.Errorf("%w (queried %d pages)", context.Canceled, page)
|
||||
}
|
||||
|
||||
// getMostRecentStableVPNGtkAppTag finds the latest proton-vpn-gtk-app semver tag,
|
||||
// in order to use it in the x-pm-appversion http request header ONLY to fetch servers
|
||||
// data. Because if we do fall behind on versioning, Proton doesn't like it because they like
|
||||
// to create complications where there is no need for it. Hence this function.
|
||||
func getMostRecentStableVPNGtkAppTag(ctx context.Context, client *http.Client) (version string, err error) {
|
||||
const url = "https://api.github.com/repos/ProtonVPN/proton-vpn-gtk-app/tags?per_page=30"
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
request.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("%w: %s", ErrHTTPStatusCodeNotOK, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var data []struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
err = decoder.Decode(&data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding JSON response: %w", err)
|
||||
}
|
||||
|
||||
// Sort tags by semver. Invalid tags are placed at the end and we ignore them.
|
||||
// Yes, proton does push invalid semver tag names sometimes. Good job yet again.
|
||||
sort.Slice(data, func(i, j int) bool {
|
||||
return semver.Compare(data[i].Name, data[j].Name) > 0
|
||||
})
|
||||
|
||||
version = "linux-vpn@" + data[0].Name[1:] // remove leading v
|
||||
return version, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func filterServers(servers []models.Server,
|
||||
selection settings.ServerSelection,
|
||||
) (filtered []models.Server) {
|
||||
for _, server := range servers {
|
||||
if filterServer(server, selection) {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered = append(filtered, server)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterServer(server models.Server,
|
||||
selection settings.ServerSelection,
|
||||
) (filtered bool) {
|
||||
// Note each condition is split to make sure
|
||||
// we have full testing coverage.
|
||||
if server.VPN != selection.VPN {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterByProtocol(selection, server.TCP, server.UDP) {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.MultiHopOnly && !server.MultiHop {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.FreeOnly && !server.Free {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.PremiumOnly && !server.Premium {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.StreamOnly && !server.Stream {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.OwnedOnly && !server.Owned {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.PortForwardOnly && !server.PortForward {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.SecureCoreOnly && !server.SecureCore {
|
||||
return true
|
||||
}
|
||||
|
||||
if *selection.TorOnly && !server.Tor {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterByPossibilities(server.Country, selection.Countries) {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterAnyByPossibilities(server.Categories, selection.Categories) {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterByPossibilities(server.Region, selection.Regions) {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterByPossibilities(server.City, selection.Cities) {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterByPossibilities(server.ISP, selection.ISPs) {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterByPossibilities(server.Number, selection.Numbers) {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterByPossibilities(server.ServerName, selection.Names) {
|
||||
return true
|
||||
}
|
||||
|
||||
if filterByPossibilities(server.Hostname, selection.Hostnames) {
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO filter port forward server for PIA
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filterByPossibilities[T string | uint16](value T, possibilities []T) (filtered bool) {
|
||||
if len(possibilities) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, possibility := range possibilities {
|
||||
if strings.EqualFold(fmt.Sprint(value), fmt.Sprint(possibility)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func filterAnyByPossibilities(values, possibilities []string) (filtered bool) {
|
||||
if len(possibilities) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
if !filterByPossibilities(value, possibilities) {
|
||||
return false // found a valid value
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_FilterServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
servers []models.Server
|
||||
selection settings.ServerSelection
|
||||
filtered []models.Server
|
||||
}{
|
||||
"no server available": {
|
||||
selection: settings.ServerSelection{}.WithDefaults(providers.Mullvad),
|
||||
},
|
||||
"no filter": {
|
||||
servers: []models.Server{
|
||||
{VPN: vpn.OpenVPN, Hostname: "a", UDP: true},
|
||||
{VPN: vpn.OpenVPN, Hostname: "b", UDP: true},
|
||||
{VPN: vpn.OpenVPN, Hostname: "c", UDP: true},
|
||||
},
|
||||
selection: settings.ServerSelection{}.WithDefaults(providers.Mullvad),
|
||||
filtered: []models.Server{
|
||||
{VPN: vpn.OpenVPN, Hostname: "a", UDP: true},
|
||||
{VPN: vpn.OpenVPN, Hostname: "b", UDP: true},
|
||||
{VPN: vpn.OpenVPN, Hostname: "c", UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by VPN protocol": {
|
||||
selection: settings.ServerSelection{
|
||||
VPN: vpn.Wireguard,
|
||||
}.WithDefaults(providers.Mullvad),
|
||||
servers: []models.Server{
|
||||
{VPN: vpn.OpenVPN, Hostname: "a", UDP: true},
|
||||
{VPN: vpn.Wireguard, Hostname: "b", UDP: true},
|
||||
{VPN: vpn.OpenVPN, Hostname: "c", UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{VPN: vpn.Wireguard, Hostname: "b", UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by network protocol": {
|
||||
selection: settings.ServerSelection{
|
||||
OpenVPN: settings.OpenVPNSelection{
|
||||
Protocol: constants.TCP,
|
||||
},
|
||||
}.WithDefaults(providers.Ivpn),
|
||||
servers: []models.Server{
|
||||
{UDP: true, Hostname: "a", VPN: vpn.OpenVPN},
|
||||
{UDP: true, TCP: true, Hostname: "b", VPN: vpn.OpenVPN},
|
||||
{UDP: true, Hostname: "c", VPN: vpn.OpenVPN},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{UDP: true, TCP: true, Hostname: "b", VPN: vpn.OpenVPN},
|
||||
},
|
||||
},
|
||||
"filter by multihop only": {
|
||||
selection: settings.ServerSelection{
|
||||
MultiHopOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.Surfshark),
|
||||
servers: []models.Server{
|
||||
{MultiHop: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{MultiHop: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{MultiHop: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{MultiHop: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by free only": {
|
||||
selection: settings.ServerSelection{
|
||||
FreeOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.Surfshark),
|
||||
servers: []models.Server{
|
||||
{Free: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Free: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Free: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Free: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by premium only": {
|
||||
selection: settings.ServerSelection{
|
||||
PremiumOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.Surfshark),
|
||||
servers: []models.Server{
|
||||
{Premium: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Premium: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Premium: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Premium: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by stream only": {
|
||||
selection: settings.ServerSelection{
|
||||
StreamOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.Surfshark),
|
||||
servers: []models.Server{
|
||||
{Stream: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Stream: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Stream: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Stream: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by secure core only": {
|
||||
selection: settings.ServerSelection{
|
||||
SecureCoreOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.Protonvpn),
|
||||
servers: []models.Server{
|
||||
{SecureCore: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{SecureCore: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{SecureCore: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{SecureCore: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by tor only": {
|
||||
selection: settings.ServerSelection{
|
||||
TorOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.Protonvpn),
|
||||
servers: []models.Server{
|
||||
{Tor: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Tor: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Tor: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Tor: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by owned": {
|
||||
selection: settings.ServerSelection{
|
||||
OwnedOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.Mullvad),
|
||||
servers: []models.Server{
|
||||
{Owned: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Owned: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Owned: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Owned: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by port forwarding only": {
|
||||
selection: settings.ServerSelection{
|
||||
PortForwardOnly: boolPtr(true),
|
||||
}.WithDefaults(providers.PrivateInternetAccess),
|
||||
servers: []models.Server{
|
||||
{PortForward: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
{PortForward: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
{PortForward: false, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{PortForward: true, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by country": {
|
||||
selection: settings.ServerSelection{
|
||||
Countries: []string{"b"},
|
||||
}.WithDefaults(providers.Mullvad),
|
||||
servers: []models.Server{
|
||||
{Country: "a", VPN: vpn.OpenVPN, UDP: true},
|
||||
{Country: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
{Country: "c", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Country: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by region": {
|
||||
selection: settings.ServerSelection{
|
||||
Regions: []string{"b"},
|
||||
}.WithDefaults(providers.Surfshark),
|
||||
servers: []models.Server{
|
||||
{Region: "a", VPN: vpn.OpenVPN, UDP: true},
|
||||
{Region: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
{Region: "c", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Region: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by city": {
|
||||
selection: settings.ServerSelection{
|
||||
Cities: []string{"b"},
|
||||
}.WithDefaults(providers.Mullvad),
|
||||
servers: []models.Server{
|
||||
{City: "a", VPN: vpn.OpenVPN, UDP: true},
|
||||
{City: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
{City: "c", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{City: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by category": {
|
||||
selection: settings.ServerSelection{
|
||||
Categories: []string{"legacy_p2p"},
|
||||
}.WithDefaults(providers.Nordvpn),
|
||||
servers: []models.Server{
|
||||
{Categories: []string{"legacy_p2p"}, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Categories: []string{"legacy_standard"}, VPN: vpn.OpenVPN, UDP: true},
|
||||
{VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Categories: []string{"legacy_p2p"}, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by ISP": {
|
||||
selection: settings.ServerSelection{
|
||||
ISPs: []string{"b"},
|
||||
}.WithDefaults(providers.Mullvad),
|
||||
servers: []models.Server{
|
||||
{ISP: "a", VPN: vpn.OpenVPN, UDP: true},
|
||||
{ISP: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
{ISP: "c", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{ISP: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by number": {
|
||||
selection: settings.ServerSelection{
|
||||
Numbers: []uint16{1},
|
||||
}.WithDefaults(providers.Mullvad),
|
||||
servers: []models.Server{
|
||||
{Number: 0, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Number: 1, VPN: vpn.OpenVPN, UDP: true},
|
||||
{Number: 2, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Number: 1, VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by server name": {
|
||||
selection: settings.ServerSelection{
|
||||
Names: []string{"b"},
|
||||
}.WithDefaults(providers.Mullvad),
|
||||
servers: []models.Server{
|
||||
{ServerName: "a", VPN: vpn.OpenVPN, UDP: true},
|
||||
{ServerName: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
{ServerName: "c", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{ServerName: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
"filter by hostname": {
|
||||
selection: settings.ServerSelection{
|
||||
Hostnames: []string{"b"},
|
||||
}.WithDefaults(providers.Mullvad),
|
||||
servers: []models.Server{
|
||||
{Hostname: "a", VPN: vpn.OpenVPN, UDP: true},
|
||||
{Hostname: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
{Hostname: "c", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
filtered: []models.Server{
|
||||
{Hostname: "b", VPN: vpn.OpenVPN, UDP: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filtered := filterServers(testCase.servers, testCase.selection)
|
||||
|
||||
assert.Equal(t, testCase.filtered, filtered)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_filterByPossibilities(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
value string
|
||||
possibilities []string
|
||||
filtered bool
|
||||
}{
|
||||
"no possibilities": {},
|
||||
"value not in possibilities": {
|
||||
value: "c",
|
||||
possibilities: []string{"a", "b"},
|
||||
filtered: true,
|
||||
},
|
||||
"value in possibilities": {
|
||||
value: "c",
|
||||
possibilities: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filtered := filterByPossibilities(testCase.value, testCase.possibilities)
|
||||
assert.Equal(t, testCase.filtered, filtered)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -62,14 +62,9 @@ func OpenVPNConfig(provider OpenVPNProviderSettings,
|
||||
lines.add("mute-replay-warnings") // these are often ignored by some VPN providers
|
||||
lines.add("auth-retry", "nointeract") // retry authenticating without interaction
|
||||
lines.add("suppress-timestamps") // do not log timestamps, the Gluetun logger takes care of it
|
||||
lines.add("handshake-window", "10") // default is 60 seconds which is too long
|
||||
lines.add("dev", settings.Interface)
|
||||
lines.add("verb", fmt.Sprint(*settings.Verbosity))
|
||||
protocol := connection.Protocol
|
||||
if protocol == constants.TCP {
|
||||
protocol = "tcp-client"
|
||||
}
|
||||
lines.add("proto", protocol)
|
||||
lines.add("proto", connection.Protocol)
|
||||
lines.add("remote", connection.IP.String(), fmt.Sprint(connection.Port))
|
||||
|
||||
if *settings.User != "" {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
func uint16Ptr(n uint16) *uint16 { return &n }
|
||||
|
||||
func Test_GetPort(t *testing.T) {
|
||||
|
||||
@@ -25,8 +25,6 @@ type PortForwardObjects struct {
|
||||
Username string
|
||||
// Password is used by Private Internet Access for port forwarding.
|
||||
Password string
|
||||
// PortsCount is used by ProtonVPN for port forwarding.
|
||||
PortsCount uint16
|
||||
}
|
||||
|
||||
type Routing interface {
|
||||
|
||||
+35468
-144434
File diff suppressed because it is too large
Load Diff
@@ -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", "tcp-client":
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
return true, false, nil
|
||||
case "udp", "udp4", "udp6":
|
||||
return false, true, nil
|
||||
|
||||
@@ -52,7 +52,7 @@ type Provider interface {
|
||||
type PortForwarder interface {
|
||||
Name() string
|
||||
PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||
internalToExternalPorts map[uint16]uint16, err error)
|
||||
ports []uint16, err error)
|
||||
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ func (n *noPortForwarder) Name() string {
|
||||
}
|
||||
|
||||
func (n *noPortForwarder) PortForward(context.Context, pfutils.PortForwardObjects) (
|
||||
internalToExternalPorts map[uint16]uint16, err error,
|
||||
ports []uint16, err error,
|
||||
) {
|
||||
return nil, fmt.Errorf("%w: for %s", ErrPortForwardingNotSupported, n.providerName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user