mirror of
https://github.com/qdm12/gluetun.git
synced 2026-08-02 02:13:25 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f21596cf4 |
@@ -1,3 +1,2 @@
|
|||||||
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
||||||
RUN apk add wireguard-tools htop openssl tcpdump iptables
|
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_PROVIDER= \
|
||||||
VPN_PORT_FORWARDING_UP_COMMAND= \
|
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||||
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||||
VPN_PORT_FORWARDING_LISTENING_PORTS=0 \
|
VPN_PORT_FORWARDING_LISTENING_PORT=0 \
|
||||||
VPN_PORT_FORWARDING_PORTS_COUNT=1 \
|
|
||||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||||
# PMTUD
|
# PMTUD
|
||||||
PMTUD_ICMP_ADDRESSES=1.1.1.1,8.8.8.8 \
|
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/genetlink v1.3.2
|
||||||
github.com/mdlayher/netlink v1.9.0
|
github.com/mdlayher/netlink v1.9.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/qdm12/dns/v2 v2.0.0-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/gosettings v0.4.4
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c
|
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c
|
||||||
@@ -26,6 +26,7 @@ require (
|
|||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
|
golang.org/x/mod v0.33.0
|
||||||
golang.org/x/net v0.51.0
|
golang.org/x/net v0.51.0
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/sys v0.42.0
|
||||||
golang.org/x/text v0.35.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/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // 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/sync v0.20.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // 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/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
|
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.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
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 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type BoringPoll struct {
|
|||||||
|
|
||||||
// Internal signals and channels
|
// Internal signals and channels
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
done *sync.WaitGroup
|
done <-chan struct{}
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,6 @@ func (b *BoringPoll) Start() (runError <-chan error, err error) {
|
|||||||
const logEveryBytes = 100 * 1000 * 1000 // 100 IEC MB
|
const logEveryBytes = 100 * 1000 * 1000 // 100 IEC MB
|
||||||
|
|
||||||
var ready, done sync.WaitGroup
|
var ready, done sync.WaitGroup
|
||||||
b.done = &done
|
|
||||||
ready.Add(len(b.urlToData))
|
ready.Add(len(b.urlToData))
|
||||||
done.Add(len(b.urlToData))
|
done.Add(len(b.urlToData))
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -167,7 +166,7 @@ func (b *BoringPoll) Stop() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
b.cancel()
|
b.cancel()
|
||||||
b.done.Wait()
|
<-b.done
|
||||||
b.cancel = nil
|
b.cancel = nil
|
||||||
b.done = nil
|
b.done = nil
|
||||||
return 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 {
|
func (d DNS) String() string {
|
||||||
return d.toLinesNode().String()
|
return d.toLinesNode().String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate EncPreset
|
// Validate EncPreset
|
||||||
if vpnProvider == providers.PrivateInternetAccess {
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
validEncryptionPresets := []string{
|
validEncryptionPresets := []string{
|
||||||
|
presets.None,
|
||||||
presets.Normal,
|
presets.Normal,
|
||||||
presets.Strong,
|
presets.Strong,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gosettings"
|
"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 can be the empty string to indicate to NOT run a command.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
DownCommand *string `json:"down_command"`
|
DownCommand *string `json:"down_command"`
|
||||||
// ListeningPorts are the ports traffic would be redirected to from the
|
// ListeningPort is the port traffic would be redirected to from the
|
||||||
// forwarded ports. The redirection is disabled if it is the slice [0],
|
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||||
// which is its default as well. If set and not [0], its length must match
|
// is its default as well.
|
||||||
// the PortsCount value, such that each forwarded port is redirected to
|
ListeningPort *uint16 `json:"listening_port"`
|
||||||
// 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"`
|
|
||||||
// Username is only used for Private Internet Access port forwarding.
|
// Username is only used for Private Internet Access port forwarding.
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
// Password is only used for Private Internet Access port forwarding.
|
// Password is only used for Private Internet Access port forwarding.
|
||||||
Password string `json:"password"`
|
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) {
|
func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||||
if !*p.Enabled {
|
if !*p.Enabled {
|
||||||
return nil
|
return nil
|
||||||
@@ -89,36 +75,13 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch providerSelected {
|
if providerSelected == providers.PrivateInternetAccess {
|
||||||
case providers.PrivateInternetAccess:
|
|
||||||
const maxPortsCount = 1
|
|
||||||
switch {
|
switch {
|
||||||
case p.PortsCount > maxPortsCount:
|
|
||||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
|
|
||||||
case p.Username == "":
|
case p.Username == "":
|
||||||
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
|
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
|
||||||
case p.Password == "":
|
case p.Password == "":
|
||||||
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
|
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
|
return nil
|
||||||
@@ -126,14 +89,14 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
|||||||
|
|
||||||
func (p *PortForwarding) Copy() (copied PortForwarding) {
|
func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||||
return PortForwarding{
|
return PortForwarding{
|
||||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
Provider: gosettings.CopyPointer(p.Provider),
|
Provider: gosettings.CopyPointer(p.Provider),
|
||||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||||
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||||
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||||
ListeningPorts: gosettings.CopySlice(p.ListeningPorts),
|
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||||
Username: p.Username,
|
Username: p.Username,
|
||||||
Password: p.Password,
|
Password: p.Password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +106,7 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
|||||||
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||||
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
||||||
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
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.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||||
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
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.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||||
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
||||||
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
||||||
p.ListeningPorts = gosettings.DefaultSlice(p.ListeningPorts, []uint16{0}) // disabled
|
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
||||||
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PortForwarding) String() string {
|
func (p PortForwarding) String() string {
|
||||||
@@ -169,14 +131,11 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
|||||||
|
|
||||||
node = gotree.New("Automatic port forwarding settings:")
|
node = gotree.New("Automatic port forwarding settings:")
|
||||||
|
|
||||||
node.Appendf("Number of ports to be forwarded: %d", p.PortsCount)
|
listeningPort := "disabled"
|
||||||
|
if *p.ListeningPort != 0 {
|
||||||
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
|
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
|
||||||
redirNode := node.Appendf("Redirection for listening ports:")
|
|
||||||
for i, port := range p.ListeningPorts {
|
|
||||||
redirNode.Appendf("Port #%d -> %d", i+1, port)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
node.Appendf("Redirection listening port: %s", listeningPort)
|
||||||
|
|
||||||
if *p.Provider == "" {
|
if *p.Provider == "" {
|
||||||
node.Appendf("Use port forwarding code for current 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",
|
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
|
||||||
reader.ForceLowercase(false))
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
p.ListeningPorts, err = r.CSVUint16("VPN_PORT_FORWARDING_LISTENING_PORTS",
|
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||||
reader.RetroKeys("VPN_PORT_FORWARDING_LISTENING_PORT"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.PortsCount, err = r.Uint16("VPN_PORT_FORWARDING_PORTS_COUNT")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ type Logger interface {
|
|||||||
Info(s string)
|
Info(s string)
|
||||||
Warn(s string)
|
Warn(s string)
|
||||||
Error(s string)
|
Error(s string)
|
||||||
Errorf(format string, args ...any)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cmder interface {
|
type Cmder interface {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/portforward/service"
|
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||||
@@ -43,12 +42,11 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
|
|||||||
settings: Settings{
|
settings: Settings{
|
||||||
VPNIsUp: ptrTo(false),
|
VPNIsUp: ptrTo(false),
|
||||||
Service: service.Settings{
|
Service: service.Settings{
|
||||||
Enabled: settings.Enabled,
|
Enabled: settings.Enabled,
|
||||||
Filepath: *settings.Filepath,
|
Filepath: *settings.Filepath,
|
||||||
UpCommand: *settings.UpCommand,
|
UpCommand: *settings.UpCommand,
|
||||||
DownCommand: *settings.DownCommand,
|
DownCommand: *settings.DownCommand,
|
||||||
ListeningPorts: settings.ListeningPorts,
|
ListeningPort: *settings.ListeningPort,
|
||||||
PortsCount: settings.PortsCount,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routing: routing,
|
routing: routing,
|
||||||
@@ -88,8 +86,6 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
defer close(runDone)
|
defer close(runDone)
|
||||||
|
|
||||||
var serviceRunError <-chan error
|
var serviceRunError <-chan error
|
||||||
var retryAfter <-chan time.Time
|
|
||||||
const retryDelay = 5 * time.Second
|
|
||||||
for {
|
for {
|
||||||
updateReceived := false
|
updateReceived := false
|
||||||
select {
|
select {
|
||||||
@@ -108,12 +104,10 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
l.settingsMutex.Unlock()
|
l.settingsMutex.Unlock()
|
||||||
case err := <-serviceRunError:
|
case err := <-serviceRunError:
|
||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
case <-retryAfter:
|
|
||||||
// Retry starting the service after a delay
|
|
||||||
retryAfter = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.service != nil {
|
firstRun := serviceRunError == nil
|
||||||
|
if !firstRun {
|
||||||
err := l.service.Stop()
|
err := l.service.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
||||||
@@ -137,10 +131,6 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
err = fmt.Errorf("starting port forwarding service: %w", err)
|
err = fmt.Errorf("starting port forwarding service: %w", err)
|
||||||
}
|
}
|
||||||
updateResult <- err
|
updateResult <- err
|
||||||
} else if err != nil {
|
|
||||||
// Log the error and schedule a retry
|
|
||||||
l.logger.Errorf("starting port forwarding service: %s - retrying in %s", err, retryDelay)
|
|
||||||
retryAfter = time.After(retryDelay)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type Logger interface {
|
|||||||
type PortForwarder interface {
|
type PortForwarder interface {
|
||||||
Name() string
|
Name() string
|
||||||
PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
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)
|
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)
|
return fmt.Errorf("cleaning up: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
internalToExternalPorts := make(map[uint16]uint16, len(ports))
|
err = s.onNewPorts(ctx, ports)
|
||||||
for _, port := range ports {
|
|
||||||
internalToExternalPorts[port] = port
|
|
||||||
}
|
|
||||||
err = s.onNewPorts(ctx, internalToExternalPorts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("handling new ports: %w", err)
|
return fmt.Errorf("handling new ports: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
@@ -18,8 +17,7 @@ type Settings struct {
|
|||||||
Interface string // needed for PIA, PrivateVPN and ProtonVPN, tun0 for example
|
Interface string // needed for PIA, PrivateVPN and ProtonVPN, tun0 for example
|
||||||
ServerName string // needed for PIA
|
ServerName string // needed for PIA
|
||||||
CanPortForward bool // needed for PIA
|
CanPortForward bool // needed for PIA
|
||||||
ListeningPorts []uint16
|
ListeningPort uint16
|
||||||
PortsCount uint16
|
|
||||||
Username string // needed for PIA
|
Username string // needed for PIA
|
||||||
Password string // needed for PIA
|
Password string // needed for PIA
|
||||||
}
|
}
|
||||||
@@ -33,8 +31,7 @@ func (s Settings) Copy() (copied Settings) {
|
|||||||
copied.Interface = s.Interface
|
copied.Interface = s.Interface
|
||||||
copied.ServerName = s.ServerName
|
copied.ServerName = s.ServerName
|
||||||
copied.CanPortForward = s.CanPortForward
|
copied.CanPortForward = s.CanPortForward
|
||||||
copied.ListeningPorts = gosettings.CopySlice(s.ListeningPorts)
|
copied.ListeningPort = s.ListeningPort
|
||||||
copied.PortsCount = s.PortsCount
|
|
||||||
copied.Username = s.Username
|
copied.Username = s.Username
|
||||||
copied.Password = s.Password
|
copied.Password = s.Password
|
||||||
return copied
|
return copied
|
||||||
@@ -49,8 +46,7 @@ func (s *Settings) OverrideWith(update Settings) {
|
|||||||
s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface)
|
s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface)
|
||||||
s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName)
|
s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName)
|
||||||
s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward)
|
s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward)
|
||||||
s.ListeningPorts = gosettings.OverrideWithSlice(s.ListeningPorts, update.ListeningPorts)
|
s.ListeningPort = gosettings.OverrideWithComparable(s.ListeningPort, update.ListeningPort)
|
||||||
s.PortsCount = gosettings.OverrideWithComparable(s.PortsCount, update.PortsCount)
|
|
||||||
s.Username = gosettings.OverrideWithComparable(s.Username, update.Username)
|
s.Username = gosettings.OverrideWithComparable(s.Username, update.Username)
|
||||||
s.Password = gosettings.OverrideWithComparable(s.Password, update.Password)
|
s.Password = gosettings.OverrideWithComparable(s.Password, update.Password)
|
||||||
}
|
}
|
||||||
@@ -62,10 +58,6 @@ var (
|
|||||||
ErrPasswordNotSet = errors.New("password not set")
|
ErrPasswordNotSet = errors.New("password not set")
|
||||||
ErrFilepathNotSet = errors.New("file path not set")
|
ErrFilepathNotSet = errors.New("file path not set")
|
||||||
ErrInterfaceNotSet = errors.New("interface 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) {
|
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)
|
return fmt.Errorf("%w", ErrPortForwarderNotSet)
|
||||||
case s.Interface == "":
|
case s.Interface == "":
|
||||||
return fmt.Errorf("%w", ErrInterfaceNotSet)
|
return fmt.Errorf("%w", ErrInterfaceNotSet)
|
||||||
case s.PortsCount == 0:
|
case s.PortForwarder.Name() == providers.PrivateInternetAccess:
|
||||||
return fmt.Errorf("%w", ErrPortsCountZero)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s.PortForwarder.Name() {
|
|
||||||
case providers.PrivateInternetAccess:
|
|
||||||
switch {
|
switch {
|
||||||
case s.ServerName == "":
|
case s.ServerName == "":
|
||||||
return fmt.Errorf("%w", ErrServerNameNotSet)
|
return fmt.Errorf("%w", ErrServerNameNotSet)
|
||||||
@@ -100,26 +87,6 @@ func (s *Settings) Validate(forStartup bool) (err error) {
|
|||||||
case s.Password == "":
|
case s.Password == "":
|
||||||
return fmt.Errorf("%w", ErrPasswordNotSet)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/netlink"
|
"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,
|
CanPortForward: s.settings.CanPortForward,
|
||||||
Username: s.settings.Username,
|
Username: s.settings.Username,
|
||||||
Password: s.settings.Password,
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("port forwarding for the first time: %w", err)
|
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()
|
s.portMutex.Lock()
|
||||||
defer s.portMutex.Unlock()
|
defer s.portMutex.Unlock()
|
||||||
|
|
||||||
err = s.onNewPorts(ctx, internalToExternalPorts)
|
err = s.onNewPorts(ctx, ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -88,60 +86,36 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
|
|||||||
return runErrorCh, nil
|
return runErrorCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[uint16]uint16) (err error) {
|
func (s *Service) onNewPorts(ctx context.Context, ports []uint16) (err error) {
|
||||||
autoRedirectionNeeded := false
|
slices.Sort(ports)
|
||||||
externalToInternalPorts := make(map[uint16]uint16, len(internalToExternalPorts))
|
|
||||||
for internal, external := range internalToExternalPorts {
|
|
||||||
externalToInternalPorts[external] = internal
|
|
||||||
if internal != external {
|
|
||||||
autoRedirectionNeeded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
externalPorts := slices.Collect(maps.Keys(externalToInternalPorts))
|
s.logger.Info(portsToString(ports))
|
||||||
slices.Sort(externalPorts)
|
|
||||||
|
|
||||||
s.logger.Info(portsToString(externalPorts))
|
for _, port := range ports {
|
||||||
|
err = s.portAllower.SetAllowedPort(ctx, port, s.settings.Interface)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("allowing port in firewall: %w", err)
|
return fmt.Errorf("allowing port in firewall: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourcePort, destinationPort uint16
|
if s.settings.ListeningPort != 0 {
|
||||||
switch {
|
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, port, s.settings.ListeningPort)
|
||||||
case userRedirectionEnabled: // precedence over auto redirection
|
if err != nil {
|
||||||
sourcePort = externalToInternalPorts[port]
|
return fmt.Errorf("redirecting port in firewall: %w", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.writePortForwardedFile(externalPorts)
|
err = s.writePortForwardedFile(ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = s.cleanup()
|
_ = s.cleanup()
|
||||||
return fmt.Errorf("writing port file: %w", err)
|
return fmt.Errorf("writing port file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ports = make([]uint16, len(internalToExternalPorts))
|
s.ports = make([]uint16, len(ports))
|
||||||
copy(s.ports, externalPorts)
|
copy(s.ports, ports)
|
||||||
|
|
||||||
if s.settings.UpCommand != "" {
|
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 {
|
if err != nil {
|
||||||
err = fmt.Errorf("running up command: %w", err)
|
err = fmt.Errorf("running up command: %w", err)
|
||||||
s.logger.Error(err.Error())
|
s.logger.Error(err.Error())
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,14 +38,13 @@ func (s *Service) cleanup() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectionWasEnabled := !slices.Equal(s.settings.ListeningPorts, []uint16{0})
|
|
||||||
for _, port := range s.ports {
|
for _, port := range s.ports {
|
||||||
err = s.portAllower.RemoveAllowedPort(context.Background(), port)
|
err = s.portAllower.RemoveAllowedPort(context.Background(), port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("blocking previous port in firewall: %w", err)
|
return fmt.Errorf("blocking previous port in firewall: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if redirectionWasEnabled {
|
if s.settings.ListeningPort != 0 {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
const listeningPort = 0 // 0 to clear the redirection
|
const listeningPort = 0 // 0 to clear the redirection
|
||||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, port, listeningPort)
|
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, "pull-filter ignore \"auth-token\"") // prevent auth failed loop
|
||||||
modified = append(modified, "auth-retry nointeract")
|
modified = append(modified, "auth-retry nointeract")
|
||||||
modified = append(modified, "suppress-timestamps")
|
modified = append(modified, "suppress-timestamps")
|
||||||
modified = append(modified, "handshake-window 10") // default is 60 seconds which is too long
|
|
||||||
if *settings.User != "" {
|
if *settings.User != "" {
|
||||||
modified = append(modified, "auth-user-pass "+openvpn.AuthConf)
|
modified = append(modified, "auth-user-pass "+openvpn.AuthConf)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ func Test_modifyConfig(t *testing.T) {
|
|||||||
"pull-filter ignore \"auth-token\"",
|
"pull-filter ignore \"auth-token\"",
|
||||||
"auth-retry nointeract",
|
"auth-retry nointeract",
|
||||||
"suppress-timestamps",
|
"suppress-timestamps",
|
||||||
"handshake-window 10",
|
|
||||||
"auth-user-pass /etc/openvpn/auth.conf",
|
"auth-user-pass /etc/openvpn/auth.conf",
|
||||||
"verb 0",
|
"verb 0",
|
||||||
"data-ciphers-fallback cipher",
|
"data-ciphers-fallback cipher",
|
||||||
|
|||||||
@@ -10,17 +10,12 @@ import (
|
|||||||
// PortForward calculates and returns the VPN server side ports forwarded.
|
// PortForward calculates and returns the VPN server side ports forwarded.
|
||||||
func (p *Provider) PortForward(_ context.Context,
|
func (p *Provider) PortForward(_ context.Context,
|
||||||
objects utils.PortForwardObjects,
|
objects utils.PortForwardObjects,
|
||||||
) (internalToExternalPorts map[uint16]uint16, err error) {
|
) (ports []uint16, err error) {
|
||||||
if !objects.InternalIP.IsValid() {
|
if !objects.InternalIP.IsValid() {
|
||||||
panic("internal ip is not set")
|
panic("internal ip is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
ports := internalIPToPorts(objects.InternalIP)
|
return internalIPToPorts(objects.InternalIP), nil
|
||||||
internalToExternalPorts = make(map[uint16]uint16, len(ports))
|
|
||||||
for _, port := range ports {
|
|
||||||
internalToExternalPorts[port] = port
|
|
||||||
}
|
|
||||||
return internalToExternalPorts, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) KeepPortForward(ctx context.Context,
|
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.
|
// Set port defaults depending on encryption preset.
|
||||||
var defaults utils.ConnectionDefaults
|
var defaults utils.ConnectionDefaults
|
||||||
switch *selection.OpenVPN.PIAEncPreset {
|
switch *selection.OpenVPN.PIAEncPreset {
|
||||||
case presets.Normal:
|
case presets.None, presets.Normal:
|
||||||
defaults.OpenVPNTCPPort = 502
|
defaults.OpenVPNTCPPort = 502
|
||||||
defaults.OpenVPNUDPPort = 1198
|
defaults.OpenVPNUDPPort = 1198
|
||||||
case presets.Strong:
|
case presets.Strong:
|
||||||
|
|||||||
@@ -24,10 +24,16 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
|||||||
)
|
)
|
||||||
switch *settings.PIAEncPreset {
|
switch *settings.PIAEncPreset {
|
||||||
case presets.Normal:
|
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}
|
providerSettings.CAs = []string{caNormalPreset}
|
||||||
default: // strong
|
default: // strong
|
||||||
providerSettings.Ciphers = []string{openvpn.AES256gcm}
|
providerSettings.Ciphers = []string{openvpn.AES256cbc}
|
||||||
|
providerSettings.Auth = openvpn.SHA256
|
||||||
providerSettings.CAs = []string{"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"} //nolint:lll
|
providerSettings.CAs = []string{"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"} //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.
|
// PortForward obtains a VPN server side port forwarded from PIA.
|
||||||
func (p *Provider) PortForward(ctx context.Context,
|
func (p *Provider) PortForward(ctx context.Context,
|
||||||
objects utils.PortForwardObjects,
|
objects utils.PortForwardObjects,
|
||||||
) (internalToExternalPorts map[uint16]uint16, err error) {
|
) (ports []uint16, err error) {
|
||||||
switch {
|
switch {
|
||||||
case objects.ServerName == "":
|
case objects.ServerName == "":
|
||||||
panic("server name cannot be empty")
|
panic("server name cannot be empty")
|
||||||
@@ -39,6 +39,7 @@ func (p *Provider) PortForward(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverName := objects.ServerName
|
serverName := objects.ServerName
|
||||||
|
apiIP := buildAPIIPAddress(objects.Gateway)
|
||||||
logger := objects.Logger
|
logger := objects.Logger
|
||||||
|
|
||||||
if !objects.CanPortForward {
|
if !objects.CanPortForward {
|
||||||
@@ -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 {
|
if !dataFound || expired {
|
||||||
client := objects.Client
|
client := objects.Client
|
||||||
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, p.apiIP,
|
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, apiIP,
|
||||||
p.portForwardPath, objects.Username, objects.Password)
|
p.portForwardPath, objects.Username, objects.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("refreshing port forward data: %w", err)
|
return nil, fmt.Errorf("refreshing port forward data: %w", err)
|
||||||
@@ -84,11 +80,11 @@ func (p *Provider) PortForward(ctx context.Context,
|
|||||||
logger.Info("Port forwarded data expires in " + format.FriendlyDuration(durationToExpiration))
|
logger.Info("Port forwarded data expires in " + format.FriendlyDuration(durationToExpiration))
|
||||||
|
|
||||||
// First time binding
|
// 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 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")
|
var ErrPortForwardedExpired = errors.New("port forwarded data expired")
|
||||||
@@ -103,6 +99,8 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
panic("gateway is not set")
|
panic("gateway is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiIP := buildAPIIPAddress(objects.Gateway)
|
||||||
|
|
||||||
privateIPClient, err := newHTTPClient(objects.ServerName)
|
privateIPClient, err := newHTTPClient(objects.ServerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating custom HTTP client: %w", err)
|
return fmt.Errorf("creating custom HTTP client: %w", err)
|
||||||
@@ -130,7 +128,7 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
}
|
}
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-keepAliveTimer.C:
|
case <-keepAliveTimer.C:
|
||||||
err = bindPort(ctx, privateIPClient, p.apiIP, data)
|
err = bindPort(ctx, privateIPClient, apiIP, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("binding port: %w", err)
|
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 buildAPIIPAddress(gateway netip.Addr) (api netip.Addr) {
|
||||||
|
|
||||||
func findAPIIP(ctx context.Context, client *http.Client, gateway netip.Addr) (
|
|
||||||
apiIP netip.Addr, err error,
|
|
||||||
) {
|
|
||||||
if gateway.Is6() {
|
if gateway.Is6() {
|
||||||
panic("IPv6 gateway not supported")
|
panic("IPv6 gateway not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
gatewayBytes := gateway.As4()
|
gatewayBytes := gateway.As4()
|
||||||
gatewayBytes[3] = 1 // x.y.z.1
|
gatewayBytes[2] = 128
|
||||||
|
gatewayBytes[3] = 1
|
||||||
gatewayBytes[2] = 128 // x.y.128.1
|
return netip.AddrFrom4(gatewayBytes)
|
||||||
oldAPIIP := netip.AddrFrom4(gatewayBytes)
|
|
||||||
gatewayBytes[2] = 0 // x.y.0.1 - new API IP reported by some users
|
|
||||||
newAPIIP := netip.AddrFrom4(gatewayBytes)
|
|
||||||
possibleIPs := []netip.Addr{oldAPIIP, newAPIIP}
|
|
||||||
|
|
||||||
errs := make([]error, 0, len(possibleIPs))
|
|
||||||
for _, ip := range possibleIPs {
|
|
||||||
const timeout = 5 * time.Second
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
url := url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: net.JoinHostPort(ip.String(), "19999"),
|
|
||||||
Path: "/ping",
|
|
||||||
}
|
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("trying IP %s: %w", ip, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("trying IP %s: %w", ip, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = response.Body.Close()
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return netip.Addr{}, fmt.Errorf("%w: %w", errAPIIPNotFound, errors.Join(errs...))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client,
|
func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package presets
|
package presets
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
None = "none"
|
||||||
Normal = "normal"
|
Normal = "normal"
|
||||||
Strong = "strong"
|
Strong = "strong"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package privateinternetaccess
|
|||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
@@ -18,7 +17,6 @@ type Provider struct {
|
|||||||
common.Fetcher
|
common.Fetcher
|
||||||
// Port forwarding
|
// Port forwarding
|
||||||
portForwardPath string
|
portForwardPath string
|
||||||
apiIP netip.Addr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type serverData struct {
|
|||||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
func fetchAPI(ctx context.Context, client *http.Client) (
|
||||||
data apiData, err error,
|
data apiData, err error,
|
||||||
) {
|
) {
|
||||||
const url = "https://serverlist.piaservers.net/vpninfo/servers/v7"
|
const url = "https://serverlist.piaservers.net/vpninfo/servers/v6"
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -77,9 +77,6 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
|||||||
|
|
||||||
func addData(regions []regionData, nts nameToServer) (change bool) {
|
func addData(regions []regionData, nts nameToServer) (change bool) {
|
||||||
for _, region := range regions {
|
for _, region := range regions {
|
||||||
if region.Offline {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, server := range region.Servers.UDP {
|
for _, server := range region.Servers.UDP {
|
||||||
const tcp, udp = false, true
|
const tcp, udp = false, true
|
||||||
if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) {
|
if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var ErrPortForwardedNotFound = errors.New("port forwarded not found")
|
|||||||
// PortForward obtains a VPN server side port forwarded from the PrivateVPN API.
|
// 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.
|
// It returns 0 if all ports are to forwarded on a dedicated server IP.
|
||||||
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
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
|
// Define a timeout since the default client has a large timeout and we don't
|
||||||
// want to wait too long.
|
// want to wait too long.
|
||||||
@@ -75,8 +75,7 @@ func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObj
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing port: %w", err)
|
return nil, fmt.Errorf("parsing port: %w", err)
|
||||||
}
|
}
|
||||||
port := uint16(portUint64)
|
return []uint16{uint16(portUint64)}, nil
|
||||||
return map[uint16]uint16{port: port}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) KeepPortForward(ctx context.Context,
|
func (p *Provider) KeepPortForward(ctx context.Context,
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ func Test_Provider_PortForward(t *testing.T) {
|
|||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
objects utils.PortForwardObjects
|
objects utils.PortForwardObjects
|
||||||
internalToExternalPorts map[uint16]uint16
|
ports []uint16
|
||||||
errMessage string
|
errMessage string
|
||||||
}{
|
}{
|
||||||
"canceled context": {
|
"canceled context": {
|
||||||
ctx: canceledCtx,
|
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 {
|
for name, testCase := range testCases {
|
||||||
@@ -203,7 +203,7 @@ func Test_Provider_PortForward(t *testing.T) {
|
|||||||
ports, err := provider.PortForward(testCase.ctx,
|
ports, err := provider.PortForward(testCase.ctx,
|
||||||
testCase.objects)
|
testCase.objects)
|
||||||
|
|
||||||
assert.Equal(t, testCase.internalToExternalPorts, ports)
|
assert.Equal(t, testCase.ports, ports)
|
||||||
if testCase.errMessage != "" {
|
if testCase.errMessage != "" {
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,18 +13,17 @@ import (
|
|||||||
|
|
||||||
var ErrServerPortForwardNotSupported = errors.New("server does not support port forwarding")
|
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.
|
// PortForward obtains a VPN server side port forwarded from ProtonVPN gateway.
|
||||||
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||||
internalToExternalPorts map[uint16]uint16, err error,
|
ports []uint16, err error,
|
||||||
) {
|
) {
|
||||||
if !objects.CanPortForward {
|
if !objects.CanPortForward {
|
||||||
return nil, fmt.Errorf("%w", ErrServerPortForwardNotSupported)
|
return nil, fmt.Errorf("%w", ErrServerPortForwardNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := natpmp.New()
|
client := natpmp.New()
|
||||||
_, externalIPv4Address, err := client.ExternalAddress(ctx, objects.Gateway)
|
_, externalIPv4Address, err := client.ExternalAddress(ctx,
|
||||||
|
objects.Gateway)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(err.Error(), "connection refused"):
|
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 := objects.Logger
|
||||||
|
|
||||||
logger.Debug("gateway external IPv4 address is " + externalIPv4Address.String())
|
logger.Info("gateway external IPv4 address is " + externalIPv4Address.String())
|
||||||
const externalPort = 0
|
const internalPort, externalPort = 0, 1
|
||||||
const lifetime = 60 * time.Second
|
const lifetime = 60 * time.Second
|
||||||
|
|
||||||
p.internalToExternalPorts = make(map[uint16]uint16, objects.PortsCount)
|
_, _, assignedUDPExternalPort, assignedLifetime, err := client.AddPortMapping(ctx, objects.Gateway, "udp",
|
||||||
for i := range objects.PortsCount {
|
internalPort, externalPort, lifetime)
|
||||||
internalPort := nonSymmetricPortStart + i
|
if err != nil {
|
||||||
protoToInternalPort := map[string]uint16{
|
return nil, fmt.Errorf("adding UDP port mapping: %w", err)
|
||||||
"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"]
|
|
||||||
}
|
}
|
||||||
|
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,
|
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) {
|
func checkExternalPorts(logger utils.Logger, udpPort, tcpPort uint16) {
|
||||||
if udpPort != tcpPort {
|
if udpPort != tcpPort {
|
||||||
logger.Warn(fmt.Sprintf("UDP external port %d differs from TCP external port %d",
|
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 (
|
var ErrExternalPortChanged = errors.New("external port changed")
|
||||||
ErrInternalPortChanged = errors.New("internal port changed")
|
|
||||||
ErrExternalPortChanged = errors.New("external port changed")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Provider) KeepPortForward(ctx context.Context,
|
func (p *Provider) KeepPortForward(ctx context.Context,
|
||||||
objects utils.PortForwardObjects,
|
objects utils.PortForwardObjects,
|
||||||
@@ -123,28 +96,32 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.Logger.Debug("refreshing forwarded ports since 45 seconds have elapsed")
|
objects.Logger.Debug("refreshing port forward since 45 seconds have elapsed")
|
||||||
networkProtocols := [...]string{"udp", "tcp"}
|
networkProtocols := []string{"udp", "tcp"}
|
||||||
|
const internalPort = 0
|
||||||
const lifetime = 60 * time.Second
|
const lifetime = 60 * time.Second
|
||||||
for internalPort, externalPort := range p.internalToExternalPorts {
|
|
||||||
for _, networkProtocol := range networkProtocols {
|
for _, networkProtocol := range networkProtocols {
|
||||||
_, assignedInternalPort, assignedExternalPort, assignedLiftetime, err := client.AddPortMapping(
|
_, _, assignedExternalPort, assignedLiftetime, err := client.AddPortMapping(ctx, objects.Gateway, networkProtocol,
|
||||||
ctx, objects.Gateway, networkProtocol, internalPort, externalPort, lifetime)
|
internalPort, p.portForwarded, lifetime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("adding port mapping: %w", err)
|
return fmt.Errorf("adding port mapping: %w", err)
|
||||||
}
|
}
|
||||||
checkLifetime(logger, networkProtocol, lifetime, assignedLiftetime)
|
|
||||||
if externalPort != assignedExternalPort {
|
if assignedLiftetime != lifetime {
|
||||||
return fmt.Errorf("%w: %d changed to %d",
|
logger.Warn(fmt.Sprintf("assigned lifetime %s differs"+
|
||||||
ErrExternalPortChanged, externalPort, assignedExternalPort)
|
" from requested lifetime %s",
|
||||||
} else if internalPort != assignedInternalPort {
|
assignedLiftetime, lifetime))
|
||||||
return fmt.Errorf("%w: %d (for external port %d) changed to %d",
|
}
|
||||||
ErrInternalPortChanged, internalPort, externalPort, assignedInternalPort)
|
|
||||||
}
|
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)
|
timer.Reset(refreshTimeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type Provider struct {
|
|||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
internalToExternalPorts map[uint16]uint16
|
portForwarded uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ import (
|
|||||||
// oddities of Proton's authentication flow they want to keep hidden
|
// oddities of Proton's authentication flow they want to keep hidden
|
||||||
// from the public.
|
// from the public.
|
||||||
type apiClient struct {
|
type apiClient struct {
|
||||||
apiURLBase string
|
apiURLBase string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
appVersion string
|
appVersion string
|
||||||
userAgent string
|
vpnGtkAppVersion string
|
||||||
generator *rand.ChaCha8
|
userAgent string
|
||||||
|
generator *rand.ChaCha8
|
||||||
}
|
}
|
||||||
|
|
||||||
// newAPIClient returns an [apiClient] with sane defaults matching Proton's
|
// 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))]
|
userAgent := userAgents[generator.Uint64()%uint64(len(userAgents))]
|
||||||
|
|
||||||
appVersion, err := getMostRecentStableTag(ctx, httpClient)
|
appVersion, err := getMostRecentStableWebAccountTag(ctx, httpClient)
|
||||||
if err != nil {
|
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{
|
return &apiClient{
|
||||||
apiURLBase: "https://account.proton.me/api",
|
apiURLBase: "https://account.proton.me/api",
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
appVersion: appVersion,
|
appVersion: appVersion,
|
||||||
userAgent: userAgent,
|
vpnGtkAppVersion: vpnGtkAppVersion,
|
||||||
generator: generator,
|
userAgent: userAgent,
|
||||||
|
generator: generator,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +72,10 @@ var ErrCodeNotSuccess = errors.New("response code is not success")
|
|||||||
// to succeed without being blocked by their "security" measures.
|
// to succeed without being blocked by their "security" measures.
|
||||||
// See for example [getMostRecentStableTag] on how the app version must
|
// See for example [getMostRecentStableTag] on how the app version must
|
||||||
// be set to a recent version or they block your request. "SeCuRiTy"...
|
// 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("Cookie", cookie.String())
|
||||||
request.Header.Set("User-Agent", c.userAgent)
|
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-locale", "en_US")
|
||||||
request.Header.Set("x-pm-uid", cookie.uid)
|
request.Header.Set("x-pm-uid", cookie.uid)
|
||||||
}
|
}
|
||||||
@@ -165,7 +171,7 @@ func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
|
|||||||
unauthCookie := cookie{
|
unauthCookie := cookie{
|
||||||
sessionID: sessionID,
|
sessionID: sessionID,
|
||||||
}
|
}
|
||||||
c.setHeaders(request, unauthCookie)
|
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -252,7 +258,7 @@ func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, acces
|
|||||||
uid: uid,
|
uid: uid,
|
||||||
sessionID: sessionID,
|
sessionID: sessionID,
|
||||||
}
|
}
|
||||||
c.setHeaders(request, unauthCookie)
|
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||||
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
@@ -325,7 +331,7 @@ func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie coo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
|
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")
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
@@ -438,7 +444,7 @@ func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return cookie{}, fmt.Errorf("creating request: %w", err)
|
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")
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
@@ -590,7 +596,9 @@ func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
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)
|
response, err := c.httpClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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
|
// 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
|
// 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.
|
// 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
|
page := 1
|
||||||
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
||||||
for ctx.Err() == nil {
|
for ctx.Err() == nil {
|
||||||
@@ -69,3 +72,45 @@ func getMostRecentStableTag(ctx context.Context, client *http.Client) (version s
|
|||||||
|
|
||||||
return "", fmt.Errorf("%w (queried %d pages)", context.Canceled, page)
|
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("mute-replay-warnings") // these are often ignored by some VPN providers
|
||||||
lines.add("auth-retry", "nointeract") // retry authenticating without interaction
|
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("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("dev", settings.Interface)
|
||||||
lines.add("verb", fmt.Sprint(*settings.Verbosity))
|
lines.add("verb", fmt.Sprint(*settings.Verbosity))
|
||||||
protocol := connection.Protocol
|
lines.add("proto", connection.Protocol)
|
||||||
if protocol == constants.TCP {
|
|
||||||
protocol = "tcp-client"
|
|
||||||
}
|
|
||||||
lines.add("proto", protocol)
|
|
||||||
lines.add("remote", connection.IP.String(), fmt.Sprint(connection.Port))
|
lines.add("remote", connection.IP.String(), fmt.Sprint(connection.Port))
|
||||||
|
|
||||||
if *settings.User != "" {
|
if *settings.User != "" {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func boolPtr(b bool) *bool { return &b }
|
||||||
func uint16Ptr(n uint16) *uint16 { return &n }
|
func uint16Ptr(n uint16) *uint16 { return &n }
|
||||||
|
|
||||||
func Test_GetPort(t *testing.T) {
|
func Test_GetPort(t *testing.T) {
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ type PortForwardObjects struct {
|
|||||||
Username string
|
Username string
|
||||||
// Password is used by Private Internet Access for port forwarding.
|
// Password is used by Private Internet Access for port forwarding.
|
||||||
Password string
|
Password string
|
||||||
// PortsCount is used by ProtonVPN for port forwarding.
|
|
||||||
PortsCount uint16
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Routing interface {
|
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.TrimSpace(s)
|
||||||
s = strings.ToLower(s)
|
s = strings.ToLower(s)
|
||||||
switch s {
|
switch s {
|
||||||
case "tcp", "tcp4", "tcp6", "tcp-client":
|
case "tcp", "tcp4", "tcp6":
|
||||||
return true, false, nil
|
return true, false, nil
|
||||||
case "udp", "udp4", "udp6":
|
case "udp", "udp4", "udp6":
|
||||||
return false, true, nil
|
return false, true, nil
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ type Provider interface {
|
|||||||
type PortForwarder interface {
|
type PortForwarder interface {
|
||||||
Name() string
|
Name() string
|
||||||
PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
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)
|
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) (
|
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)
|
return nil, fmt.Errorf("%w: for %s", ErrPortForwardingNotSupported, n.providerName)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user