mirror of
https://github.com/qdm12/gluetun.git
synced 2026-07-30 17:03:22 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35137cfba0 | |||
| 66b9f71ecf | |||
| 704a7fd7ef | |||
| f615e3c780 | |||
| f1a8303db7 | |||
| 628b0a22e2 | |||
| ea3d138bd6 | |||
| c3a6809447 | |||
| 792a5ff5f3 | |||
| 7eef1c89a7 | |||
| 8bc2fbd487 | |||
| a4eb625fbe | |||
| 17a7bf6d54 | |||
| b11de4f0c3 | |||
| e87a92efa0 | |||
| 44977f4d9e | |||
| c473579261 | |||
| d5eeec6fb3 |
@@ -1,2 +1,3 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
# 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.
|
||||||
+2
-1
@@ -164,7 +164,8 @@ 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_PORT=0 \
|
VPN_PORT_FORWARDING_LISTENING_PORTS=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.20260310123525-76fabc2b3294
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
||||||
github.com/qdm12/gosettings v0.4.4
|
github.com/qdm12/gosettings v0.4.4
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c
|
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c
|
||||||
|
|||||||
@@ -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.20260310123525-76fabc2b3294 h1:adkCP7N9mEHpsKSR/5LToF27qJo0yOufhT5zBdKpyrE=
|
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/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type BoringPoll struct {
|
|||||||
|
|
||||||
// Internal signals and channels
|
// Internal signals and channels
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
done <-chan struct{}
|
done *sync.WaitGroup
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@ 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())
|
||||||
@@ -166,7 +167,7 @@ func (b *BoringPoll) Stop() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
b.cancel()
|
b.cancel()
|
||||||
<-b.done
|
b.done.Wait()
|
||||||
b.cancel = nil
|
b.cancel = nil
|
||||||
b.done = nil
|
b.done = nil
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -138,46 +138,6 @@ 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,7 +132,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate EncPreset
|
// Validate EncPreset
|
||||||
if vpnProvider == providers.PrivateInternetAccess {
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
validEncryptionPresets := []string{
|
validEncryptionPresets := []string{
|
||||||
presets.None,
|
|
||||||
presets.Normal,
|
presets.Normal,
|
||||||
presets.Strong,
|
presets.Strong,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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"
|
||||||
@@ -37,16 +39,28 @@ 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"`
|
||||||
// ListeningPort is the port traffic would be redirected to from the
|
// ListeningPorts are the ports traffic would be redirected to from the
|
||||||
// forwarded port. The redirection is disabled if it is set to 0, which
|
// forwarded ports. The redirection is disabled if it is the slice [0],
|
||||||
// is its default as well.
|
// which is its default as well. If set and not [0], its length must match
|
||||||
ListeningPort *uint16 `json:"listening_port"`
|
// the PortsCount value, such that each forwarded port is redirected to
|
||||||
|
// the corresponding listening port.
|
||||||
|
ListeningPorts []uint16 `json:"listening_port"`
|
||||||
|
// PortsCount is the number of ports to forward. It is optional for ProtonVPN
|
||||||
|
// and be between 1 and 5. For other providers, it must be set to 1 if port
|
||||||
|
// forwarding is enabled.
|
||||||
|
PortsCount uint16 `json:"ports_count"`
|
||||||
// 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
|
||||||
@@ -75,13 +89,36 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if providerSelected == providers.PrivateInternetAccess {
|
switch providerSelected {
|
||||||
|
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
|
||||||
@@ -89,14 +126,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),
|
||||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
ListeningPorts: gosettings.CopySlice(p.ListeningPorts),
|
||||||
Username: p.Username,
|
Username: p.Username,
|
||||||
Password: p.Password,
|
Password: p.Password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +143,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.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
p.ListeningPorts = gosettings.OverrideWithSlice(p.ListeningPorts, other.ListeningPorts)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -117,7 +154,8 @@ 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.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
p.ListeningPorts = gosettings.DefaultSlice(p.ListeningPorts, []uint16{0}) // disabled
|
||||||
|
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PortForwarding) String() string {
|
func (p PortForwarding) String() string {
|
||||||
@@ -131,11 +169,14 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
|||||||
|
|
||||||
node = gotree.New("Automatic port forwarding settings:")
|
node = gotree.New("Automatic port forwarding settings:")
|
||||||
|
|
||||||
listeningPort := "disabled"
|
node.Appendf("Number of ports to be forwarded: %d", p.PortsCount)
|
||||||
if *p.ListeningPort != 0 {
|
|
||||||
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
|
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
|
||||||
|
redirNode := node.Appendf("Redirection for listening ports:")
|
||||||
|
for i, port := range p.ListeningPorts {
|
||||||
|
redirNode.Appendf("Port #%d -> %d", i+1, port)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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")
|
||||||
@@ -190,7 +231,13 @@ 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.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
p.ListeningPorts, err = r.CSVUint16("VPN_PORT_FORWARDING_LISTENING_PORTS",
|
||||||
|
reader.RetroKeys("VPN_PORT_FORWARDING_LISTENING_PORT"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.PortsCount, err = r.Uint16("VPN_PORT_FORWARDING_PORTS_COUNT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type Logger interface {
|
|||||||
Info(s string)
|
Info(s string)
|
||||||
Warn(s string)
|
Warn(s string)
|
||||||
Error(s string)
|
Error(s string)
|
||||||
|
Errorf(format string, args ...any)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cmder interface {
|
type Cmder interface {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/portforward/service"
|
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||||
@@ -42,11 +43,12 @@ 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,
|
||||||
ListeningPort: *settings.ListeningPort,
|
ListeningPorts: settings.ListeningPorts,
|
||||||
|
PortsCount: settings.PortsCount,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routing: routing,
|
routing: routing,
|
||||||
@@ -86,6 +88,8 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
defer close(runDone)
|
defer close(runDone)
|
||||||
|
|
||||||
var serviceRunError <-chan error
|
var serviceRunError <-chan error
|
||||||
|
var retryAfter <-chan time.Time
|
||||||
|
const retryDelay = 5 * time.Second
|
||||||
for {
|
for {
|
||||||
updateReceived := false
|
updateReceived := false
|
||||||
select {
|
select {
|
||||||
@@ -104,10 +108,12 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
l.settingsMutex.Unlock()
|
l.settingsMutex.Unlock()
|
||||||
case err := <-serviceRunError:
|
case err := <-serviceRunError:
|
||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
|
case <-retryAfter:
|
||||||
|
// Retry starting the service after a delay
|
||||||
|
retryAfter = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
firstRun := serviceRunError == nil
|
if l.service != nil {
|
||||||
if !firstRun {
|
|
||||||
err := l.service.Stop()
|
err := l.service.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
||||||
@@ -131,6 +137,10 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
err = fmt.Errorf("starting port forwarding service: %w", err)
|
err = fmt.Errorf("starting port forwarding service: %w", err)
|
||||||
}
|
}
|
||||||
updateResult <- err
|
updateResult <- err
|
||||||
|
} else if err != nil {
|
||||||
|
// Log the error and schedule a retry
|
||||||
|
l.logger.Errorf("starting port forwarding service: %s - retrying in %s", err, retryDelay)
|
||||||
|
retryAfter = time.After(retryDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) (
|
||||||
ports []uint16, err error)
|
internalToExternalPorts map[uint16]uint16, err error)
|
||||||
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.onNewPorts(ctx, ports)
|
internalToExternalPorts := make(map[uint16]uint16, len(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,6 +3,7 @@ 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"
|
||||||
@@ -17,7 +18,8 @@ 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
|
||||||
ListeningPort uint16
|
ListeningPorts []uint16
|
||||||
|
PortsCount uint16
|
||||||
Username string // needed for PIA
|
Username string // needed for PIA
|
||||||
Password string // needed for PIA
|
Password string // needed for PIA
|
||||||
}
|
}
|
||||||
@@ -31,7 +33,8 @@ 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.ListeningPort = s.ListeningPort
|
copied.ListeningPorts = gosettings.CopySlice(s.ListeningPorts)
|
||||||
|
copied.PortsCount = s.PortsCount
|
||||||
copied.Username = s.Username
|
copied.Username = s.Username
|
||||||
copied.Password = s.Password
|
copied.Password = s.Password
|
||||||
return copied
|
return copied
|
||||||
@@ -46,7 +49,8 @@ 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.ListeningPort = gosettings.OverrideWithComparable(s.ListeningPort, update.ListeningPort)
|
s.ListeningPorts = gosettings.OverrideWithSlice(s.ListeningPorts, update.ListeningPorts)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -58,6 +62,10 @@ 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) {
|
||||||
@@ -78,7 +86,12 @@ 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.PortForwarder.Name() == providers.PrivateInternetAccess:
|
case s.PortsCount == 0:
|
||||||
|
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)
|
||||||
@@ -87,6 +100,26 @@ 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,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/netlink"
|
"github.com/qdm12/gluetun/internal/netlink"
|
||||||
@@ -42,8 +43,9 @@ 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,
|
||||||
}
|
}
|
||||||
ports, err := s.settings.PortForwarder.PortForward(ctx, obj)
|
internalToExternalPorts, 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)
|
||||||
}
|
}
|
||||||
@@ -51,7 +53,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, ports)
|
err = s.onNewPorts(ctx, internalToExternalPorts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -86,36 +88,60 @@ 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, ports []uint16) (err error) {
|
func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[uint16]uint16) (err error) {
|
||||||
slices.Sort(ports)
|
autoRedirectionNeeded := false
|
||||||
|
externalToInternalPorts := make(map[uint16]uint16, len(internalToExternalPorts))
|
||||||
|
for internal, external := range internalToExternalPorts {
|
||||||
|
externalToInternalPorts[external] = internal
|
||||||
|
if internal != external {
|
||||||
|
autoRedirectionNeeded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.logger.Info(portsToString(ports))
|
externalPorts := slices.Collect(maps.Keys(externalToInternalPorts))
|
||||||
|
slices.Sort(externalPorts)
|
||||||
|
|
||||||
for _, port := range ports {
|
s.logger.Info(portsToString(externalPorts))
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.settings.ListeningPort != 0 {
|
var sourcePort, destinationPort uint16
|
||||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, port, s.settings.ListeningPort)
|
switch {
|
||||||
if err != nil {
|
case userRedirectionEnabled: // precedence over auto redirection
|
||||||
return fmt.Errorf("redirecting port in firewall: %w", err)
|
sourcePort = externalToInternalPorts[port]
|
||||||
}
|
destinationPort = s.settings.ListeningPorts[i]
|
||||||
|
case autoRedirectionNeeded:
|
||||||
|
sourcePort = externalToInternalPorts[port]
|
||||||
|
destinationPort = port
|
||||||
|
default:
|
||||||
|
// No redirection needed, source and destination ports are the same.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, sourcePort, destinationPort)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("redirecting port %d to %d in firewall: %w",
|
||||||
|
sourcePort, destinationPort, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.writePortForwardedFile(ports)
|
err = s.writePortForwardedFile(externalPorts)
|
||||||
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(ports))
|
s.ports = make([]uint16, len(internalToExternalPorts))
|
||||||
copy(s.ports, ports)
|
copy(s.ports, externalPorts)
|
||||||
|
|
||||||
if s.settings.UpCommand != "" {
|
if s.settings.UpCommand != "" {
|
||||||
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports, s.settings.Interface)
|
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, externalPorts, 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,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,13 +39,14 @@ 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 s.settings.ListeningPort != 0 {
|
if redirectionWasEnabled {
|
||||||
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,6 +76,7 @@ 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,6 +62,7 @@ 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,12 +10,17 @@ 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,
|
||||||
) (ports []uint16, err error) {
|
) (internalToExternalPorts map[uint16]uint16, err error) {
|
||||||
if !objects.InternalIP.IsValid() {
|
if !objects.InternalIP.IsValid() {
|
||||||
panic("internal ip is not set")
|
panic("internal ip is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
return internalIPToPorts(objects.InternalIP), nil
|
ports := internalIPToPorts(objects.InternalIP)
|
||||||
|
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.None, presets.Normal:
|
case presets.Normal:
|
||||||
defaults.OpenVPNTCPPort = 502
|
defaults.OpenVPNTCPPort = 502
|
||||||
defaults.OpenVPNUDPPort = 1198
|
defaults.OpenVPNUDPPort = 1198
|
||||||
case presets.Strong:
|
case presets.Strong:
|
||||||
|
|||||||
@@ -24,16 +24,10 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
|||||||
)
|
)
|
||||||
switch *settings.PIAEncPreset {
|
switch *settings.PIAEncPreset {
|
||||||
case presets.Normal:
|
case presets.Normal:
|
||||||
providerSettings.Ciphers = []string{openvpn.AES128cbc}
|
providerSettings.Ciphers = []string{openvpn.AES128gcm}
|
||||||
providerSettings.Auth = openvpn.SHA1
|
|
||||||
providerSettings.CAs = []string{caNormalPreset}
|
|
||||||
case presets.None:
|
|
||||||
providerSettings.Ciphers = []string{"none"}
|
|
||||||
providerSettings.Auth = "none"
|
|
||||||
providerSettings.CAs = []string{caNormalPreset}
|
providerSettings.CAs = []string{caNormalPreset}
|
||||||
default: // strong
|
default: // strong
|
||||||
providerSettings.Ciphers = []string{openvpn.AES256cbc}
|
providerSettings.Ciphers = []string{openvpn.AES256gcm}
|
||||||
providerSettings.Auth = openvpn.SHA256
|
|
||||||
providerSettings.CAs = []string{"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"} //nolint:lll
|
providerSettings.CAs = []string{"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"} //nolint:lll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
) (ports []uint16, err error) {
|
) (internalToExternalPorts map[uint16]uint16, err error) {
|
||||||
switch {
|
switch {
|
||||||
case objects.ServerName == "":
|
case objects.ServerName == "":
|
||||||
panic("server name cannot be empty")
|
panic("server name cannot be empty")
|
||||||
@@ -39,7 +39,6 @@ func (p *Provider) PortForward(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverName := objects.ServerName
|
serverName := objects.ServerName
|
||||||
apiIP := buildAPIIPAddress(objects.Gateway)
|
|
||||||
logger := objects.Logger
|
logger := objects.Logger
|
||||||
|
|
||||||
if !objects.CanPortForward {
|
if !objects.CanPortForward {
|
||||||
@@ -68,9 +67,14 @@ func (p *Provider) PortForward(ctx context.Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.apiIP, err = findAPIIP(ctx, privateIPClient, objects.Gateway)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("finding API IP address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !dataFound || expired {
|
if !dataFound || expired {
|
||||||
client := objects.Client
|
client := objects.Client
|
||||||
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, apiIP,
|
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, p.apiIP,
|
||||||
p.portForwardPath, objects.Username, objects.Password)
|
p.portForwardPath, objects.Username, objects.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("refreshing port forward data: %w", err)
|
return nil, fmt.Errorf("refreshing port forward data: %w", err)
|
||||||
@@ -80,11 +84,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, apiIP, data); err != nil {
|
if err := bindPort(ctx, privateIPClient, p.apiIP, data); err != nil {
|
||||||
return nil, fmt.Errorf("binding port: %w", err)
|
return nil, fmt.Errorf("binding port: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []uint16{data.Port}, nil
|
return map[uint16]uint16{data.Port: data.Port}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrPortForwardedExpired = errors.New("port forwarded data expired")
|
var ErrPortForwardedExpired = errors.New("port forwarded data expired")
|
||||||
@@ -99,8 +103,6 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
panic("gateway is not set")
|
panic("gateway is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
apiIP := buildAPIIPAddress(objects.Gateway)
|
|
||||||
|
|
||||||
privateIPClient, err := newHTTPClient(objects.ServerName)
|
privateIPClient, err := newHTTPClient(objects.ServerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating custom HTTP client: %w", err)
|
return fmt.Errorf("creating custom HTTP client: %w", err)
|
||||||
@@ -128,7 +130,7 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
}
|
}
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-keepAliveTimer.C:
|
case <-keepAliveTimer.C:
|
||||||
err = bindPort(ctx, privateIPClient, apiIP, data)
|
err = bindPort(ctx, privateIPClient, p.apiIP, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("binding port: %w", err)
|
return fmt.Errorf("binding port: %w", err)
|
||||||
}
|
}
|
||||||
@@ -140,15 +142,53 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAPIIPAddress(gateway netip.Addr) (api netip.Addr) {
|
var errAPIIPNotFound = errors.New("API IP address not found")
|
||||||
|
|
||||||
|
func findAPIIP(ctx context.Context, client *http.Client, gateway netip.Addr) (
|
||||||
|
apiIP netip.Addr, err error,
|
||||||
|
) {
|
||||||
if gateway.Is6() {
|
if gateway.Is6() {
|
||||||
panic("IPv6 gateway not supported")
|
panic("IPv6 gateway not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
gatewayBytes := gateway.As4()
|
gatewayBytes := gateway.As4()
|
||||||
gatewayBytes[2] = 128
|
gatewayBytes[3] = 1 // x.y.z.1
|
||||||
gatewayBytes[3] = 1
|
|
||||||
return netip.AddrFrom4(gatewayBytes)
|
gatewayBytes[2] = 128 // x.y.128.1
|
||||||
|
oldAPIIP := netip.AddrFrom4(gatewayBytes)
|
||||||
|
gatewayBytes[2] = 0 // x.y.0.1 - new API IP reported by some users
|
||||||
|
newAPIIP := netip.AddrFrom4(gatewayBytes)
|
||||||
|
possibleIPs := []netip.Addr{oldAPIIP, newAPIIP}
|
||||||
|
|
||||||
|
errs := make([]error, 0, len(possibleIPs))
|
||||||
|
for _, ip := range possibleIPs {
|
||||||
|
const timeout = 5 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
url := url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: net.JoinHostPort(ip.String(), "19999"),
|
||||||
|
Path: "/ping",
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("trying IP %s: %w", ip, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("trying IP %s: %w", ip, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = response.Body.Close()
|
||||||
|
return ip, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return netip.Addr{}, fmt.Errorf("%w: %w", errAPIIPNotFound, errors.Join(errs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client,
|
func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package presets
|
package presets
|
||||||
|
|
||||||
const (
|
const (
|
||||||
None = "none"
|
|
||||||
Normal = "normal"
|
Normal = "normal"
|
||||||
Strong = "strong"
|
Strong = "strong"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package privateinternetaccess
|
|||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
@@ -17,6 +18,7 @@ type Provider struct {
|
|||||||
common.Fetcher
|
common.Fetcher
|
||||||
// Port forwarding
|
// Port forwarding
|
||||||
portForwardPath string
|
portForwardPath string
|
||||||
|
apiIP netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type serverData struct {
|
|||||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
func fetchAPI(ctx context.Context, client *http.Client) (
|
||||||
data apiData, err error,
|
data apiData, err error,
|
||||||
) {
|
) {
|
||||||
const url = "https://serverlist.piaservers.net/vpninfo/servers/v6"
|
const url = "https://serverlist.piaservers.net/vpninfo/servers/v7"
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
|||||||
|
|
||||||
func addData(regions []regionData, nts nameToServer) (change bool) {
|
func addData(regions []regionData, nts nameToServer) (change bool) {
|
||||||
for _, region := range regions {
|
for _, region := range regions {
|
||||||
|
if region.Offline {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for _, server := range region.Servers.UDP {
|
for _, server := range region.Servers.UDP {
|
||||||
const tcp, udp = false, true
|
const tcp, udp = false, true
|
||||||
if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) {
|
if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) {
|
||||||
|
|||||||
@@ -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) (
|
||||||
ports []uint16, err error,
|
internalToExternalPorts map[uint16]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,7 +75,8 @@ 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)
|
||||||
}
|
}
|
||||||
return []uint16{uint16(portUint64)}, nil
|
port := uint16(portUint64)
|
||||||
|
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
|
||||||
ports []uint16
|
internalToExternalPorts map[uint16]uint16
|
||||||
errMessage string
|
errMessage string
|
||||||
}{
|
}{
|
||||||
"canceled context": {
|
"canceled context": {
|
||||||
ctx: canceledCtx,
|
ctx: canceledCtx,
|
||||||
@@ -192,7 +192,7 @@ func Test_Provider_PortForward(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ports: []uint16{61527},
|
internalToExternalPorts: map[uint16]uint16{61527: 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.ports, ports)
|
assert.Equal(t, testCase.internalToExternalPorts, ports)
|
||||||
if testCase.errMessage != "" {
|
if testCase.errMessage != "" {
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,17 +14,18 @@ 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) (
|
||||||
ports []uint16, err error,
|
internalToExternalPorts map[uint16]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,
|
_, externalIPv4Address, err := client.ExternalAddress(ctx, objects.Gateway)
|
||||||
objects.Gateway)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(err.Error(), "connection refused"):
|
case strings.HasSuffix(err.Error(), "connection refused"):
|
||||||
@@ -38,29 +40,37 @@ func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObj
|
|||||||
|
|
||||||
logger := objects.Logger
|
logger := objects.Logger
|
||||||
|
|
||||||
logger.Info("gateway external IPv4 address is " + externalIPv4Address.String())
|
logger.Debug("gateway external IPv4 address is " + externalIPv4Address.String())
|
||||||
const internalPort, externalPort = 0, 1
|
const externalPort = 0
|
||||||
const lifetime = 60 * time.Second
|
const lifetime = 60 * time.Second
|
||||||
|
|
||||||
_, _, assignedUDPExternalPort, assignedLifetime, err := client.AddPortMapping(ctx, objects.Gateway, "udp",
|
p.internalToExternalPorts = make(map[uint16]uint16, objects.PortsCount)
|
||||||
internalPort, externalPort, lifetime)
|
for i := range objects.PortsCount {
|
||||||
if err != nil {
|
internalPort := nonSymmetricPortStart + i
|
||||||
return nil, fmt.Errorf("adding UDP port mapping: %w", err)
|
protoToInternalPort := map[string]uint16{
|
||||||
|
"udp": 0,
|
||||||
|
"tcp": 0,
|
||||||
|
}
|
||||||
|
protoToExternalPort := maps.Clone(protoToInternalPort)
|
||||||
|
for protocol := range protoToExternalPort {
|
||||||
|
_, assignedInternalPort, assignedExternalPort, assignedLifetime, err := client.AddPortMapping(
|
||||||
|
ctx, objects.Gateway, protocol, internalPort, externalPort, lifetime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("adding %d/%d %s port mapping: %w",
|
||||||
|
i+1, objects.PortsCount, strings.ToUpper(protocol), err)
|
||||||
|
}
|
||||||
|
checkLifetime(logger, strings.ToUpper(protocol), lifetime, assignedLifetime)
|
||||||
|
checkInternalPort(logger, internalPort, assignedInternalPort)
|
||||||
|
protoToInternalPort[protocol] = assignedInternalPort
|
||||||
|
protoToExternalPort[protocol] = assignedExternalPort
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInternalPorts(logger, protoToInternalPort["udp"], protoToInternalPort["tcp"])
|
||||||
|
checkExternalPorts(logger, protoToExternalPort["udp"], protoToExternalPort["tcp"])
|
||||||
|
p.internalToExternalPorts[protoToInternalPort["tcp"]] = protoToExternalPort["tcp"]
|
||||||
}
|
}
|
||||||
checkLifetime(logger, "UDP", lifetime, assignedLifetime)
|
|
||||||
|
|
||||||
_, _, assignedTCPExternalPort, assignedLifetime, err := client.AddPortMapping(ctx, objects.Gateway, "tcp",
|
return maps.Clone(p.internalToExternalPorts), nil
|
||||||
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,
|
||||||
@@ -73,6 +83,20 @@ 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",
|
||||||
@@ -80,7 +104,10 @@ func checkExternalPorts(logger utils.Logger, udpPort, tcpPort uint16) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrExternalPortChanged = errors.New("external port changed")
|
var (
|
||||||
|
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,
|
||||||
@@ -96,32 +123,28 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.Logger.Debug("refreshing port forward since 45 seconds have elapsed")
|
objects.Logger.Debug("refreshing forwarded ports 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 {
|
||||||
_, _, assignedExternalPort, assignedLiftetime, err := client.AddPortMapping(ctx, objects.Gateway, networkProtocol,
|
_, assignedInternalPort, assignedExternalPort, assignedLiftetime, err := client.AddPortMapping(
|
||||||
internalPort, p.portForwarded, lifetime)
|
ctx, objects.Gateway, networkProtocol, internalPort, externalPort, 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 assignedLiftetime != lifetime {
|
if externalPort != assignedExternalPort {
|
||||||
logger.Warn(fmt.Sprintf("assigned lifetime %s differs"+
|
return fmt.Errorf("%w: %d changed to %d",
|
||||||
" from requested lifetime %s",
|
ErrExternalPortChanged, externalPort, assignedExternalPort)
|
||||||
assignedLiftetime, lifetime))
|
} else if internalPort != assignedInternalPort {
|
||||||
}
|
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
|
||||||
portForwarded uint16
|
internalToExternalPorts map[uint16]uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
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,9 +62,14 @@ 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))
|
||||||
lines.add("proto", connection.Protocol)
|
protocol := connection.Protocol
|
||||||
|
if protocol == constants.TCP {
|
||||||
|
protocol = "tcp-client"
|
||||||
|
}
|
||||||
|
lines.add("proto", protocol)
|
||||||
lines.add("remote", connection.IP.String(), fmt.Sprint(connection.Port))
|
lines.add("remote", connection.IP.String(), fmt.Sprint(connection.Port))
|
||||||
|
|
||||||
if *settings.User != "" {
|
if *settings.User != "" {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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,6 +25,8 @@ 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 {
|
||||||
|
|||||||
+157272
-48306
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":
|
case "tcp", "tcp4", "tcp6", "tcp-client":
|
||||||
return true, false, nil
|
return true, false, nil
|
||||||
case "udp", "udp4", "udp6":
|
case "udp", "udp4", "udp6":
|
||||||
return false, true, nil
|
return false, true, nil
|
||||||
|
|||||||
@@ -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) (
|
||||||
ports []uint16, err error)
|
internalToExternalPorts map[uint16]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) (
|
||||||
ports []uint16, err error,
|
internalToExternalPorts map[uint16]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