Compare commits

..

6 Commits

Author SHA1 Message Date
Quentin McGaw 7f22fb3276 fix(protonvpn): support port 51820 for UDP OpenVPN 2026-02-11 14:13:34 +00:00
Quentin McGaw 6909a0c123 fix(healthcheck): prevent race condition and fix #3096 (#3123) 2026-02-11 14:12:20 +00:00
Quentin McGaw 3e1f48932a fix(openvpn): only log openvpn version corresponding to OPENVPN_VERSION 2026-02-11 14:12:08 +00:00
Chris Duck 50744852c5 fix(protonvpn): update OpenVPN settings (#3120) 2026-02-11 14:11:57 +00:00
Quentin McGaw 09e52bc685 fix(httpproxy): remove info log when no Proxy-Authorization header is present 2026-02-11 14:11:46 +00:00
Quentin McGaw 857fe425ec fix(wireguard): fix detection of kernelspace wireguard 2026-02-11 14:11:36 +00:00
308 changed files with 51549 additions and 164977 deletions
+1 -2
View File
@@ -1,3 +1,2 @@
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
RUN apk add wireguard-tools htop openssl tcpdump iptables RUN apk add wireguard-tools htop openssl
RUN apk add nodejs npm && npm install markdownlint-cli2 --global && apk del npm
-4
View File
@@ -23,8 +23,6 @@
color: "959a9c" color: "959a9c"
- name: "Closed: ☠️ cannot be done" - name: "Closed: ☠️ cannot be done"
color: "959a9c" color: "959a9c"
- name: "Closed: 🤖🍺 drunk AI"
color: "959a9c"
- name: "Priority: 🚨 Urgent" - name: "Priority: 🚨 Urgent"
color: "03adfc" color: "03adfc"
@@ -128,8 +126,6 @@
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Firewall ⛓️" - name: "Category: Firewall ⛓️"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: MTU discovery 🔦"
color: "ffc7ea"
- name: "Category: Routing 🛤️" - name: "Category: Routing 🛤️"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: IPv6 🛰️" - name: "Category: IPv6 🛰️"
+8 -16
View File
@@ -45,7 +45,6 @@ jobs:
level: error level: error
exclude: | exclude: |
./internal/storage/servers.json ./internal/storage/servers.json
./.golangci.yml
*.md *.md
- name: Linting - name: Linting
@@ -60,13 +59,10 @@ jobs:
- name: Run tests in test container - name: Run tests in test container
run: | run: |
touch coverage.txt touch coverage.txt
docker run --rm --cap-add=NET_ADMIN --device /dev/net/tun \ docker run --rm --device /dev/net/tun \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \ -v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container test-container
- name: Verify dev cross platform compatibility
run: docker build --target xcompile .
- name: Build final image - name: Build final image
run: docker build -t final-image . run: docker build -t final-image .
@@ -138,7 +134,7 @@ jobs:
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
flavor: | flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
@@ -153,15 +149,15 @@ jobs:
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- uses: docker/setup-qemu-action@v4 - uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v4 - uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v4 - uses: docker/login-action@v3
with: with:
username: qmcgaw username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
- uses: docker/login-action@v4 - uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: qdm12 username: qdm12
@@ -169,14 +165,10 @@ jobs:
- name: Short commit - name: Short commit
id: shortcommit id: shortcommit
run: | run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
# Use the PR head SHA if it exists, otherwise fallback to GITHUB_SHA
FULL_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
SHORT_SHA=
echo "value=$(echo $FULL_SHA | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Build and push final image - name: Build and push final image
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+3 -7
View File
@@ -14,12 +14,8 @@ jobs:
token: ${{ github.token }} token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}
body: | body: |
Closed issues are **NOT** monitored, so commenting here will likely NOT be seen. Closed issues are **NOT** monitored, so commenting here is likely to be not seen.
If you think this is *still unresolved* and have **more information** to bring, please either If you think this is *still unresolved* and have **more information** to bring, please create another issue.
re-open this issue or create another issue.
❤️😠 temporarily help the Gluetun community and fight the AI slop scam website `gluetun[dot]com` by setting `BORINGPOLL_GLUETUNCOM=on` on the latest image.
See [the option in the wiki for more information](https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/others.md)
This is an automated comment setup because @qdm12 is the sole maintainer of this project This is an automated comment setup because @qdm12 is the sole maintainer of this project
which became too popular to monitor closed issues. which became too popular to monitor issues closed.
+1 -1
View File
@@ -12,6 +12,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: crazy-max/ghaction-github-labeler@v6 - uses: crazy-max/ghaction-github-labeler@v5
with: with:
yaml-file: .github/labels.yml yaml-file: .github/labels.yml
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: DavidAnson/markdownlint-cli2-action@v22 - uses: DavidAnson/markdownlint-cli2-action@v21
with: with:
globs: "**.md" globs: "**.md"
config: .markdownlint-cli2.jsonc config: .markdownlint-cli2.jsonc
@@ -37,7 +37,7 @@ jobs:
use-quiet-mode: yes use-quiet-mode: yes
config-file: .github/workflows/configs/mlc-config.json config-file: .github/workflows/configs/mlc-config.json
- uses: peter-evans/dockerhub-description@v5 - uses: peter-evans/dockerhub-description@v4
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push' if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
with: with:
username: qmcgaw username: qmcgaw
-98
View File
@@ -1,98 +0,0 @@
name: Update servers list
on:
workflow_dispatch:
inputs:
provider:
description: "VPN Provider to update"
required: true
default: "all"
type: choice
options:
- all
- airvpn
- cyberghost
- expressvpn
- fastestvpn
- giganews
- hidemyass
- ipvanish
- ivpn
- mullvad
- nordvpn
- perfect privacy
- privado
- private internet access
- privatevpn
- protonvpn
- purevpn
- slickvpn
- surfshark
- torguard
- vpnsecure
- vpn unlimited
- vyprvpn
- windscribe
schedule:
- cron: "11 3 1 */2 *" # Run at 03:11 on the 1st of every 2nd month
jobs:
update-servers-list:
if: github.repository == 'qdm12/gluetun'
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Update servers list
run: |
SELECTED_PROVIDER="${{ github.event.inputs.provider || 'all' }}"
if [ "$SELECTED_PROVIDER" = "all" ]; then
FLAGS="-all"
else
FLAGS="-providers $SELECTED_PROVIDER"
fi
go run ./cmd/gluetun/main.go update $FLAGS \
-maintainer \
-proton-email "${{ secrets.PROTON_EMAIL }}" \
-proton-password "${{ secrets.PROTON_PASSWORD }}"
- name: Check for changes
run: |
if git diff --exit-code internal/storage/servers.json >/dev/null; then
echo "Error: internal/storage/servers.json was not modified."
exit 1
fi
- name: Check no other file changes
run: |
if ! git diff --exit-code --quiet ':!internal/storage/servers.json'; then
echo "Error: Unexpected changes detected in files other than servers.json"
git status --short
exit 1
fi
- name: Create Pull Request
id: createpr
uses: peter-evans/create-pull-request@v8
with:
branch-suffix: timestamp
branch: bot/update-servers-list
base: master
delete-branch: true
title: "feat(providers/${{ github.event.inputs.provider || 'all' }}): servers data update"
body: |
This PR was automatically generated by the [Update servers list](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow run.
# - name: Merge Pull Request
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# run: |
# gh pr merge ${{ steps.createpr.outputs.pull-request-number }} --auto -m -d
-1
View File
@@ -1,2 +1 @@
scratch.txt scratch.txt
.DS_Store
+1 -6
View File
@@ -22,7 +22,6 @@ linters:
- "^disabled$" - "^disabled$"
# Firewall and routing strings # Firewall and routing strings
- "^(ACCEPT|DROP)$" - "^(ACCEPT|DROP)$"
- "^--append$"
- "^--delete$" - "^--delete$"
- "^all$" - "^all$"
- "^(tcp|udp)$" - "^(tcp|udp)$"
@@ -48,7 +47,7 @@ linters:
path: internal\/server\/.+\.go path: internal\/server\/.+\.go
- linters: - linters:
- ireturn - ireturn
text: returns interface \(golang\.org\/x\/sys\/unix\.Sockaddr\) text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
- linters: - linters:
- ireturn - ireturn
path: internal\/openvpn\/pkcs8\/descbc\.go path: internal\/openvpn\/pkcs8\/descbc\.go
@@ -60,10 +59,6 @@ linters:
- linters: - linters:
- lll - lll
source: "^// https://.+$" source: "^// https://.+$"
- linters:
- mnd
source: "^ cleanups\\.Add.+$"
path: internal\/(wireguard|amneziawg)\/run\.go
- linters: - linters:
- err113 - err113
- mnd - mnd
-195
View File
@@ -1,195 +0,0 @@
# AGENTS
Guidance for coding agents working in this repository.
## Scope and priorities
- Keep changes minimal and targeted. Feel free to do light refactors that are relevant to the modifications.
- Breaking changes:
- Do not introduce breaking usage behavior (cli flags, environment variables, etc.) unless explicitly agreed.
- Do not introduce breaking changes for the Go API in the `pkg/` directory.
- If a compatibility break seems beneficial, stop and ask for confirmation before implementing it.
- Update or add tests when behavior changes.
## Go coding conventions
### General guidelines
- Use explicit, descriptive variable names by default.
- Notable bad examples: `req`, `resp`, `cfg`, `v`
- Allowed short-name exceptions:
- indexes such as `i`, `j`
- `ctx` for `context.Context`
- `t` for `*testing.T` and `b` for `*testing.B`
- `ctrl` for `*gomock.Controller`
- `err` for `error`, `errs` for `[]error`
- `wg` for `*sync.WaitGroup`
- Avoid using global variables except for:
- exported sentinel errors that are used outside the package boundaries
- regular expressions defined with `regexp.MustCompile`
- variables set by the build pipeline, such as `Version` and `BuildDate`
- Constants
- Prefer defining them inline in a function if it's only used in that function, rather than at the package level.
- Each one should be defined right above where it's used, instead of having multiple defined at the same place in a `const ()` block
- If one is only used in a single production code function, define it right above it so it's more local for readability.
- Do not define constants when constants exist in other packages, for example `http.StatusBadRequest` or `log.LevelDebug`.
- Structs
- Prefer defining them inline in a function if it's only used in that function, rather than at the package level.
- Do not use the short if form, prefer the longer one
- Follow modern Go, according to the Go version defined in go.mod. Prefer modern constructs when equivalent:
- Example: use `for i := range 5` rather than `for i := 0; i < 5; i++`.
- Example: use `new("string")` rather than helper wrappers such as `stringPtr("string")`.
- Example: no need to pin variables in for loops when using them in goroutines or subtests.
- Use `New(...) *Item` constructor per package. Each package should ideally only have one constructor, although this is not a strict rule. The constructor should return a pointer to the struct, and not an interface.
- Always prefer using context-aware functions, for example:
- `exec.CommandContext` rather than `exec.Command`
- `http.NewRequestWithContext` rather than `http.NewRequest`
- Never export a symbol unless absolutely necessary.
- Always use the most restrictive builtin types. For example prefer `uint` over `int` if it's only zero or positive. Prefer `uint16` is the max value is 65535.
- Prefer using builtin types whenever possible AND do not define single field structs unless necessary
- Prefer splitting a code line only when it triggers the `lll` linter, do not split a command or arguments list for each element
- Use `netip` types instead of `net` types whenever possible
- Use constants instead of variables whenever possible, especially function-local inline constants.
- Do not use `time.Sleep`, prefer using a `time.Timer` with a `select` statement also listening on a context cancelation
- `panic`:
- should only be used when a programming error is encountered and you should NOT return errors for programming errors (such as passing nil objects)
- Its counterpart `recover` should not really be used, except for testing a panic in test code (or use `assert.PanicsWithValue`).
### Directory structure and file naming
- Executable main packages with a single `main()` function must be in the `cmd` directory.
Prefer having top level logic and have a longer `main()` function rather than having an `internal/app` package.
- Code lives by default in subpackages within the `internal` directory
- Code needing to be imported by external Go modules must be in subpackages within the `pkg` directory
- Example code especially using the `pkg` directory must be in `main` packages within the `examples` directory, each with a single `main.go` function.
- If AND only if the repository is a Go library and not a Go application, you may have Go files at the root of the project to simplify import paths. Most of the code should still be in subpackages in the `internal` directory.
- Interfaces should be defined in `interfaces.go` files for each package. If there are unexported interfaces which need to be mocked, which is rare, they should be defined in `interfaces_local.go` files.
- Mock files are
- `mocks_generate_test.go` which only contains `//go:generate` directives for generating mocks, and no actual code
- `mocks_test.go` which contains the generated mocks from exported interfaces and no other code, and is ignored in coverage reports
- `mocks_local_test.go` (rare) which contains the generated mocks from unexported interfaces and no other code, and is ignored in coverage reports
- NEVER generate an exported mock in a non test file, prefer re-generating files across packages.
- Package naming
- Your package name should be the same as the directory containing it, **except for the `main` package**
- Use single words for package names
- Do not use generic names for package names such as `utils` or `helpers`
- Package nesting
- Try to avoid nesting packages by default
- You can nest packages if you have different implementations for the same interface (e.g. a store interface)
- You can nest packages if you start having a lot of Go files (more than 10) and it really does make sense to make subpackages
### Linting
The linter is `golangci-lint` with the configuration defined in `.golangci.yml`.
To exclude code from linting, prefer using, when absolutely necessary, command comments `//nolint:<linter>`.
This allows the `nolintlint` linter to detect and report unnecessary `//nolint` comments later.
You can notably use `//nolint:lll` and, for good valid reasons, `//nolint:gosec`. Sometimes `//nolint:mnd` when it just doesn't make sense to extract a constant such as `n = n << 4`
Always prefer placing `//nolint` comments on the same code line where the error comes from, and not above a code block.
### Mocking
Mocking works with the `go.uber.org/mock` library, and the `mockgen` tool.
- Mocks from exported interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_test.go` files, using:
```go
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . InterfaceA,InterfaceB
```
- Mocks from unexported interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_local_test.go` files. The source file for unexported interfaces is `interfaces_local.go`. The go generate command is similar to:
```go
//go:generate mockgen -destination=mocks_local_test.go -package $GOPACKAGE -source interfaces_local.go
```
- Mocks from external interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_<package-name>_test.go` files, using:
```go
//go:generate mockgen -destination=mocks_<package-name>_test.go -package $GOPACKAGE module-name InterfaceA,InterfaceB
```
- Generated mocks usage in tests:
- Define mocks in the subtest, not in the parent test. You can also have a function returning the mocks as a field of the test case struct, which takes in the subtest `*testing.T` as argument, and call it in the subtest to get the mocks.
- **Never** use `gomock.Any()` as argument. Always use concrete, precise arguments. You might need to define a custom GoMock matcher for your argument in some very niche and corner cases.
- **Never** use `.AnyTimes()` on mocks. Always define the number of times a certain mock call should be called, with `.Times(3)` for example.
- **Always** set the `.Return(...)` on the mock if the function returns something.
- Avoid using **mock helpers** functions, prefer a bit of repetition than tight coupling and dependency
### main.go
- Make the program OS signal aware, so it attempts a graceful shutdown when interruped. Force quit the program on a second interrupt signal.
### Formatting
The Go formatter used is gofumpt.
### Errors
- Always prefer wrapping errors with some context with `fmt.Errorf("doing this: %w", err)`
- In rare cases, you can just use `return err` notably:
- If the function is called **recursively**, since we don't wrap the wrapping multiple times for each recursion
- If the current function only statement is the call to another function, for example:
```go
func (s *Struct) Fetch() error {
return fetch() // do not wrap the error
}
```
- When wrapping errors, use verbs ending in "ing" and no "failed to" or "cannot" to avoid redundancy. For example, use `fmt.Errorf("resolving host: %w", err)` rather than `fmt.Errorf("failed to resolve host: %w", err)`.
- When wrapping an error, the context should NEVER contain variables injected as arguments in the function returning an error, to avoid repeating the same variable in multiple error messages.
- Testing errors:
- If the error does not wrap a sentinel error, use `assert.ErrorContains` to check for error messages, rather than `assert.EqualError`, to avoid having to update tests for minor changes in error messages. And use `assert.NoError` to check for no error.
- If the error wraps a sentinel error, use `assert.ErrorIs` to check both for the sentinel error or an expected nil error. You can also check the error message with `assert.ErrorContains`
### User program settings
- For configuration structs, each field Go zero value (i.e. `0` for `int`, `nil` for `*string`) should be an INVALID value in the user sense. This is used to detect when a field is not set, in order to default it, merge it or override it. For example if `""` is not a valid value, the field should be of type `string`. Conversely, if `""` is a valid value, the field should be of type `*string` to distinguish between "not set" and "set to empty string". Notably, boolean fields are ALWAYS of type `*bool` for this reason, since both `true` and `false` are valid values.
- Configuration reading and handling relies on the Go library github.com/qdm12/gosettings please use it whenever appropriate.
- Do not wrap errors coming from `reader.Reader` methods, since they already contain the necessary context.
- All keys passed to `reader.Reader` methods must be in environment variable format, i.e. uppercase with underscores. These get converted to lowercase and dashes for flags notably.
- For each settings structs, define the following methods, which are usually unexported, but can be exported especially for the top level Settings struct, in this order:
- `func (s *Settings) setDefaults()` whichs sets defaults (using `gosettings.Default*` functions) on unset fields
- If the settings need to be patched at runtime, which is rarely the case, define `func (s *Settings) overrideWith(other Settings)` which overrides the settings with another settings struct, only for fields that are set in the other struct (using `gosettings.OverrideWith` functions).
- `func (s Settings) validate() error` which validates the settings, and returns an error if anything is invalid
- `func (s *Settings) read(r *reader.Reader) error` which reads the settings from a gosettings/reader.Reader (which can be from multiple sources, such as environment variables, cli flags, config files etc.)
- `func (s Settings) String() string` which uses `toLinesNode().String()` to return a string representation of the settings
- `func (s Settings) toLinesNode() *gotree.Node` which a github.com/qdm12/gotree `*Node` representing the settings
### Testing
- Use the github.com/stretchr/testify library for assertions
- Most tests should be table tests with parallel subtests
- Prefer map-based table tests of the form `map[string]struct{ ... }`, with the key as the test name.
Use underscores in test names, not spaces, to keep `go test` output searchable.
- Use `testCases` for the table variable name, and `testCase` for each iterated case value.
- Run all tests in parallel:
- call `t.Parallel()` in the top-level test
- call `t.Parallel()` in each subtest
### Libraries to use
- Logging: `github.com/qdm12/log`
- Splash information at program start: `github.com/qdm12/gosplash`
- Long running services (i.e. health server, http prod server, backup loop etc.): `github.com/qdm12/goservices`
- String tree structures: `github.com/qdm12/gotree`
### Extra rules
- Do not use `http.DefaultClient`, use a custom `*http.Client` with a fixed timeout and share with dependency injections.
- Do not check for injected dependencies being `nil`, prefer to just panic on a nil pointer. By default it's fine to panic if a developer injects a dependency `nil`. `nil` does not mean use a default.
## Validation checklist
Run the following before finishing changes:
1. Go building `go build ./...`
1. Go linting `golangci-lint run`
1. Go unit tests `go test ./...`
1. If a module is added or modified, run `go mod tidy`
1. If an interface or mock command is modified, run `go generate -run mockgen ./...`
If a Markdown file is modified and `markdownlint-cli2` is available, run `markdownlint-cli2 "**/*.md"`
If a command is unavailable in the current environment, report it clearly and provide the exact command needed once available.
+15 -73
View File
@@ -1,5 +1,5 @@
ARG ALPINE_VERSION=3.23 ARG ALPINE_VERSION=3.22
ARG GO_ALPINE_VERSION=3.23 ARG GO_ALPINE_VERSION=3.22
ARG GO_VERSION=1.25 ARG GO_VERSION=1.25
ARG XCPUTRANSLATE_VERSION=v0.9.0 ARG XCPUTRANSLATE_VERSION=v0.9.0
ARG GOLANGCI_LINT_VERSION=v2.4.0 ARG GOLANGCI_LINT_VERSION=v2.4.0
@@ -13,7 +13,7 @@ FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION}
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
# Note: findutils needed to have xargs support `-d` flag for mocks stage. # Note: findutils needed to have xargs support `-d` flag for mocks stage.
RUN apk --update add git g++ findutils iptables RUN apk --update add git g++ findutils
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
COPY --from=golangci-lint /bin /go/bin/golangci-lint COPY --from=golangci-lint /bin /go/bin/golangci-lint
COPY --from=mockgen /bin /go/bin/mockgen COPY --from=mockgen /bin /go/bin/mockgen
@@ -46,10 +46,6 @@ RUN git init && \
git diff --exit-code && \ git diff --exit-code && \
rm -rf .git/ rm -rf .git/
FROM --platform=${BUILDPLATFORM} base AS xcompile
RUN GOOS=darwin go build -o /dev/null ./...
RUN GOOS=windows go build -o /dev/null ./...
FROM --platform=${BUILDPLATFORM} base AS build FROM --platform=${BUILDPLATFORM} base AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG VERSION=unknown ARG VERSION=unknown
@@ -110,66 +106,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \ WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
WIREGUARD_ADDRESSES= \ WIREGUARD_ADDRESSES= \
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \ WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
WIREGUARD_MTU= \ WIREGUARD_MTU=1320 \
WIREGUARD_IMPLEMENTATION=auto \ WIREGUARD_IMPLEMENTATION=auto \
# Amnezia
AMNEZIAWG_ENDPOINT_IP= \
AMNEZIAWG_ENDPOINT_PORT= \
AMNEZIAWG_CONF_SECRETFILE=/run/secrets/wg0.conf \
AMNEZIAWG_PRIVATE_KEY= \
AMNEZIAWG_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
AMNEZIAWG_PRESHARED_KEY= \
AMNEZIAWG_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
AMNEZIAWG_PUBLIC_KEY= \
AMNEZIAWG_ALLOWED_IPS= \
AMNEZIAWG_PERSISTENT_KEEPALIVE_INTERVAL=0 \
AMNEZIAWG_ADDRESSES= \
AMNEZIAWG_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
AMNEZIAWG_MTU= \
AMNEZIAWG_JC=0 \
AMNEZIAWG_JMIN=0 \
AMNEZIAWG_JMAX=0 \
AMNEZIAWG_S1=0 \
AMNEZIAWG_S2=0 \
AMNEZIAWG_S3=0 \
AMNEZIAWG_S4=0 \
AMNEZIAWG_H1= \
AMNEZIAWG_H2= \
AMNEZIAWG_H3= \
AMNEZIAWG_H4= \
AMNEZIAWG_I1= \
AMNEZIAWG_I2= \
AMNEZIAWG_I3= \
AMNEZIAWG_I4= \
AMNEZIAWG_I5= \
# Wireguard AmneziaWG userspace obfuscation (requires WIREGUARD_IMPLEMENTATION=amneziawg)
AMNEZIAWG_JC=0 \
AMNEZIAWG_JMIN=0 \
AMNEZIAWG_JMAX=0 \
AMNEZIAWG_S1=0 \
AMNEZIAWG_S2=0 \
AMNEZIAWG_S3=0 \
AMNEZIAWG_S4=0 \
AMNEZIAWG_H1= \
AMNEZIAWG_H2= \
AMNEZIAWG_H3= \
AMNEZIAWG_H4= \
AMNEZIAWG_I1= \
AMNEZIAWG_I2= \
AMNEZIAWG_I3= \
AMNEZIAWG_I4= \
AMNEZIAWG_I5= \
# VPN server port forwarding
VPN_PORT_FORWARDING=off \
VPN_PORT_FORWARDING_PROVIDER= \
VPN_PORT_FORWARDING_UP_COMMAND= \
VPN_PORT_FORWARDING_DOWN_COMMAND= \
VPN_PORT_FORWARDING_LISTENING_PORTS=0 \
VPN_PORT_FORWARDING_PORTS_COUNT=1 \
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
# PMTUD
PMTUD_ICMP_ADDRESSES=1.1.1.1,8.8.8.8 \
PMTUD_TCP_ADDRESSES=1.1.1.1:443,8.8.8.8:443,1.1.1.1:53,8.8.8.8:53,[2606:4700:4700::1111]:53,[2001:4860:4860::8888]:53,[2606:4700:4700::1111]:443,[2001:4860:4860::8888]:443 \
# VPN server filtering # VPN server filtering
SERVER_REGIONS= \ SERVER_REGIONS= \
SERVER_COUNTRIES= \ SERVER_COUNTRIES= \
@@ -181,8 +119,14 @@ ENV VPN_SERVICE_PROVIDER=pia \
OWNED_ONLY=no \ OWNED_ONLY=no \
# # Private Internet Access only: # # Private Internet Access only:
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \ PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
VPN_PORT_FORWARDING=off \
VPN_PORT_FORWARDING_LISTENING_PORT=0 \
VPN_PORT_FORWARDING_PROVIDER= \
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
VPN_PORT_FORWARDING_USERNAME= \ VPN_PORT_FORWARDING_USERNAME= \
VPN_PORT_FORWARDING_PASSWORD= \ VPN_PORT_FORWARDING_PASSWORD= \
VPN_PORT_FORWARDING_UP_COMMAND= \
VPN_PORT_FORWARDING_DOWN_COMMAND= \
# # Cyberghost only: # # Cyberghost only:
OPENVPN_CERT= \ OPENVPN_CERT= \
OPENVPN_KEY= \ OPENVPN_KEY= \
@@ -214,9 +158,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
FIREWALL_VPN_INPUT_PORTS= \ FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \ FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \ FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_IPTABLES_LOG_LEVEL=info \ FIREWALL_DEBUG=off \
# IPv6
IPV6_CHECK_ADDRESSES=[2001:4860:4860::8888]:53,[2606:4700:4700::1111]:53 \
# Logging # Logging
LOG_LEVEL=info \ LOG_LEVEL=info \
# Health # Health
@@ -226,9 +168,9 @@ ENV VPN_SERVICE_PROVIDER=pia \
HEALTH_SMALL_CHECK_TYPE=icmp \ HEALTH_SMALL_CHECK_TYPE=icmp \
HEALTH_RESTART_VPN=on \ HEALTH_RESTART_VPN=on \
# DNS # DNS
DNS_SERVER=on \
DNS_UPSTREAM_RESOLVER_TYPE=DoT \ DNS_UPSTREAM_RESOLVER_TYPE=DoT \
# Note: DNS_UPSTREAM_RESOLVERS defaults to cloudflare in code if DNS_UPSTREAM_PLAIN_ADDRESSES is empty DNS_UPSTREAM_RESOLVERS=cloudflare \
DNS_UPSTREAM_RESOLVERS= \
DNS_BLOCK_IPS= \ DNS_BLOCK_IPS= \
DNS_BLOCK_IP_PREFIXES= \ DNS_BLOCK_IP_PREFIXES= \
DNS_CACHING=on \ DNS_CACHING=on \
@@ -239,7 +181,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
DNS_UNBLOCK_HOSTNAMES= \ DNS_UNBLOCK_HOSTNAMES= \
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \ DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
DNS_UPDATE_PERIOD=24h \ DNS_UPDATE_PERIOD=24h \
DNS_UPSTREAM_PLAIN_ADDRESSES= \ DNS_ADDRESS=127.0.0.1 \
DNS_KEEP_NAMESERVER=off \
# HTTP proxy # HTTP proxy
HTTPPROXY= \ HTTPPROXY= \
HTTPPROXY_LOG=off \ HTTPPROXY_LOG=off \
@@ -281,7 +224,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
PPROF_HTTP_SERVER_ADDRESS=":6060" \ PPROF_HTTP_SERVER_ADDRESS=":6060" \
# Extras # Extras
VERSION_INFORMATION=on \ VERSION_INFORMATION=on \
BORINGPOLL_GLUETUNCOM=off \
TZ= \ TZ= \
PUID=1000 \ PUID=1000 \
PGID=1000 PGID=1000
+2 -5
View File
@@ -2,8 +2,6 @@
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️ ⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
💁 You can optionally set `BORINGPOLL_GLUETUNCOM=on` to... [poll](./internal/boringpoll/boringpoll.go) that **scammy AI slop** website every few minutes so it costs them too much to keep it up. My gentle email reminders to take it down are being grossly ignored 🤷 This would make me very happy and serve this community.
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg) ![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
@@ -59,15 +57,14 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
## Features ## Features
- Based on Alpine 3.23 for a small Docker image of 43.1MB - Based on Alpine 3.22 for a small Docker image of 41.1MB
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad** (Wireguard only), **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers - Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers
- Supports OpenVPN for all providers listed - Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace - Supports Wireguard both kernelspace and userspace
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe** - For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134) - More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- Supports AmneziaWG only with the custom provider for now
- DNS over TLS baked in with service provider(s) of your choice - DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- Choose the vpn network protocol, `udp` or `tcp` - Choose the vpn network protocol, `udp` or `tcp`
+1 -1
View File
@@ -17,7 +17,7 @@ import (
func ptrTo[T any](v T) *T { return &v } func ptrTo[T any](v T) *T { return &v }
func simpleTest(ctx context.Context, env []string, logger Logger) error { func simpleTest(ctx context.Context, env []string, logger Logger) error {
const timeout = 60 * time.Second const timeout = 30 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout) ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() defer cancel()
+31 -64
View File
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"net/netip"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@@ -16,10 +15,7 @@ import (
_ "time/tzdata" _ "time/tzdata"
_ "github.com/breml/rootcerts" _ "github.com/breml/rootcerts"
"github.com/qdm12/dns/v2/pkg/doh"
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/alpine" "github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/boringpoll"
"github.com/qdm12/gluetun/internal/cli" "github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/command" "github.com/qdm12/gluetun/internal/command"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
@@ -171,7 +167,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
defer fmt.Println(gluetunLogo) defer fmt.Println(gluetunLogo)
announcementExp, err := time.Parse(time.RFC3339, "2026-04-30T00:00:00Z") announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
if err != nil { if err != nil {
return err return err
} }
@@ -182,7 +178,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
Version: buildInfo.Version, Version: buildInfo.Version,
Commit: buildInfo.Commit, Commit: buildInfo.Commit,
Created: buildInfo.Created, Created: buildInfo.Created,
Announcement: "Set BORINGPOLL_GLUETUNCOM=on to help combat AI slop and shutdown that scam website", Announcement: "All control server routes will become private by default after the v3.41.0 release",
AnnounceExp: announcementExp, AnnounceExp: announcementExp,
// Sponsor information // Sponsor information
PaypalUser: "qmcgaw", PaypalUser: "qmcgaw",
@@ -210,6 +206,9 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
netLinker.PatchLoggerLevel(logLevel) netLinker.PatchLoggerLevel(logLevel)
routingLogger := logger.New(log.SetComponent("routing")) routingLogger := logger.New(log.SetComponent("routing"))
if *allSettings.Firewall.Debug { // To remove in v4
routingLogger.Patch(log.SetLevel(log.LevelDebug))
}
routingConf := routing.New(netLinker, routingLogger) routingConf := routing.New(netLinker, routingLogger)
defaultRoutes, err := routingConf.DefaultRoutes() defaultRoutes, err := routingConf.DefaultRoutes()
@@ -222,11 +221,11 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
iptablesLogLevel, _ := log.ParseLevel(allSettings.Firewall.Iptables.LogLevel)
iptablesLogger := logger.New(log.SetComponent("iptables"), log.SetLevel(iptablesLogLevel))
firewallLogger := logger.New(log.SetComponent("firewall")) firewallLogger := logger.New(log.SetComponent("firewall"))
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, iptablesLogger, cmder, if *allSettings.Firewall.Debug { // To remove in v4
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
}
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
defaultRoutes, localNetworks) defaultRoutes, localNetworks)
if err != nil { if err != nil {
return err return err
@@ -237,10 +236,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
if err != nil { if err != nil {
return err return err
} }
err = netLinker.FlushConntrack()
if err != nil {
logger.Warnf("flushing conntrack failed: %s", err)
}
} }
// TODO run this in a loop or in openvpn to reload from file without restarting // TODO run this in a loop or in openvpn to reload from file without restarting
@@ -250,13 +245,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel(ctx, ipv6Supported, err := netLinker.IsIPv6Supported()
allSettings.IPv6.CheckAddresses, firewallConf)
if err != nil { if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err) return fmt.Errorf("checking for IPv6 support: %w", err)
} }
ipv6Supported := ipv6SupportLevel == netlink.IPv6Supported ||
ipv6SupportLevel == netlink.IPv6Internet
err = allSettings.Validate(storage, ipv6Supported, logger) err = allSettings.Validate(storage, ipv6Supported, logger)
if err != nil { if err != nil {
@@ -271,7 +263,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID) puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
const clientTimeout = 35 * time.Second const clientTimeout = 15 * time.Second
httpClient := &http.Client{Timeout: clientTimeout} httpClient := &http.Client{Timeout: clientTimeout}
// Create configurators // Create configurators
alpineConf := alpine.New() alpineConf := alpine.New()
@@ -286,7 +278,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
err = printVersions(ctx, logger, []printVersionElement{ err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version}, {name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN", getVersion: ovpnVersion}, {name: "OpenVPN", getVersion: ovpnVersion},
{name: "Firewall", getVersion: firewallConf.Version}, {name: "IPtables", getVersion: firewallConf.Version},
}) })
if err != nil { if err != nil {
return err return err
@@ -402,7 +394,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
dnsLogger := logger.New(log.SetComponent("dns")) dnsLogger := logger.New(log.SetComponent("dns"))
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient, dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
dnsLogger, localNetworksToPrefixes(localNetworks)) dnsLogger)
if err != nil { if err != nil {
return fmt.Errorf("creating DNS loop: %w", err) return fmt.Errorf("creating DNS loop: %w", err)
} }
@@ -435,31 +427,20 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go healthcheckServer.Run(healthServerCtx, healthServerDone) go healthcheckServer.Run(healthServerCtx, healthServerDone)
healthChecker := healthcheck.NewChecker(healthLogger) healthChecker := healthcheck.NewChecker(healthLogger)
// Note: we use a separate DoH dialer for the VPN servers data updater, separate from the
// main DNS local server to make sure no request is blocked by filters.
dohDialer, err := doh.New(doh.Settings{
UpstreamResolvers: []dnsprovider.Provider{dnsprovider.Cloudflare(), dnsprovider.Google()},
})
if err != nil {
return fmt.Errorf("creating updater DoH dialer: %w", err)
}
updaterLogger := logger.New(log.SetComponent("updater")) updaterLogger := logger.New(log.SetComponent("updater"))
unzipper := unzip.New(httpClient) unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(dohDialer) parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
openvpnFileExtractor := extract.New() openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger, providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
openvpnFileExtractor, allSettings.Updater) openvpnFileExtractor, allSettings.Updater)
boringPollLogger := logger.New(log.SetComponent("boring poll"))
boringPoll := boringpoll.New(httpClient, boringPollLogger, allSettings.BoringPoll)
vpnLogger := logger.New(log.SetComponent("vpn")) vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6SupportLevel, allSettings.Firewall.VPNInputPorts, vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
providers, storage, boringPoll, allSettings.Health, healthChecker, healthcheckServer, providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, cmder, publicIPLooper, routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
dnsLooper, vpnLogger, httpClient, buildInfo, *allSettings.Version.Enabled) buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler( vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second)) "vpn", goroutine.OptionTimeout(time.Second))
go vpnLooper.Run(vpnCtx, vpnDone) go vpnLooper.Run(vpnCtx, vpnDone)
@@ -497,7 +478,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer, httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
logger.New(log.SetComponent("http server")), logger.New(log.SetComponent("http server")),
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper, buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6SupportLevel.IsSupported()) storage, ipv6Supported)
if err != nil { if err != nil {
return fmt.Errorf("setting up control server: %w", err) return fmt.Errorf("setting up control server: %w", err)
} }
@@ -567,42 +548,31 @@ func printVersions(ctx context.Context, logger infoer,
return nil return nil
} }
func localNetworksToPrefixes(localNetworks []routing.LocalNetwork) (prefixes []netip.Prefix) {
prefixes = make([]netip.Prefix, len(localNetworks))
for i, localNetwork := range localNetworks {
prefixes[i] = localNetwork.IPNet
}
return prefixes
}
type netLinker interface { type netLinker interface {
Addresser Addresser
Router Router
Ruler Ruler
Linker Linker
IsWireguardSupported() (ok bool, err error) IsWireguardSupported() bool
FindIPv6SupportLevel(ctx context.Context, IsIPv6Supported() (ok bool, err error)
checkAddresses []netip.AddrPort, firewall netlink.Firewall,
) (level netlink.IPv6SupportLevel, err error)
FlushConntrack() error
PatchLoggerLevel(level log.Level) PatchLoggerLevel(level log.Level)
} }
type Addresser interface { type Addresser interface {
AddrList(linkIndex uint32, family uint8) ( AddrList(link netlink.Link, family int) (
addresses []netip.Prefix, err error) addresses []netlink.Addr, err error)
AddrReplace(linkIndex uint32, addr netip.Prefix) error AddrReplace(link netlink.Link, addr netlink.Addr) error
} }
type Router interface { type Router interface {
RouteList(family uint8) (routes []netlink.Route, err error) RouteList(family int) (routes []netlink.Route, err error)
RouteAdd(route netlink.Route) error RouteAdd(route netlink.Route) error
RouteDel(route netlink.Route) error RouteDel(route netlink.Route) error
RouteReplace(route netlink.Route) error RouteReplace(route netlink.Route) error
} }
type Ruler interface { type Ruler interface {
RuleList(family uint8) (rules []netlink.Rule, err error) RuleList(family int) (rules []netlink.Rule, err error)
RuleAdd(rule netlink.Rule) error RuleAdd(rule netlink.Rule) error
RuleDel(rule netlink.Rule) error RuleDel(rule netlink.Rule) error
} }
@@ -610,12 +580,11 @@ type Ruler interface {
type Linker interface { type Linker interface {
LinkList() (links []netlink.Link, err error) LinkList() (links []netlink.Link, err error)
LinkByName(name string) (link netlink.Link, err error) LinkByName(name string) (link netlink.Link, err error)
LinkByIndex(index uint32) (link netlink.Link, err error) LinkByIndex(index int) (link netlink.Link, err error)
LinkAdd(link netlink.Link) (linkIndex uint32, err error) LinkAdd(link netlink.Link) (linkIndex int, err error)
LinkDel(linkIndex uint32) (err error) LinkDel(link netlink.Link) (err error)
LinkSetUp(linkIndex uint32) (err error) LinkSetUp(link netlink.Link) (linkIndex int, err error)
LinkSetDown(linkIndex uint32) (err error) LinkSetDown(link netlink.Link) (err error)
LinkSetMTU(linkIndex, mtu uint32) error
} }
type clier interface { type clier interface {
@@ -636,8 +605,6 @@ type RunStarter interface {
Run(cmd *exec.Cmd) (output string, err error) Run(cmd *exec.Cmd) (output string, err error)
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string, Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, err error) waitError <-chan error, err error)
RunAndLog(ctx context.Context, commandString string,
logger command.Logger) (err error)
} }
const gluetunLogo = ` @@@ const gluetunLogo = ` @@@
+20 -19
View File
@@ -4,34 +4,30 @@ go 1.25.0
require ( require (
github.com/ProtonMail/go-srp v0.0.7 github.com/ProtonMail/go-srp v0.0.7
github.com/amnezia-vpn/amneziawg-go v0.2.16 github.com/breml/rootcerts v0.3.3
github.com/breml/rootcerts v0.3.4
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/jsimonetti/rtnetlink v1.4.2 github.com/klauspost/compress v1.18.1
github.com/klauspost/compress v1.18.4
github.com/klauspost/pgzip v1.2.6 github.com/klauspost/pgzip v1.2.6
github.com/mdlayher/genetlink v1.3.2
github.com/mdlayher/netlink v1.9.0
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a github.com/qdm12/dns/v2 v2.0.0-rc10
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.0
github.com/qdm12/gotree v0.3.0 github.com/qdm12/gotree v0.3.0
github.com/qdm12/log v0.1.0 github.com/qdm12/log v0.1.0
github.com/qdm12/ss-server v0.6.0 github.com/qdm12/ss-server v0.6.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/ti-mo/netfilter v0.5.3
github.com/ulikunitz/xz v0.5.15 github.com/ulikunitz/xz v0.5.15
github.com/vishvananda/netlink v1.3.1
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
golang.org/x/net v0.51.0 golang.org/x/net v0.47.0
golang.org/x/sys v0.42.0 golang.org/x/sys v0.38.0
golang.org/x/text v0.35.0 golang.org/x/text v0.31.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/ini.v1 v1.67.1 gopkg.in/ini.v1 v1.67.0
) )
require ( require (
@@ -42,10 +38,13 @@ require (
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect github.com/cronokirby/saferith v0.33.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/miekg/dns v1.1.62 // indirect github.com/miekg/dns v1.1.62 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
@@ -56,10 +55,12 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
golang.org/x/crypto v0.48.0 // indirect github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/mod v0.33.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/mod v0.29.0 // indirect
golang.org/x/tools v0.42.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.35.1 // indirect google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
+44 -55
View File
@@ -6,37 +6,32 @@ github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYS
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/amnezia-vpn/amneziawg-go v0.2.16 h1:XY6HOq/xtqH8ZXMncRWkjFs85EKdN10NLNnw23kTpE0=
github.com/amnezia-vpn/amneziawg-go v0.2.16/go.mod h1:nRkPpIzjCxMW8pZKXTRkpqAQVlmFJdVOGkeQSC7wbms=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/breml/rootcerts v0.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys= github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
github.com/breml/rootcerts v0.3.4/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo= github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -50,10 +45,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
@@ -74,16 +69,16 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc= github.com/qdm12/dns/v2 v2.0.0-rc10 h1:IyeNEYXfhBsaE1dwxx5eAqdAz1HS98dT+8c7xoKODa0=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE= github.com/qdm12/dns/v2 v2.0.0-rc10/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=
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg= github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM= github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM= github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c h1:l8qz53IqEXRGK0X62gWwipG077Fz5eNM7qe4mUbAr/Q= github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c/go.mod h1:vgRg8Skq9+RNp1THecwMI7SGsnIwO/NPMfYenNTgpAc= github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI= github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw= github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw= github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
@@ -94,39 +89,32 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ti-mo/netfilter v0.5.3 h1:ikzduvnaUMwre5bhbNwWOd6bjqLMVb33vv0XXbK0xGQ=
github.com/ti-mo/netfilter v0.5.3/go.mod h1:08SyBCg6hu1qyQk4s3DjjJKNrm3RTb32nm6AzyT972E=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -134,14 +122,14 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -152,10 +140,12 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -165,17 +155,17 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -190,13 +180,12 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw= kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4= kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI= kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
-22
View File
@@ -1,22 +0,0 @@
package amneziawg
type Amneziawg struct {
logger Logger
settings Settings
netlink NetLinker
}
func New(settings Settings, netlink NetLinker,
logger Logger,
) (a *Amneziawg, err error) {
settings.SetDefaults()
if err := settings.Check(); err != nil {
return nil, err
}
return &Amneziawg{
logger: logger,
settings: settings,
netlink: netlink,
}, nil
}
-86
View File
@@ -1,86 +0,0 @@
package amneziawg
import (
"net/netip"
"testing"
"github.com/qdm12/gluetun/internal/wireguard"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.zx2c4.com/wireguard/device"
)
func Test_New(t *testing.T) {
t.Parallel()
const validKeyString = "oMNSf/zJ0pt1ciy+qIRk8Rlyfs9accwuRLnKd85Yl1Q="
logger := NewMockLogger(nil)
netLinker := NewMockNetLinker(nil)
testCases := map[string]struct {
settings Settings
amneziawg *Amneziawg
err error
}{
"bad_settings": {
settings: Settings{
Wireguard: wireguard.Settings{
PrivateKey: "",
},
},
err: wireguard.ErrPrivateKeyMissing,
},
"minimal valid settings": {
settings: Settings{
Wireguard: wireguard.Settings{
PrivateKey: validKeyString,
PublicKey: validKeyString,
Endpoint: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 0),
Addresses: []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32),
},
FirewallMark: 100,
},
},
amneziawg: &Amneziawg{
logger: logger,
netlink: netLinker,
settings: Settings{
Wireguard: wireguard.Settings{
InterfaceName: "wg0",
PrivateKey: validKeyString,
PublicKey: validKeyString,
Endpoint: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 51820),
Addresses: []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32),
},
AllowedIPs: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
},
FirewallMark: 100,
MTU: device.DefaultMTU,
IPv6: ptrTo(false),
Implementation: "auto",
},
},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
wireguard, err := New(testCase.settings, netLinker, logger)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.amneziawg, wireguard)
})
}
}
-5
View File
@@ -1,5 +0,0 @@
package amneziawg
func ptrTo[T any](v T) *T {
return &v
}
-11
View File
@@ -1,11 +0,0 @@
package amneziawg
//go:generate mockgen -destination=log_mock_test.go -package amneziawg . Logger
type Logger interface {
Debug(s string)
Debugf(format string, args ...interface{})
Info(s string)
Error(s string)
Errorf(format string, args ...interface{})
}
-104
View File
@@ -1,104 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/amneziawg (interfaces: Logger)
// Package amneziawg is a generated GoMock package.
package amneziawg
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Debug", arg0)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(arg0 string, arg1 ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Error", arg0)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(arg0 string, arg1 ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Info mocks base method.
func (m *MockLogger) Info(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Info", arg0)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
}
-36
View File
@@ -1,36 +0,0 @@
package amneziawg
import (
"net/netip"
"github.com/qdm12/gluetun/internal/netlink"
)
//go:generate mockgen -destination=netlinker_mock_test.go -package amneziawg . NetLinker
type NetLinker interface {
AddrReplace(linkIndex uint32, addr netip.Prefix) error
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
}
type Router interface {
RouteList(family uint8) (routes []netlink.Route, err error)
RouteAdd(route netlink.Route) error
}
type Ruler interface {
RuleAdd(rule netlink.Rule) error
RuleDel(rule netlink.Rule) error
}
type Linker interface {
LinkAdd(link netlink.Link) (linkIndex uint32, err error)
LinkList() (links []netlink.Link, err error)
LinkByName(name string) (link netlink.Link, err error)
LinkSetUp(linkIndex uint32) error
LinkSetDown(linkIndex uint32) error
LinkDel(linkIndex uint32) error
}
-209
View File
@@ -1,209 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/amneziawg (interfaces: NetLinker)
// Package amneziawg is a generated GoMock package.
package amneziawg
import (
netip "net/netip"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
netlink "github.com/qdm12/gluetun/internal/netlink"
)
// MockNetLinker is a mock of NetLinker interface.
type MockNetLinker struct {
ctrl *gomock.Controller
recorder *MockNetLinkerMockRecorder
}
// MockNetLinkerMockRecorder is the mock recorder for MockNetLinker.
type MockNetLinkerMockRecorder struct {
mock *MockNetLinker
}
// NewMockNetLinker creates a new mock instance.
func NewMockNetLinker(ctrl *gomock.Controller) *MockNetLinker {
mock := &MockNetLinker{ctrl: ctrl}
mock.recorder = &MockNetLinkerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockNetLinker) EXPECT() *MockNetLinkerMockRecorder {
return m.recorder
}
// AddrReplace mocks base method.
func (m *MockNetLinker) AddrReplace(arg0 uint32, arg1 netip.Prefix) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddrReplace", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// AddrReplace indicates an expected call of AddrReplace.
func (mr *MockNetLinkerMockRecorder) AddrReplace(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrReplace", reflect.TypeOf((*MockNetLinker)(nil).AddrReplace), arg0, arg1)
}
// IsWireguardSupported mocks base method.
func (m *MockNetLinker) IsWireguardSupported() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsWireguardSupported")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsWireguardSupported indicates an expected call of IsWireguardSupported.
func (mr *MockNetLinkerMockRecorder) IsWireguardSupported() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsWireguardSupported", reflect.TypeOf((*MockNetLinker)(nil).IsWireguardSupported))
}
// LinkAdd mocks base method.
func (m *MockNetLinker) LinkAdd(arg0 netlink.Link) (uint32, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LinkAdd", arg0)
ret0, _ := ret[0].(uint32)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LinkAdd indicates an expected call of LinkAdd.
func (mr *MockNetLinkerMockRecorder) LinkAdd(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkAdd", reflect.TypeOf((*MockNetLinker)(nil).LinkAdd), arg0)
}
// LinkByName mocks base method.
func (m *MockNetLinker) LinkByName(arg0 string) (netlink.Link, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LinkByName", arg0)
ret0, _ := ret[0].(netlink.Link)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LinkByName indicates an expected call of LinkByName.
func (mr *MockNetLinkerMockRecorder) LinkByName(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkByName", reflect.TypeOf((*MockNetLinker)(nil).LinkByName), arg0)
}
// LinkDel mocks base method.
func (m *MockNetLinker) LinkDel(arg0 uint32) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LinkDel", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// LinkDel indicates an expected call of LinkDel.
func (mr *MockNetLinkerMockRecorder) LinkDel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkDel", reflect.TypeOf((*MockNetLinker)(nil).LinkDel), arg0)
}
// LinkList mocks base method.
func (m *MockNetLinker) LinkList() ([]netlink.Link, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LinkList")
ret0, _ := ret[0].([]netlink.Link)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LinkList indicates an expected call of LinkList.
func (mr *MockNetLinkerMockRecorder) LinkList() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkList", reflect.TypeOf((*MockNetLinker)(nil).LinkList))
}
// LinkSetDown mocks base method.
func (m *MockNetLinker) LinkSetDown(arg0 uint32) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LinkSetDown", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// LinkSetDown indicates an expected call of LinkSetDown.
func (mr *MockNetLinkerMockRecorder) LinkSetDown(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetDown", reflect.TypeOf((*MockNetLinker)(nil).LinkSetDown), arg0)
}
// LinkSetUp mocks base method.
func (m *MockNetLinker) LinkSetUp(arg0 uint32) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LinkSetUp", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// LinkSetUp indicates an expected call of LinkSetUp.
func (mr *MockNetLinkerMockRecorder) LinkSetUp(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetUp", reflect.TypeOf((*MockNetLinker)(nil).LinkSetUp), arg0)
}
// RouteAdd mocks base method.
func (m *MockNetLinker) RouteAdd(arg0 netlink.Route) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RouteAdd", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// RouteAdd indicates an expected call of RouteAdd.
func (mr *MockNetLinkerMockRecorder) RouteAdd(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteAdd", reflect.TypeOf((*MockNetLinker)(nil).RouteAdd), arg0)
}
// RouteList mocks base method.
func (m *MockNetLinker) RouteList(arg0 byte) ([]netlink.Route, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RouteList", arg0)
ret0, _ := ret[0].([]netlink.Route)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RouteList indicates an expected call of RouteList.
func (mr *MockNetLinkerMockRecorder) RouteList(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteList", reflect.TypeOf((*MockNetLinker)(nil).RouteList), arg0)
}
// RuleAdd mocks base method.
func (m *MockNetLinker) RuleAdd(arg0 netlink.Rule) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RuleAdd", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// RuleAdd indicates an expected call of RuleAdd.
func (mr *MockNetLinkerMockRecorder) RuleAdd(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleAdd", reflect.TypeOf((*MockNetLinker)(nil).RuleAdd), arg0)
}
// RuleDel mocks base method.
func (m *MockNetLinker) RuleDel(arg0 netlink.Rule) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RuleDel", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// RuleDel indicates an expected call of RuleDel.
func (mr *MockNetLinkerMockRecorder) RuleDel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleDel", reflect.TypeOf((*MockNetLinker)(nil).RuleDel), arg0)
}
-133
View File
@@ -1,133 +0,0 @@
package amneziawg
import (
"context"
"errors"
"fmt"
"net"
amneziaconn "github.com/amnezia-vpn/amneziawg-go/conn"
amneziadevice "github.com/amnezia-vpn/amneziawg-go/device"
amneziatun "github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/qdm12/gluetun/internal/cleanup"
"github.com/qdm12/gluetun/internal/wireguard"
)
var (
errTunNameMismatch = errors.New("TUN device name is mismatching")
errDeviceWaited = errors.New("device waited for")
)
// Run runs the amneziawg interface and waits until the context is done, then it cleans up the
// interface and returns any error that occurred during setup or waiting. It sends an error to
// waitError if any error occurs during setup or waiting, otherwise it sends nil when the context
// is done. It sends a signal to ready when the setup is complete and the interface is ready to use.
// See https://github.com/amnezia-vpn/amneziawg-go/blob/master/main.go
func (a *Amneziawg) Run(ctx context.Context, waitError chan<- error, ready chan<- struct{}) {
setup := func(ctx context.Context, cleanups *cleanup.Cleanups) (
linkIndex uint32, waitAndCleanup func() error, err error,
) {
return setupUserspace(ctx, a.settings.Wireguard.InterfaceName,
a.netlink, a.settings.Wireguard.MTU, cleanups, a.logger, a.settings)
}
wireguard.Run(ctx, waitError, ready, setup, a.settings.Wireguard, a.netlink, a.logger)
}
func setupUserspace(ctx context.Context,
interfaceName string, netLinker NetLinker, mtu uint32,
cleanups *cleanup.Cleanups, logger Logger,
settings Settings,
) (
linkIndex uint32, waitAndCleanup func() error, err error,
) {
tun, err := amneziatun.CreateTUN(interfaceName, int(mtu))
if err != nil {
return 0, nil, fmt.Errorf("creating TUN device: %w", err)
}
cleanups.Add("closing TUN device", 7, tun.Close)
tunName, err := tun.Name()
if err != nil {
return 0, nil, fmt.Errorf("getting created TUN device name: %w", err)
} else if tunName != interfaceName {
return 0, nil, fmt.Errorf("%w: expected %q and got %q",
errTunNameMismatch, interfaceName, tunName)
}
link, err := netLinker.LinkByName(interfaceName)
if err != nil {
return 0, nil, fmt.Errorf("finding link %s: %w", interfaceName, err)
}
cleanups.Add("deleting link", 5, func() error {
return netLinker.LinkDel(link.Index)
})
bind := amneziaconn.NewDefaultBind()
cleanups.Add("closing bind", 7, bind.Close)
deviceLogger := amneziadevice.Logger{
Verbosef: logger.Debugf,
Errorf: logger.Errorf,
}
device := amneziadevice.NewDevice(tun, bind, &deviceLogger)
cleanups.Add("closing Wireguard device", 6, func() error {
device.Close()
return nil
})
uapiFile, err := wireguard.UAPIOpen(interfaceName)
if err != nil {
return 0, nil, fmt.Errorf("opening UAPI socket: %w", err)
}
cleanups.Add("closing UAPI file", 3, uapiFile.Close)
uapiListener, err := wireguard.UAPIListen(interfaceName, uapiFile)
if err != nil {
return 0, nil, fmt.Errorf("listening on UAPI socket: %w", err)
}
cleanups.Add("closing UAPI listener", 2, uapiListener.Close)
uapiConfig := settings.uapiConfig()
err = device.IpcSet(uapiConfig)
if err != nil {
return 0, nil, fmt.Errorf("setting amneziawg uapi config: %w", err)
}
// acceptAndHandle exits when uapiListener is closed
uapiAcceptErrorCh := make(chan error)
go acceptAndHandle(uapiListener, device, uapiAcceptErrorCh)
waitAndCleanup = func() error {
select {
case <-ctx.Done():
err = ctx.Err()
case err = <-uapiAcceptErrorCh:
close(uapiAcceptErrorCh)
case <-device.Wait():
err = errDeviceWaited
}
cleanups.Cleanup(logger)
<-uapiAcceptErrorCh // wait for acceptAndHandle to exit
return err
}
return link.Index, waitAndCleanup, nil
}
func acceptAndHandle(uapi net.Listener, device *amneziadevice.Device,
uapiAcceptErrorCh chan<- error,
) {
for { // stopped by uapiFile.Close()
conn, err := uapi.Accept()
if err != nil {
uapiAcceptErrorCh <- err
return
}
go device.IpcHandle(conn)
}
}
-69
View File
@@ -1,69 +0,0 @@
package amneziawg
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/wireguard"
)
type Settings struct {
Wireguard wireguard.Settings
JunkPacketCount uint16
JunkPacketMin uint16
JunkPacketMax uint16
PaddingS1 uint16
PaddingS2 uint16
PaddingS3 uint16
PaddingS4 uint16
HeaderH1 string
HeaderH2 string
HeaderH3 string
HeaderH4 string
InitPacketI1 string
InitPacketI2 string
InitPacketI3 string
InitPacketI4 string
InitPacketI5 string
}
func (s Settings) uapiConfig() string {
uintFields := map[string]uint16{
"jc": s.JunkPacketCount,
"jmin": s.JunkPacketMin,
"jmax": s.JunkPacketMax,
"s1": s.PaddingS1,
"s2": s.PaddingS2,
"s3": s.PaddingS3,
"s4": s.PaddingS4,
}
stringFields := map[string]string{
"h1": s.HeaderH1,
"h2": s.HeaderH2,
"h3": s.HeaderH3,
"h4": s.HeaderH4,
"i1": s.InitPacketI1,
"i2": s.InitPacketI2,
"i3": s.InitPacketI3,
"i4": s.InitPacketI4,
"i5": s.InitPacketI5,
}
lines := make([]string, 0, len(uintFields)+len(stringFields))
for key, val := range uintFields {
lines = append(lines, fmt.Sprintf("%s=%d", key, val))
}
for key, val := range stringFields {
lines = append(lines, key+"="+val)
}
return strings.Join(lines, "\n")
}
func (s *Settings) SetDefaults() {
s.Wireguard.SetDefaults()
}
func (s *Settings) Check() error {
return s.Wireguard.Check()
}
-189
View File
@@ -1,189 +0,0 @@
package boringpoll
import (
"context"
"fmt"
"io"
"math/rand"
"net/http"
"sync"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
type BoringPoll struct {
// Injected dependencies
client *http.Client
logger Logger
// Internal state
urlToData map[string]*urlData
// Internal signals and channels
cancel context.CancelFunc
done *sync.WaitGroup
mutex sync.Mutex
}
type urlData struct{}
func New(client *http.Client, logger Logger, settings settings.BoringPoll) *BoringPoll {
urlToData := make(map[string]*urlData)
if *settings.GluetunCom {
urlToData["https://gluetun.com/wp-json"] = &urlData{}
}
return &BoringPoll{
client: client,
logger: logger,
urlToData: urlToData,
}
}
func (b *BoringPoll) Start() (runError <-chan error, err error) {
b.mutex.Lock()
defer b.mutex.Unlock()
if len(b.urlToData) == 0 {
return nil, nil //nolint:nilnil
}
const minPeriod = time.Minute
const maxPeriod = 5 * time.Minute
const logEveryBytes = 100 * 1000 * 1000 // 100 IEC MB
var ready, done sync.WaitGroup
b.done = &done
ready.Add(len(b.urlToData))
done.Add(len(b.urlToData))
ctx, cancel := context.WithCancel(context.Background())
b.cancel = cancel
for url := range b.urlToData {
go func(url string) {
defer done.Done()
b.logger.Infof("running against %s periodically between %s and %s "+
"and will log every %s downloaded",
url, minPeriod, maxPeriod, byteCountSI(logEveryBytes))
totalDownloaded := uint64(0)
lastDownloaded := uint64(0)
consecutiveFails := 0
const maxConsecutiveErrs = 3
const coolDownTimeout = time.Hour
timer := time.NewTimer(time.Hour)
var err error
ready.Done()
for {
timeout := minPeriod + time.Duration(rand.Int63n(int64(maxPeriod-minPeriod))) //nolint:gosec
if consecutiveFails >= maxConsecutiveErrs {
b.logger.Debugf("pausing poll to %s for %s due to %d consecutive errors, last error: %s",
url, coolDownTimeout, consecutiveFails, err)
timeout = coolDownTimeout
}
timer.Reset(timeout)
select {
case <-ctx.Done():
timer.Stop()
totalDownloaded += lastDownloaded
if totalDownloaded > 0 {
b.logger.Infof("stopping poll to %s, downloaded %s!", url, byteCountSI(totalDownloaded))
}
return
case <-timer.C:
}
var n int64
n, err = fetchURL(ctx, b.client, url)
if err != nil {
consecutiveFails++
continue
}
consecutiveFails = 0
totalDownloaded += uint64(n) //nolint:gosec
lastDownloaded += uint64(n) //nolint:gosec
if lastDownloaded >= logEveryBytes {
b.logger.Infof("thanks for helping! You have downloaded %s from %s so far!",
byteCountSI(totalDownloaded), url)
lastDownloaded = 0
}
}
}(url)
}
return nil, nil //nolint:nilnil
}
func fetchURL(ctx context.Context, client *http.Client, url string) (downloaded int64, err error) {
const requestTimeout = 10 * time.Second
ctx, cancel := context.WithTimeout(ctx, requestTimeout)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
cancel()
return 0, err
}
request.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
request.Header.Set("Pragma", "no-cache")
request.Header.Set("Expires", "0")
request.Header.Set("User-Agent", getRandomUserAgent())
response, err := client.Do(request)
if err != nil {
return 0, err
}
downloaded, err = io.Copy(io.Discard, response.Body)
_ = response.Body.Close()
if err != nil {
return 0, err
}
return downloaded, nil
}
func getRandomUserAgent() string {
//nolint:lll
userAgents := [...]string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (iPad; CPU OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Android 14; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
}
return userAgents[rand.Intn(len(userAgents))] //nolint:gosec
}
func (b *BoringPoll) Stop() error {
b.mutex.Lock()
defer b.mutex.Unlock()
if b.cancel == nil {
return nil
}
b.cancel()
b.done.Wait()
b.cancel = nil
b.done = nil
return nil
}
func byteCountSI(b uint64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%dB", b)
}
div, exp := uint64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
}
-6
View File
@@ -1,6 +0,0 @@
package boringpoll
type Logger interface {
Infof(format string, args ...any)
Debugf(format string, args ...any)
}
-51
View File
@@ -1,51 +0,0 @@
package cleanup
import "sort"
type Cleanups []cleanup
type cleanup struct {
operation string
orderIndex uint
cleanup func() error
done bool
}
// Add adds a cleanup function to the list of cleanups, with a description of the
// operation being cleaned up, and an order index that determines the order in which
// the cleanup functions are run. The lower the order index, the earlier the cleanup
// function is run.
func (c *Cleanups) Add(operation string, orderIndex uint,
cleanupFunc func() error,
) {
closer := cleanup{
operation: operation,
orderIndex: orderIndex,
cleanup: cleanupFunc,
}
*c = append(*c, closer)
}
// Cleanup runs the cleanup functions in the order of their orderIndex,
// and logs any error that occurs during cleanup.
// It can also be re-called in case a cleanup fails, and already cleaned up
// functions will not be re-run.
func (c *Cleanups) Cleanup(logger Logger) {
closers := *c
sort.Slice(closers, func(i, j int) bool {
return closers[i].orderIndex < closers[j].orderIndex
})
for i, closer := range closers {
if closer.done {
continue
}
closers[i].done = true
logger.Debug(closer.operation + "...")
err := closer.cleanup()
if err != nil {
logger.Error("failed " + closer.operation + ": " + err.Error())
}
}
}
-57
View File
@@ -1,57 +0,0 @@
package cleanup
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func Test_Cleanups(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
var ACloseCalled, BCloseCalled, CCloseCalled bool
var (
AErr error
BErr = errors.New("B failed")
CErr = errors.New("C failed")
)
var cleanups Cleanups
cleanups.Add("cleaning up A", 5, func() error {
ACloseCalled = true
return AErr
})
cleanups.Add("cleaning up B", 3, func() error {
BCloseCalled = true
return BErr
})
cleanups.Add("cleaning up C", 2, func() error {
CCloseCalled = true
return CErr
})
logger := NewMockLogger(ctrl)
prevCall := logger.EXPECT().Debug("cleaning up C...")
prevCall = logger.EXPECT().Error("failed cleaning up C: C failed").After(prevCall)
prevCall = logger.EXPECT().Debug("cleaning up B...").After(prevCall)
prevCall = logger.EXPECT().Error("failed cleaning up B: B failed").After(prevCall)
logger.EXPECT().Debug("cleaning up A...").After(prevCall)
cleanups.Cleanup(logger)
cleanups.Cleanup(logger) // run twice should not close already closed
for _, cleanup := range cleanups {
assert.True(t, cleanup.done)
}
assert.True(t, ACloseCalled)
assert.True(t, BCloseCalled)
assert.True(t, CCloseCalled)
}
-6
View File
@@ -1,6 +0,0 @@
package cleanup
type Logger interface {
Debug(string)
Error(string)
}
-3
View File
@@ -1,3 +0,0 @@
package cleanup
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Logger
-58
View File
@@ -1,58 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/cleanup (interfaces: Logger)
// Package cleanup is a generated GoMock package.
package cleanup
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Debug", arg0)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
}
// Error mocks base method.
func (m *MockLogger) Error(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Error", arg0)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
}
-14
View File
@@ -1,14 +0,0 @@
package cli
import (
"context"
"net/netip"
)
type noopFirewall struct{}
func (f *noopFirewall) AcceptOutput(_ context.Context, _, _ string, _ netip.Addr,
_ uint16, _ bool,
) (err error) {
return nil
}
+5 -10
View File
@@ -11,7 +11,6 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn/extract" "github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
@@ -41,9 +40,7 @@ type IPFetcher interface {
} }
type IPv6Checker interface { type IPv6Checker interface {
FindIPv6SupportLevel(ctx context.Context, IsIPv6Supported() (supported bool, err error)
checkAddresses []netip.AddrPort, firewall netlink.Firewall,
) (level netlink.IPv6SupportLevel, err error)
} }
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
@@ -61,14 +58,12 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
} }
allSettings.SetDefaults() allSettings.SetDefaults()
ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel(context.Background(), ipv6Supported, err := ipv6Checker.IsIPv6Supported()
allSettings.IPv6.CheckAddresses, &noopFirewall{})
if err != nil { if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err) return fmt.Errorf("checking for IPv6 support: %w", err)
} }
err = allSettings.Validate(storage, ipv6SupportLevel.IsSupported(), logger) if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
if err != nil {
return fmt.Errorf("validating settings: %w", err) return fmt.Errorf("validating settings: %w", err)
} }
@@ -84,13 +79,13 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater) unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
providerConf := providers.Get(allSettings.VPN.Provider.Name) providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection( connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6SupportLevel == netlink.IPv6Internet) allSettings.VPN.Provider.ServerSelection, ipv6Supported)
if err != nil { if err != nil {
return err return err
} }
lines := providerConf.OpenVPNConfig(connection, lines := providerConf.OpenVPNConfig(connection,
allSettings.VPN.OpenVPN, ipv6SupportLevel.IsSupported()) allSettings.VPN.OpenVPN, ipv6Supported)
fmt.Println(strings.Join(lines, "\n")) fmt.Println(strings.Join(lines, "\n"))
return nil return nil
+3 -20
View File
@@ -10,8 +10,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/qdm12/dns/v2/pkg/doh"
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
@@ -40,12 +38,12 @@ type UpdaterLogger interface {
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error { func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{} options := settings.Updater{}
var endUserMode, maintainerMode, updateAll bool var endUserMode, maintainerMode, updateAll bool
var dnsServer, csvProviders, ipToken, protonUsername, protonEmail, protonPassword string var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
flagSet := flag.NewFlagSet("update", flag.ExitOnError) flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)") flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false, flagSet.BoolVar(&maintainerMode, "maintainer", false,
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)") "Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&dnsServer, "dns", "", "no longer used, your DNS will use DoH with Cloudflare and Google") flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
const defaultMinRatio = 0.8 const defaultMinRatio = 0.8
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio, flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
"Minimum ratio of servers to find for the update to succeed") "Minimum ratio of servers to find for the update to succeed")
@@ -60,10 +58,6 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return err return err
} }
if dnsServer != "" {
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
}
if !endUserMode && !maintainerMode { if !endUserMode && !maintainerMode {
return fmt.Errorf("%w", ErrModeUnspecified) return fmt.Errorf("%w", ErrModeUnspecified)
} }
@@ -103,21 +97,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return fmt.Errorf("creating servers storage: %w", err) return fmt.Errorf("creating servers storage: %w", err)
} }
dohSettings := doh.Settings{
UpstreamResolvers: []dnsprovider.Provider{
dnsprovider.Cloudflare(),
dnsprovider.Google(),
},
}
dnsDialer, err := doh.New(dohSettings)
if err != nil {
return fmt.Errorf("creating DoH dialer: %w", err)
}
const clientTimeout = 10 * time.Second const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout} httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient) unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(dnsDialer) parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
nameTokenPairs := []api.NameToken{ nameTokenPairs := []api.NameToken{
{Name: string(api.IPInfo), Token: ipToken}, {Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)}, {Name: string(api.IP2Location)},
-6
View File
@@ -1,6 +0,0 @@
package command
type Logger interface {
Info(s string)
Error(s string)
}
+11 -11
View File
@@ -9,13 +9,13 @@ import (
) )
var ( var (
errCommandEmpty = errors.New("command is empty") ErrCommandEmpty = errors.New("command is empty")
errSingleQuoteUnterminated = errors.New("unterminated single-quoted string") ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
errDoubleQuoteUnterminated = errors.New("unterminated double-quoted string") ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
errEscapeUnterminated = errors.New("unterminated backslash-escape") ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
) )
// split splits a command string into a slice of arguments. // Split splits a command string into a slice of arguments.
// This is especially important for commands such as: // This is especially important for commands such as:
// /bin/sh -c "echo hello" // /bin/sh -c "echo hello"
// which should be split into: ["/bin/sh", "-c", "echo hello"] // which should be split into: ["/bin/sh", "-c", "echo hello"]
@@ -23,9 +23,9 @@ var (
// It does not support: // It does not support:
// - the $" quoting style. // - the $" quoting style.
// - expansion (brace, shell or pathname). // - expansion (brace, shell or pathname).
func split(command string) (words []string, err error) { func Split(command string) (words []string, err error) {
if command == "" { if command == "" {
return nil, fmt.Errorf("%w", errCommandEmpty) return nil, fmt.Errorf("%w", ErrCommandEmpty)
} }
const bufferSize = 1024 const bufferSize = 1024
@@ -42,7 +42,7 @@ func split(command string) (words []string, err error) {
case character == '\\': case character == '\\':
// Look ahead to eventually skip an escaped newline // Look ahead to eventually skip an escaped newline
if command[startIndex+runeSize:] == "" { if command[startIndex+runeSize:] == "" {
return nil, fmt.Errorf("%w: %q", errEscapeUnterminated, command) return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
} }
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:]) character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
if character == '\n' { if character == '\n' {
@@ -119,7 +119,7 @@ func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
startIndex = cursor startIndex = cursor
} }
} }
return "", 0, fmt.Errorf("%w", errDoubleQuoteUnterminated) return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
} }
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) ( func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
@@ -127,7 +127,7 @@ func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
) { ) {
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'') closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
if closingQuoteIndex == -1 { if closingQuoteIndex == -1 {
return "", 0, fmt.Errorf("%w", errSingleQuoteUnterminated) return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
} }
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex]) buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
const singleQuoteRuneLength = 1 const singleQuoteRuneLength = 1
@@ -139,7 +139,7 @@ func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error, word string, newStartIndex int, err error,
) { ) {
if input[startIndex:] == "" { if input[startIndex:] == "" {
return "", 0, fmt.Errorf("%w", errEscapeUnterminated) return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
} }
character, runeLength := utf8.DecodeRuneInString(input[startIndex:]) character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
if character != '\n' { // backslash-escaped newline is ignored if character != '\n' { // backslash-escaped newline is ignored
+7 -7
View File
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_split(t *testing.T) { func Test_Split(t *testing.T) {
t.Parallel() t.Parallel()
testCases := map[string]struct { testCases := map[string]struct {
@@ -17,7 +17,7 @@ func Test_split(t *testing.T) {
}{ }{
"empty": { "empty": {
command: "", command: "",
errWrapped: errCommandEmpty, errWrapped: ErrCommandEmpty,
errMessage: "command is empty", errMessage: "command is empty",
}, },
"concrete_sh_command": { "concrete_sh_command": {
@@ -74,22 +74,22 @@ func Test_split(t *testing.T) {
}, },
"unterminated_single_quote": { "unterminated_single_quote": {
command: "'abc'\\''def", command: "'abc'\\''def",
errWrapped: errSingleQuoteUnterminated, errWrapped: ErrSingleQuoteUnterminated,
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`, errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
}, },
"unterminated_double_quote": { "unterminated_double_quote": {
command: "\"abc'def", command: "\"abc'def",
errWrapped: errDoubleQuoteUnterminated, errWrapped: ErrDoubleQuoteUnterminated,
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`, errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
}, },
"unterminated_escape": { "unterminated_escape": {
command: "abc\\", command: "abc\\",
errWrapped: errEscapeUnterminated, errWrapped: ErrEscapeUnterminated,
errMessage: `splitting word in "abc\\": unterminated backslash-escape`, errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
}, },
"unterminated_escape_only": { "unterminated_escape_only": {
command: " \\", command: " \\",
errWrapped: errEscapeUnterminated, errWrapped: ErrEscapeUnterminated,
errMessage: `unterminated backslash-escape: " \\"`, errMessage: `unterminated backslash-escape: " \\"`,
}, },
} }
@@ -98,7 +98,7 @@ func Test_split(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()
words, err := split(testCase.command) words, err := Split(testCase.command)
assert.Equal(t, testCase.words, words) assert.Equal(t, testCase.words, words)
assert.ErrorIs(t, err, testCase.errWrapped) assert.ErrorIs(t, err, testCase.errWrapped)
-48
View File
@@ -1,48 +0,0 @@
package command
import (
"context"
"fmt"
"os/exec"
)
func (c *Cmder) RunAndLog(ctx context.Context, command string, logger Logger) (err error) {
args, err := split(command)
if err != nil {
return fmt.Errorf("parsing command: %w", err)
}
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec G204
stdout, stderr, waitError, err := c.Start(cmd)
if err != nil {
return err
}
streamCtx, streamCancel := context.WithCancel(context.Background())
streamDone := make(chan struct{})
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
err = <-waitError
streamCancel()
<-streamDone
return err
}
func streamLines(ctx context.Context, done chan<- struct{},
logger Logger, stdout, stderr <-chan string,
) {
defer close(done)
var line string
for {
select {
case <-ctx.Done():
return
case line = <-stdout:
logger.Info(line)
case line = <-stderr:
logger.Error(line)
}
}
}
@@ -1,243 +0,0 @@
package settings
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type AmneziaWg struct {
// Wireguard contains the configuration for Wireguard, given
// AmneziaWg is based on Wireguard
Wireguard Wireguard `json:"wireguard"`
JunkPacketCount *uint16 `json:"junk_packet_count"`
JunkPacketMin *uint16 `json:"junk_packet_min"`
JunkPacketMax *uint16 `json:"junk_packet_max"`
PaddingS1 *uint16 `json:"padding_s1"`
PaddingS2 *uint16 `json:"padding_s2"`
PaddingS3 *uint16 `json:"padding_s3"`
PaddingS4 *uint16 `json:"padding_s4"`
HeaderH1 *string `json:"header_h1"`
HeaderH2 *string `json:"header_h2"`
HeaderH3 *string `json:"header_h3"`
HeaderH4 *string `json:"header_h4"`
InitPacketI1 *string `json:"init_packet_i1"`
InitPacketI2 *string `json:"init_packet_i2"`
InitPacketI3 *string `json:"init_packet_i3"`
InitPacketI4 *string `json:"init_packet_i4"`
InitPacketI5 *string `json:"init_packet_i5"`
}
func (a *AmneziaWg) read(r *reader.Reader) (err error) {
const amneziawg = true
err = a.Wireguard.read(r, amneziawg)
if err != nil {
return err // do not wrap this error
}
uint16Fields := map[string]**uint16{
"AMNEZIAWG_JC": &a.JunkPacketCount,
"AMNEZIAWG_JMIN": &a.JunkPacketMin,
"AMNEZIAWG_JMAX": &a.JunkPacketMax,
"AMNEZIAWG_S1": &a.PaddingS1,
"AMNEZIAWG_S2": &a.PaddingS2,
"AMNEZIAWG_S3": &a.PaddingS3,
"AMNEZIAWG_S4": &a.PaddingS4,
}
for key, dst := range uint16Fields {
*dst, err = r.Uint16Ptr(key)
if err != nil {
return err
}
}
stringFields := map[string]**string{
"AMNEZIAWG_H1": &a.HeaderH1,
"AMNEZIAWG_H2": &a.HeaderH2,
"AMNEZIAWG_H3": &a.HeaderH3,
"AMNEZIAWG_H4": &a.HeaderH4,
"AMNEZIAWG_I1": &a.InitPacketI1,
"AMNEZIAWG_I2": &a.InitPacketI2,
"AMNEZIAWG_I3": &a.InitPacketI3,
"AMNEZIAWG_I4": &a.InitPacketI4,
"AMNEZIAWG_I5": &a.InitPacketI5,
}
opt := reader.ForceLowercase(false)
for key, dst := range stringFields {
*dst = r.Get(key, opt)
}
return nil
}
func (a AmneziaWg) copy() (copied AmneziaWg) {
return AmneziaWg{
Wireguard: a.Wireguard.copy(),
JunkPacketCount: gosettings.CopyPointer(a.JunkPacketCount),
JunkPacketMin: gosettings.CopyPointer(a.JunkPacketMin),
JunkPacketMax: gosettings.CopyPointer(a.JunkPacketMax),
PaddingS1: gosettings.CopyPointer(a.PaddingS1),
PaddingS2: gosettings.CopyPointer(a.PaddingS2),
PaddingS3: gosettings.CopyPointer(a.PaddingS3),
PaddingS4: gosettings.CopyPointer(a.PaddingS4),
HeaderH1: gosettings.CopyPointer(a.HeaderH1),
HeaderH2: gosettings.CopyPointer(a.HeaderH2),
HeaderH3: gosettings.CopyPointer(a.HeaderH3),
HeaderH4: gosettings.CopyPointer(a.HeaderH4),
InitPacketI1: gosettings.CopyPointer(a.InitPacketI1),
InitPacketI2: gosettings.CopyPointer(a.InitPacketI2),
InitPacketI3: gosettings.CopyPointer(a.InitPacketI3),
InitPacketI4: gosettings.CopyPointer(a.InitPacketI4),
InitPacketI5: gosettings.CopyPointer(a.InitPacketI5),
}
}
func (a *AmneziaWg) overrideWith(other AmneziaWg) {
a.Wireguard.overrideWith(other.Wireguard)
a.JunkPacketCount = gosettings.OverrideWithPointer(a.JunkPacketCount, other.JunkPacketCount)
a.JunkPacketMin = gosettings.OverrideWithPointer(a.JunkPacketMin, other.JunkPacketMin)
a.JunkPacketMax = gosettings.OverrideWithPointer(a.JunkPacketMax, other.JunkPacketMax)
a.PaddingS1 = gosettings.OverrideWithPointer(a.PaddingS1, other.PaddingS1)
a.PaddingS2 = gosettings.OverrideWithPointer(a.PaddingS2, other.PaddingS2)
a.PaddingS3 = gosettings.OverrideWithPointer(a.PaddingS3, other.PaddingS3)
a.PaddingS4 = gosettings.OverrideWithPointer(a.PaddingS4, other.PaddingS4)
a.HeaderH1 = gosettings.OverrideWithPointer(a.HeaderH1, other.HeaderH1)
a.HeaderH2 = gosettings.OverrideWithPointer(a.HeaderH2, other.HeaderH2)
a.HeaderH3 = gosettings.OverrideWithPointer(a.HeaderH3, other.HeaderH3)
a.HeaderH4 = gosettings.OverrideWithPointer(a.HeaderH4, other.HeaderH4)
a.InitPacketI1 = gosettings.OverrideWithPointer(a.InitPacketI1, other.InitPacketI1)
a.InitPacketI2 = gosettings.OverrideWithPointer(a.InitPacketI2, other.InitPacketI2)
a.InitPacketI3 = gosettings.OverrideWithPointer(a.InitPacketI3, other.InitPacketI3)
a.InitPacketI4 = gosettings.OverrideWithPointer(a.InitPacketI4, other.InitPacketI4)
a.InitPacketI5 = gosettings.OverrideWithPointer(a.InitPacketI5, other.InitPacketI5)
}
func (a *AmneziaWg) setDefaults(vpnProvider string) {
a.Wireguard.setDefaults(vpnProvider)
a.Wireguard.Implementation = "userspace" // unused except in logs
a.JunkPacketCount = gosettings.DefaultPointer(a.JunkPacketCount, 0)
a.JunkPacketMin = gosettings.DefaultPointer(a.JunkPacketMin, 0)
a.JunkPacketMax = gosettings.DefaultPointer(a.JunkPacketMax, 0)
a.PaddingS1 = gosettings.DefaultPointer(a.PaddingS1, 0)
a.PaddingS2 = gosettings.DefaultPointer(a.PaddingS2, 0)
a.PaddingS3 = gosettings.DefaultPointer(a.PaddingS3, 0)
a.PaddingS4 = gosettings.DefaultPointer(a.PaddingS4, 0)
a.HeaderH1 = gosettings.DefaultPointer(a.HeaderH1, "")
a.HeaderH2 = gosettings.DefaultPointer(a.HeaderH2, "")
a.HeaderH3 = gosettings.DefaultPointer(a.HeaderH3, "")
a.HeaderH4 = gosettings.DefaultPointer(a.HeaderH4, "")
a.InitPacketI1 = gosettings.DefaultPointer(a.InitPacketI1, "")
a.InitPacketI2 = gosettings.DefaultPointer(a.InitPacketI2, "")
a.InitPacketI3 = gosettings.DefaultPointer(a.InitPacketI3, "")
a.InitPacketI4 = gosettings.DefaultPointer(a.InitPacketI4, "")
a.InitPacketI5 = gosettings.DefaultPointer(a.InitPacketI5, "")
}
func (a AmneziaWg) toLinesNode() (node *gotree.Node) {
node = gotree.New("AmneziaWG settings:")
node.AppendNode(a.Wireguard.toLinesNode())
uintFields := []struct {
key string
val *uint16
}{
{"JC", a.JunkPacketCount},
{"JMIN", a.JunkPacketMin},
{"JMAX", a.JunkPacketMax},
{"S1", a.PaddingS1},
{"S2", a.PaddingS2},
{"S3", a.PaddingS3},
{"S4", a.PaddingS4},
}
for _, f := range uintFields {
node.Appendf("%s: %d", f.key, *f.val)
}
stringFields := []struct {
key string
val *string
}{
{"H1", a.HeaderH1},
{"H2", a.HeaderH2},
{"H3", a.HeaderH3},
{"H4", a.HeaderH4},
{"I1", a.InitPacketI1},
{"I2", a.InitPacketI2},
{"I3", a.InitPacketI3},
{"I4", a.InitPacketI4},
{"I5", a.InitPacketI5},
}
for _, f := range stringFields {
node.Appendf("%s: %s", f.key, *f.val)
}
return node
}
var (
ErrAmenziawgImplementationNotValid = errors.New("AmneziaWG implementation is not valid")
ErrJunkPacketBounds = errors.New("junk packet minimum must be lower than or equal to maximum")
ErrJunkPacketMinMaxNotSet = errors.New("junk packet min and max must be set when junk packet count is set")
ErrJunkPacketCountNotSet = errors.New("junk packet count must be set when junk packet min or max is set")
ErrHeaderRangeMalformed = errors.New("header range is malformed")
)
func (a AmneziaWg) validate(vpnProvider string, ipv6Supported bool) error {
const amneziaWG = true
err := a.Wireguard.validate(vpnProvider, ipv6Supported, amneziaWG)
if err != nil {
return fmt.Errorf("wireguard settings: %w", err)
}
if *a.JunkPacketCount == 0 {
if *a.JunkPacketMin != 0 || *a.JunkPacketMax != 0 {
return fmt.Errorf("%w: jc=%d and jmin=%d and jmax=%d",
ErrJunkPacketCountNotSet, a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
}
} else {
if *a.JunkPacketMin == 0 || *a.JunkPacketMax == 0 {
return fmt.Errorf("%w: jc=%d and jmin=%d and jmax=%d",
ErrJunkPacketMinMaxNotSet, a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
} else if *a.JunkPacketMin > *a.JunkPacketMax {
return fmt.Errorf("%w: jmin=%d and jmax=%d",
ErrJunkPacketBounds, *a.JunkPacketMin, *a.JunkPacketMax)
}
}
nameToHeaderRange := map[string]string{
"h1": *a.HeaderH1,
"h2": *a.HeaderH2,
"h3": *a.HeaderH3,
"h4": *a.HeaderH4,
}
for name, headerRange := range nameToHeaderRange {
if headerRange == "" {
continue
}
fields := strings.Split(headerRange, "-")
switch len(fields) {
case 1:
_, err := strconv.Atoi(fields[0])
if err != nil {
return fmt.Errorf("%w: %s value %s is not a number",
ErrHeaderRangeMalformed, name, headerRange)
}
case 2: //nolint:mnd
for _, field := range fields {
_, err := strconv.Atoi(field)
if err != nil {
return fmt.Errorf("%w: %s value %s is not a valid range",
ErrHeaderRangeMalformed, name, headerRange)
}
}
default:
return fmt.Errorf("%w: %s value %s must be in the form n or n-m",
ErrHeaderRangeMalformed, name, headerRange)
}
}
return nil
}
@@ -1,51 +0,0 @@
package settings
import (
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type BoringPoll struct {
GluetunCom *bool
}
func (b BoringPoll) validate() error {
return nil
}
func (b BoringPoll) Copy() BoringPoll {
return BoringPoll{
GluetunCom: gosettings.CopyPointer(b.GluetunCom),
}
}
func (b *BoringPoll) overrideWith(other BoringPoll) {
b.GluetunCom = gosettings.OverrideWithPointer(b.GluetunCom, other.GluetunCom)
}
func (b *BoringPoll) setDefaults() {
b.GluetunCom = gosettings.DefaultPointer(b.GluetunCom, false)
}
func (b BoringPoll) String() string {
return b.toLinesNode().String()
}
func (b BoringPoll) toLinesNode() *gotree.Node {
if !*b.GluetunCom {
return nil
}
node := gotree.New("Boring-poll settings:")
node.Append("gluetun.com: on")
return node
}
func (b *BoringPoll) read(r *reader.Reader) (err error) {
b.GluetunCom, err = r.BoolPtr("BORINGPOLL_GLUETUNCOM")
if err != nil {
return err
}
return nil
}
@@ -14,10 +14,6 @@ func readObsolete(r *reader.Reader) (warnings []string) {
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.", "DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete", "HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete", "HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
"DNS_SERVER": "DNS_SERVER is obsolete because the forwarding server is always enabled.",
"DOT": "DOT is obsolete because the forwarding server is always enabled.",
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because the forwarding server is always used and " +
"forwards local names to private DNS resolvers found in /etc/resolv.conf",
} }
sortedKeys := maps.Keys(keyToMessage) sortedKeys := maps.Keys(keyToMessage)
slices.Sort(sortedKeys) slices.Sort(sortedKeys)
+78 -92
View File
@@ -13,25 +13,20 @@ import (
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
const (
DNSUpstreamTypeDot = "dot"
DNSUpstreamTypeDoh = "doh"
DNSUpstreamTypePlain = "plain"
)
// DNS contains settings to configure DNS. // DNS contains settings to configure DNS.
type DNS struct { type DNS struct {
// UpstreamType can be [DNSUpstreamTypeDot], [DNSUpstreamTypeDoh] // ServerEnabled is true if the server should be running
// or [DNSUpstreamTypePlain]. It defaults to [DNSUpstreamTypeDot]. // and used. It defaults to true, and cannot be nil
// in the internal state.
ServerEnabled *bool
// UpstreamType can be dot or plain, and defaults to dot.
UpstreamType string `json:"upstream_type"` UpstreamType string `json:"upstream_type"`
// UpdatePeriod is the period to update DNS block lists. // UpdatePeriod is the period to update DNS block lists.
// It can be set to 0 to disable the update. // It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in // It defaults to 24h and cannot be nil in
// the internal state. // the internal state.
UpdatePeriod *time.Duration UpdatePeriod *time.Duration
// Providers is a list of DNS providers. // Providers is a list of DNS providers
// It defaults to ["cloudflare"] and is ignored if the UpstreamType is
// [DNSUpstreamTypePlain] and the UpstreamPlainAddresses field is set.
Providers []string `json:"providers"` Providers []string `json:"providers"`
// Caching is true if the server should cache // Caching is true if the server should cache
// DNS responses. // DNS responses.
@@ -41,22 +36,32 @@ type DNS struct {
// Blacklist contains settings to configure the filter // Blacklist contains settings to configure the filter
// block lists. // block lists.
Blacklist DNSBlacklist Blacklist DNSBlacklist
// UpstreamPlainAddresses are the upstream plaintext DNS resolver // ServerAddress is the DNS server to use inside
// addresses to use by the built-in DNS server forwarder. // the Go program and for the system.
// Note, if the upstream type is [dnsUpstreamTypePlain] and this field is set, // It defaults to '127.0.0.1' to be used with the
// the Providers field is ignored. // local server. It cannot be the zero value in the internal
UpstreamPlainAddresses []netip.AddrPort // state.
ServerAddress netip.Addr
// KeepNameserver is true if the existing DNS server
// found in /etc/resolv.conf should be used
// Note setting this to true will likely DNS traffic
// outside the VPN tunnel since it would go through
// the local DNS server of your Docker/Kubernetes
// configuration, which is likely not going through the tunnel.
// This will also disable the DNS forwarder server and the
// `ServerAddress` field will be ignored.
// It defaults to false and cannot be nil in the
// internal state.
KeepNameserver *bool
} }
var ( var (
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid") ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short") ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
ErrDNSUpstreamPlainNoIPv6 = errors.New("upstream plain addresses do not contain any IPv6 address")
ErrDNSUpstreamPlainNoIPv4 = errors.New("upstream plain addresses do not contain any IPv4 address")
) )
func (d DNS) validate() (err error) { func (d DNS) validate() (err error) {
if !helpers.IsOneOf(d.UpstreamType, DNSUpstreamTypeDot, DNSUpstreamTypeDoh, DNSUpstreamTypePlain) { if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType) return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
} }
@@ -66,27 +71,13 @@ func (d DNS) validate() (err error) {
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod) ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
} }
if d.UpstreamType == DNSUpstreamTypePlain { providers := provider.NewProviders()
selectedHasPlainIPv4, selectedHasPlainIPv6 := false, false for _, providerName := range d.Providers {
for _, addrPort := range d.UpstreamPlainAddresses { _, err := providers.Get(providerName)
if !selectedHasPlainIPv4 && addrPort.Addr().Is4() { if err != nil {
selectedHasPlainIPv4 = true return err
}
if !selectedHasPlainIPv6 && addrPort.Addr().Is6() {
selectedHasPlainIPv6 = true
}
if selectedHasPlainIPv4 && selectedHasPlainIPv6 {
break
} }
} }
switch {
case *d.IPv6 && !selectedHasPlainIPv6:
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv6, len(d.UpstreamPlainAddresses))
case !*d.IPv6 && !selectedHasPlainIPv4:
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv4, len(d.UpstreamPlainAddresses))
}
}
// Note: all DNS built in providers have both IPv4 and IPv6 addresses for all modes
err = d.Blacklist.validate() err = d.Blacklist.validate()
if err != nil { if err != nil {
@@ -98,13 +89,15 @@ func (d DNS) validate() (err error) {
func (d *DNS) Copy() (copied DNS) { func (d *DNS) Copy() (copied DNS) {
return DNS{ return DNS{
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
UpstreamType: d.UpstreamType, UpstreamType: d.UpstreamType,
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod), UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers), Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching), Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6), IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(), Blacklist: d.Blacklist.copy(),
UpstreamPlainAddresses: gosettings.CopySlice(d.UpstreamPlainAddresses), ServerAddress: d.ServerAddress,
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
} }
} }
@@ -112,30 +105,48 @@ func (d *DNS) Copy() (copied DNS) {
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (d *DNS) overrideWith(other DNS) { func (d *DNS) overrideWith(other DNS) {
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType) d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod) d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers) d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching) d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6) d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist) d.Blacklist.overrideWith(other.Blacklist)
d.UpstreamPlainAddresses = gosettings.OverrideWithSlice(d.UpstreamPlainAddresses, other.UpstreamPlainAddresses) d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
} }
func (d *DNS) setDefaults() { func (d *DNS) setDefaults() {
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, DNSUpstreamTypeDot) d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
const defaultUpdatePeriod = 24 * time.Hour const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod) d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.UpstreamPlainAddresses = gosettings.DefaultSlice(d.UpstreamPlainAddresses, []netip.AddrPort{}) d.Providers = gosettings.DefaultSlice(d.Providers, []string{
d.Providers = gosettings.DefaultSlice(d.Providers, defaultDNSProviders()) provider.Cloudflare().Name,
})
d.Caching = gosettings.DefaultPointer(d.Caching, true) d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false) d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults() d.Blacklist.setDefaults()
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
} }
func defaultDNSProviders() []string { func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
return []string{ localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
provider.Cloudflare().Name, if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
return d.ServerAddress
} }
providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0])
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
return provider.Plain.IPv4[0].Addr()
} }
func (d DNS) String() string { func (d DNS) String() string {
@@ -144,26 +155,23 @@ func (d DNS) String() string {
func (d DNS) toLinesNode() (node *gotree.Node) { func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:") node = gotree.New("DNS settings:")
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
if *d.KeepNameserver {
return node
}
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled))
if !*d.ServerEnabled {
return node
}
node.Appendf("Upstream resolver type: %s", d.UpstreamType) node.Appendf("Upstream resolver type: %s", d.UpstreamType)
upstreamResolvers := node.Append("Upstream resolvers:") upstreamResolvers := node.Append("Upstream resolvers:")
if len(d.UpstreamPlainAddresses) > 0 {
if d.UpstreamType == DNSUpstreamTypePlain {
for _, addr := range d.UpstreamPlainAddresses {
upstreamResolvers.Append(addr.String())
}
} else {
node.Appendf("Upstream plain addresses: ignored because upstream type is not plain")
for _, provider := range d.Providers { for _, provider := range d.Providers {
upstreamResolvers.Append(provider) upstreamResolvers.Append(provider)
} }
}
} else {
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching)) node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6)) node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
@@ -180,6 +188,11 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
} }
func (d *DNS) read(r *reader.Reader) (err error) { func (d *DNS) read(r *reader.Reader) (err error) {
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
if err != nil {
return err
}
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE") d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD") d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
@@ -204,42 +217,15 @@ func (d *DNS) read(r *reader.Reader) (err error) {
return err return err
} }
err = d.readUpstreamPlainAddresses(r) d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
if err != nil {
return err
}
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (d *DNS) readUpstreamPlainAddresses(r *reader.Reader) (err error) {
// If DNS_UPSTREAM_PLAIN_ADDRESSES is set, the user must also set DNS_UPSTREAM_TYPE=plain
// for these to be used. This is an added safety measure to reduce misunderstandings, and
// reduce odd settings overrides.
d.UpstreamPlainAddresses, err = r.CSVNetipAddrPorts("DNS_UPSTREAM_PLAIN_ADDRESSES")
if err != nil {
return err
}
// Retro-compatibility - remove in v4
// If DNS_ADDRESS is set to a non-localhost address, append it to the other
// upstream plain addresses, assuming port 53, and force the upstream type to plain
// to maintain retro-compatibility behavior.
serverAddress, err := r.NetipAddr("DNS_ADDRESS",
reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"),
reader.IsRetro("DNS_UPSTREAM_PLAIN_ADDRESSES"))
if err != nil {
return err
} else if !serverAddress.IsValid() {
return nil
}
isLocalhost := serverAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) == 0
if isLocalhost {
return nil
}
const defaultPlainPort = 53
addrPort := netip.AddrPortFrom(serverAddress, defaultPlainPort)
d.UpstreamPlainAddresses = append(d.UpstreamPlainAddresses, addrPort)
d.UpstreamType = DNSUpstreamTypePlain
return nil
}
@@ -1,26 +0,0 @@
package settings
import (
"testing"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/stretchr/testify/require"
)
func Test_defaultDNSProviders(t *testing.T) {
t.Parallel()
names := defaultDNSProviders()
found := false
providers := provider.NewProviders()
for _, name := range names {
provider, err := providers.Get(name)
require.NoError(t, err)
if len(provider.Plain.IPv4) > 0 {
found = true
break
}
}
require.True(t, found, "no default DNS provider has a plaintext IPv4 address")
}
@@ -23,9 +23,7 @@ type DNSBlacklist struct {
AddBlockedIPs []netip.Addr AddBlockedIPs []netip.Addr
AddBlockedIPPrefixes []netip.Prefix AddBlockedIPPrefixes []netip.Prefix
// RebindingProtectionExemptHostnames is a list of hostnames // RebindingProtectionExemptHostnames is a list of hostnames
// exempt from DNS rebinding protection. It can contain parent // exempt from DNS rebinding protection.
// domains which are of the form "*.example.com". Note the wildcard
// can only be used at the start of the hostname.
RebindingProtectionExemptHostnames []string RebindingProtectionExemptHostnames []string
} }
@@ -57,9 +55,6 @@ func (b DNSBlacklist) validate() (err error) {
} }
for _, host := range b.RebindingProtectionExemptHostnames { for _, host := range b.RebindingProtectionExemptHostnames {
if len(host) > 2 && host[:2] == "*." {
host = host[2:]
}
if !hostRegex.MatchString(host) { if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host) return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
} }
+10 -13
View File
@@ -15,7 +15,7 @@ type Firewall struct {
InputPorts []uint16 InputPorts []uint16
OutboundSubnets []netip.Prefix OutboundSubnets []netip.Prefix
Enabled *bool Enabled *bool
Iptables Iptables Debug *bool
} }
func (f Firewall) validate() (err error) { func (f Firewall) validate() (err error) {
@@ -33,11 +33,6 @@ func (f Firewall) validate() (err error) {
} }
} }
err = f.Iptables.validate()
if err != nil {
return fmt.Errorf("iptables settings: %w", err)
}
return nil return nil
} }
@@ -56,7 +51,7 @@ func (f *Firewall) copy() (copied Firewall) {
InputPorts: gosettings.CopySlice(f.InputPorts), InputPorts: gosettings.CopySlice(f.InputPorts),
OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets), OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets),
Enabled: gosettings.CopyPointer(f.Enabled), Enabled: gosettings.CopyPointer(f.Enabled),
Iptables: f.Iptables.copy(), Debug: gosettings.CopyPointer(f.Debug),
} }
} }
@@ -68,12 +63,12 @@ func (f *Firewall) overrideWith(other Firewall) {
f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts) f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets) f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled) f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled)
f.Iptables.overrideWith(other.Iptables) f.Debug = gosettings.OverrideWithPointer(f.Debug, other.Debug)
} }
func (f *Firewall) setDefaults(globalLogLevel string) { func (f *Firewall) setDefaults() {
f.Enabled = gosettings.DefaultPointer(f.Enabled, true) f.Enabled = gosettings.DefaultPointer(f.Enabled, true)
f.Iptables.setDefaults(globalLogLevel) f.Debug = gosettings.DefaultPointer(f.Debug, false)
} }
func (f Firewall) String() string { func (f Firewall) String() string {
@@ -88,7 +83,9 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
return node return node
} }
node.AppendNode(f.Iptables.toLinesNode()) if *f.Debug {
node.Appendf("Debug mode: on")
}
if len(f.VPNInputPorts) > 0 { if len(f.VPNInputPorts) > 0 {
vpnInputPortsNode := node.Appendf("VPN input ports:") vpnInputPortsNode := node.Appendf("VPN input ports:")
@@ -136,9 +133,9 @@ func (f *Firewall) read(r *reader.Reader) (err error) {
return err return err
} }
err = f.Iptables.read(r) f.Debug, err = r.BoolPtr("FIREWALL_DEBUG")
if err != nil { if err != nil {
return fmt.Errorf("reading iptables settings: %w", err) return err
} }
return nil return nil
@@ -4,7 +4,6 @@ import (
"net/netip" "net/netip"
"testing" "testing"
"github.com/qdm12/log"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -16,10 +15,7 @@ func Test_Firewall_validate(t *testing.T) {
errWrapped error errWrapped error
errMessage string errMessage string
}{ }{
"empty": { "empty": {},
errWrapped: log.ErrLevelNotRecognized,
errMessage: "iptables settings: log level: level is not recognized: ",
},
"zero_vpn_input_port": { "zero_vpn_input_port": {
firewall: Firewall{ firewall: Firewall{
VPNInputPorts: []uint16{0}, VPNInputPorts: []uint16{0},
@@ -45,7 +41,6 @@ func Test_Firewall_validate(t *testing.T) {
}, },
"public_outbound_subnet": { "public_outbound_subnet": {
firewall: Firewall{ firewall: Firewall{
Iptables: Iptables{LogLevel: log.LevelInfo.String()},
OutboundSubnets: []netip.Prefix{ OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("1.2.3.4/32"), netip.MustParsePrefix("1.2.3.4/32"),
}, },
@@ -53,7 +48,6 @@ func Test_Firewall_validate(t *testing.T) {
}, },
"valid_settings": { "valid_settings": {
firewall: Firewall{ firewall: Firewall{
Iptables: Iptables{LogLevel: log.LevelInfo.String()},
VPNInputPorts: []uint16{100, 101}, VPNInputPorts: []uint16{100, 101},
InputPorts: []uint16{200, 201}, InputPorts: []uint16{200, 201},
OutboundSubnets: []netip.Prefix{ OutboundSubnets: []netip.Prefix{
@@ -1,67 +0,0 @@
package settings
import (
"fmt"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
"github.com/qdm12/log"
)
// Iptables contains settings to customize iptables.
type Iptables struct {
LogLevel string
}
func (i Iptables) validate() (err error) {
_, err = log.ParseLevel(i.LogLevel)
if err != nil {
return fmt.Errorf("log level: %w", err)
}
return nil
}
func (i *Iptables) copy() (copied Iptables) {
return Iptables{
LogLevel: i.LogLevel,
}
}
func (i *Iptables) overrideWith(other Iptables) {
i.LogLevel = gosettings.OverrideWithComparable(i.LogLevel, other.LogLevel)
}
func (i *Iptables) setDefaults(globalLogLevel string) {
defaultLevel := globalLogLevel
if defaultLevel == log.LevelDebug.String() {
// Given iptables debug logger is quite verbose, we only turn it to debug level
// if it is explicitly asked to be at debug level; even if the global logger is
// at the debug level, we keep iptables at info level by default.
defaultLevel = log.LevelInfo.String()
}
i.LogLevel = gosettings.DefaultComparable(i.LogLevel, defaultLevel)
}
func (i Iptables) String() string {
return i.toLinesNode().String()
}
func (i Iptables) toLinesNode() (node *gotree.Node) {
node = gotree.New("Iptables settings:")
node.Appendf("Log level: %s", i.LogLevel)
return node
}
func (i *Iptables) read(r *reader.Reader) (err error) {
debugMode, err := r.BoolPtr("FIREWALL_DEBUG", reader.IsRetro("FIREWALL_IPTABLES_LOG_LEVEL"))
if err != nil {
return err
}
if debugMode != nil && *debugMode {
i.LogLevel = log.LevelDebug.String()
}
i.LogLevel = r.String("FIREWALL_IPTABLES_LOG_LEVEL")
return nil
}
-58
View File
@@ -1,58 +0,0 @@
package settings
import (
"net/netip"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// IPv6 contains settings regarding IPv6 configuration.
type IPv6 struct {
// CheckAddresses are the TCP ip:port addresses to dial to check if
// IPv6 is supported, in case a default IPv6 route is found.
// It defaults to google and cloudflare IPv6 anycast addresses
// [2001:4860:4860::8888]:53,[2606:4700:4700::1111]:53
CheckAddresses []netip.AddrPort
}
func (i IPv6) validate() (err error) {
return nil
}
func (i *IPv6) copy() (copied IPv6) {
return IPv6{
CheckAddresses: gosettings.CopySlice(i.CheckAddresses),
}
}
func (i *IPv6) overrideWith(other IPv6) {
i.CheckAddresses = gosettings.OverrideWithSlice(i.CheckAddresses, other.CheckAddresses)
}
func (i *IPv6) setDefaults() {
defaultCheckAddresses := []netip.AddrPort{
netip.MustParseAddrPort("[2001:4860:4860::8888]:53"),
netip.MustParseAddrPort("[2606:4700:4700::1111]:53"),
}
i.CheckAddresses = gosettings.DefaultSlice(i.CheckAddresses, defaultCheckAddresses)
}
func (i IPv6) String() string {
return i.toLinesNode().String()
}
func (i IPv6) toLinesNode() (node *gotree.Node) {
node = gotree.New("IPv6 settings:")
addrsNode := node.Appendf("Check addresses:")
for _, addr := range i.CheckAddresses {
addrsNode.Append(addr.String())
}
return node
}
func (i *IPv6) read(r *reader.Reader) (err error) {
i.CheckAddresses, err = r.CSVNetipAddrPorts("IPV6_CHECK_ADDRESSES")
return err
}
@@ -60,6 +60,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
providers.Giganews, providers.Giganews,
providers.Ipvanish, providers.Ipvanish,
providers.Perfectprivacy, providers.Perfectprivacy,
providers.Privado,
providers.Vyprvpn, providers.Vyprvpn,
) { ) {
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
@@ -74,8 +75,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
providers.Privatevpn, providers.Torguard: providers.Privatevpn, providers.Torguard:
// no custom port allowed // no custom port allowed
case providers.Expressvpn, providers.Fastestvpn, case providers.Expressvpn, providers.Fastestvpn,
providers.Giganews, providers.Ipvanish, providers.Giganews, providers.Ipvanish, providers.Nordvpn,
providers.Nordvpn, providers.Purevpn, providers.Privado, providers.Purevpn,
providers.Surfshark, providers.VPNSecure, providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn: providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
@@ -98,9 +99,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
case providers.Perfectprivacy: case providers.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433} allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433} allowedUDP = []uint16{44, 443, 4433}
case providers.Privado:
allowedTCP = []uint16{443, 1194, 8080, 8443}
allowedUDP = []uint16{443, 1194, 8080, 8443}
case providers.PrivateInternetAccess: case providers.PrivateInternetAccess:
allowedTCP = []uint16{80, 110, 443} allowedTCP = []uint16{80, 110, 443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201} allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
@@ -132,6 +130,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate EncPreset // Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess { if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{ validEncryptionPresets := []string{
presets.None,
presets.Normal, presets.Normal,
presets.Strong, presets.Strong,
} }
-111
View File
@@ -1,111 +0,0 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// PMTUD contains settings to configure Path MTU Discovery.
type PMTUD struct {
// ICMPAddresses is the redundancy list of addresses to use
// for ICMP path MTU discovery. Each address MUST handle ICMP
// packets for PMTUD to work.
// It cannot be nil in the internal state.
ICMPAddresses []netip.Addr `json:"icmp_addresses"`
// TCPAddresses is the redundancy list of addresses to use
// for TCP path MTU discovery. Each address MUST have a listening
// TCP server on the port specified.
// It cannot be nil in the internal state.
TCPAddresses []netip.AddrPort `json:"tcp_addresses"`
}
var (
ErrPMTUDICMPAddressNotValid = errors.New("PMTUD ICMP address is not valid")
ErrPMTUDTCPAddressNotValid = errors.New("PMTUD TCP address is not valid")
)
// Validate validates PMTUD settings.
func (p PMTUD) validate() (err error) {
for i, addr := range p.ICMPAddresses {
if !addr.IsValid() {
return fmt.Errorf("%w: at index %d", ErrPMTUDICMPAddressNotValid, i)
}
}
for i, addr := range p.TCPAddresses {
if !addr.IsValid() {
return fmt.Errorf("%w: at index %d", ErrPMTUDTCPAddressNotValid, i)
}
}
return nil
}
func (p *PMTUD) copy() (copied PMTUD) {
return PMTUD{
ICMPAddresses: gosettings.CopySlice(p.ICMPAddresses),
TCPAddresses: gosettings.CopySlice(p.TCPAddresses),
}
}
func (p *PMTUD) overrideWith(other PMTUD) {
p.ICMPAddresses = gosettings.OverrideWithSlice(p.ICMPAddresses, other.ICMPAddresses)
p.TCPAddresses = gosettings.OverrideWithSlice(p.TCPAddresses, other.TCPAddresses)
}
func (p *PMTUD) setDefaults() {
defaultICMPAddresses := []netip.Addr{
netip.AddrFrom4([4]byte{1, 1, 1, 1}),
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
}
p.ICMPAddresses = gosettings.DefaultSlice(p.ICMPAddresses, defaultICMPAddresses)
const dnsPort, tlsPort = 53, 443
defaultTCPAddresses := []netip.AddrPort{
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), dnsPort),
netip.AddrPortFrom(netip.AddrFrom4([4]byte{8, 8, 8, 8}), dnsPort),
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), tlsPort),
netip.AddrPortFrom(netip.AddrFrom4([4]byte{8, 8, 8, 8}), tlsPort),
netip.AddrPortFrom(netip.MustParseAddr("2606:4700:4700::1111"), dnsPort),
netip.AddrPortFrom(netip.MustParseAddr("2001:4860:4860::8888"), dnsPort),
netip.AddrPortFrom(netip.MustParseAddr("2606:4700:4700::1111"), tlsPort),
netip.AddrPortFrom(netip.MustParseAddr("2001:4860:4860::8888"), tlsPort),
}
p.TCPAddresses = gosettings.DefaultSlice(p.TCPAddresses, defaultTCPAddresses)
}
func (p PMTUD) String() string {
return p.toLinesNode().String()
}
func (p PMTUD) toLinesNode() (node *gotree.Node) {
node = gotree.New("Path MTU discovery:")
icmpAddrNode := node.Append("ICMP addresses:")
for _, addr := range p.ICMPAddresses {
icmpAddrNode.Append(addr.String())
}
tcpAddrNode := node.Append("TCP addresses:")
for _, addr := range p.TCPAddresses {
tcpAddrNode.Append(addr.String())
}
return node
}
func (p *PMTUD) read(r *reader.Reader) (err error) {
p.ICMPAddresses, err = r.CSVNetipAddresses("PMTUD_ICMP_ADDRESSES")
if err != nil {
return err
}
p.TCPAddresses, err = r.CSVNetipAddrPorts("PMTUD_TCP_ADDRESSES")
if err != nil {
return err
}
return nil
}
+13 -60
View File
@@ -1,10 +1,8 @@
package settings package settings
import ( import (
"errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"slices"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
@@ -39,28 +37,16 @@ type PortForwarding struct {
// It can be the empty string to indicate to NOT run a command. // It can be the empty string to indicate to NOT run a command.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
DownCommand *string `json:"down_command"` DownCommand *string `json:"down_command"`
// ListeningPorts are the ports traffic would be redirected to from the // ListeningPort is the port traffic would be redirected to from the
// forwarded ports. The redirection is disabled if it is the slice [0], // forwarded port. The redirection is disabled if it is set to 0, which
// which is its default as well. If set and not [0], its length must match // is its default as well.
// the PortsCount value, such that each forwarded port is redirected to ListeningPort *uint16 `json:"listening_port"`
// the corresponding listening port.
ListeningPorts []uint16 `json:"listening_port"`
// PortsCount is the number of ports to forward. It is optional for ProtonVPN
// and be between 1 and 5. For other providers, it must be set to 1 if port
// forwarding is enabled.
PortsCount uint16 `json:"ports_count"`
// Username is only used for Private Internet Access port forwarding. // Username is only used for Private Internet Access port forwarding.
Username string `json:"username"` Username string `json:"username"`
// Password is only used for Private Internet Access port forwarding. // Password is only used for Private Internet Access port forwarding.
Password string `json:"password"` Password string `json:"password"`
} }
var (
ErrPortsCountTooHigh = errors.New("ports count too high")
ErrListeningPortsLen = errors.New("listening ports length must be equal to ports count")
ErrListeningPortZero = errors.New("listening port cannot be 0")
)
func (p PortForwarding) Validate(vpnProvider string) (err error) { func (p PortForwarding) Validate(vpnProvider string) (err error) {
if !*p.Enabled { if !*p.Enabled {
return nil return nil
@@ -89,36 +75,13 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
} }
} }
switch providerSelected { if providerSelected == providers.PrivateInternetAccess {
case providers.PrivateInternetAccess:
const maxPortsCount = 1
switch { switch {
case p.PortsCount > maxPortsCount:
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
case p.Username == "": case p.Username == "":
return fmt.Errorf("%w", ErrPortForwardingUserEmpty) return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
case p.Password == "": case p.Password == "":
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty) return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
} }
case providers.Protonvpn:
const maxPortsCount = 4
if p.PortsCount > maxPortsCount {
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
}
default:
const maxPortsCount = 1
if p.PortsCount > maxPortsCount {
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
}
}
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
switch {
case len(p.ListeningPorts) != int(p.PortsCount):
return fmt.Errorf("%w: %d != %d", ErrListeningPortsLen, len(p.ListeningPorts), p.PortsCount)
case slices.Contains(p.ListeningPorts, 0):
return fmt.Errorf("%w: in %v", ErrListeningPortZero, p.ListeningPorts)
}
} }
return nil return nil
@@ -131,7 +94,7 @@ func (p *PortForwarding) Copy() (copied PortForwarding) {
Filepath: gosettings.CopyPointer(p.Filepath), Filepath: gosettings.CopyPointer(p.Filepath),
UpCommand: gosettings.CopyPointer(p.UpCommand), UpCommand: gosettings.CopyPointer(p.UpCommand),
DownCommand: gosettings.CopyPointer(p.DownCommand), DownCommand: gosettings.CopyPointer(p.DownCommand),
ListeningPorts: gosettings.CopySlice(p.ListeningPorts), ListeningPort: gosettings.CopyPointer(p.ListeningPort),
Username: p.Username, Username: p.Username,
Password: p.Password, Password: p.Password,
} }
@@ -143,7 +106,7 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath) p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand) p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand) p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
p.ListeningPorts = gosettings.OverrideWithSlice(p.ListeningPorts, other.ListeningPorts) p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username) p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password) p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
} }
@@ -154,8 +117,7 @@ func (p *PortForwarding) setDefaults() {
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port") p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "") p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "") p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
p.ListeningPorts = gosettings.DefaultSlice(p.ListeningPorts, []uint16{0}) // disabled p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, 1)
} }
func (p PortForwarding) String() string { func (p PortForwarding) String() string {
@@ -169,14 +131,11 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
node = gotree.New("Automatic port forwarding settings:") node = gotree.New("Automatic port forwarding settings:")
node.Appendf("Number of ports to be forwarded: %d", p.PortsCount) listeningPort := "disabled"
if *p.ListeningPort != 0 {
if !slices.Equal(p.ListeningPorts, []uint16{0}) { listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
redirNode := node.Appendf("Redirection for listening ports:")
for i, port := range p.ListeningPorts {
redirNode.Appendf("Port #%d -> %d", i+1, port)
}
} }
node.Appendf("Redirection listening port: %s", listeningPort)
if *p.Provider == "" { if *p.Provider == "" {
node.Appendf("Use port forwarding code for current provider") node.Appendf("Use port forwarding code for current provider")
@@ -231,13 +190,7 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND", p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
reader.ForceLowercase(false)) reader.ForceLowercase(false))
p.ListeningPorts, err = r.CSVUint16("VPN_PORT_FORWARDING_LISTENING_PORTS", p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
reader.RetroKeys("VPN_PORT_FORWARDING_LISTENING_PORT"))
if err != nil {
return err
}
p.PortsCount, err = r.Uint16("VPN_PORT_FORWARDING_PORTS_COUNT")
if err != nil { if err != nil {
return err return err
} }
+3 -13
View File
@@ -2,8 +2,6 @@ package settings
import ( import (
"fmt" "fmt"
"slices"
"sort"
"strings" "strings"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
@@ -30,18 +28,10 @@ type Provider struct {
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) { func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
// Validate Name // Validate Name
var validNames []string var validNames []string
switch vpnType { if vpnType == vpn.OpenVPN {
case vpn.AmneziaWg:
validNames = []string{providers.Custom}
case vpn.OpenVPN:
validNames = providers.AllWithCustom() validNames = providers.AllWithCustom()
validNames = append(validNames, "pia") // Retro-compatibility validNames = append(validNames, "pia") // Retro-compatibility
// Remove Mullvad since it no longer supports OpenVPN as of January 15th, 2026 } else { // Wireguard
mullvadIndex := slices.Index(validNames, providers.Mullvad)
validNames[mullvadIndex], validNames[len(validNames)-1] = validNames[len(validNames)-1], validNames[mullvadIndex]
validNames = validNames[:len(validNames)-1]
sort.Strings(validNames)
case vpn.Wireguard:
validNames = []string{ validNames = []string{
providers.Airvpn, providers.Airvpn,
providers.Custom, providers.Custom,
@@ -55,7 +45,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
} }
} }
if err = validate.IsOneOf(p.Name, validNames...); err != nil { if err = validate.IsOneOf(p.Name, validNames...); err != nil {
return fmt.Errorf("%w for %s: %w", ErrVPNProviderNameNotValid, vpnType, err) return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
} }
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner) err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
@@ -87,7 +87,7 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
filterChoicesGetter FilterChoicesGetter, warner Warner, filterChoicesGetter FilterChoicesGetter, warner Warner,
) (err error) { ) (err error) {
switch ss.VPN { switch ss.VPN {
case vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard: case vpn.OpenVPN, vpn.Wireguard:
default: default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN) return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
} }
@@ -518,8 +518,7 @@ func (ss *ServerSelection) read(r *reader.Reader,
return err return err
} }
amneziawg := ss.VPN == vpn.AmneziaWg err = ss.Wireguard.read(r)
err = ss.Wireguard.read(r, amneziawg)
if err != nil { if err != nil {
return err return err
} }
+9 -21
View File
@@ -2,6 +2,7 @@ package settings
import ( import (
"fmt" "fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
@@ -26,9 +27,7 @@ type Settings struct {
Updater Updater Updater Updater
Version Version Version Version
VPN VPN VPN VPN
IPv6 IPv6
Pprof pprof.Settings Pprof pprof.Settings
BoringPoll BoringPoll
} }
type FilterChoicesGetter interface { type FilterChoicesGetter interface {
@@ -54,12 +53,10 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
"system": s.System.validate, "system": s.System.validate,
"updater": s.Updater.Validate, "updater": s.Updater.Validate,
"version": s.Version.validate, "version": s.Version.validate,
"ipv6": s.IPv6.validate,
// Pprof validation done in pprof constructor // Pprof validation done in pprof constructor
"VPN": func() error { "VPN": func() error {
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner) return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
}, },
"boring poll": s.BoringPoll.validate,
} }
for name, validation := range nameToValidation { for name, validation := range nameToValidation {
@@ -88,8 +85,6 @@ func (s *Settings) copy() (copied Settings) {
Version: s.Version.copy(), Version: s.Version.copy(),
VPN: s.VPN.Copy(), VPN: s.VPN.Copy(),
Pprof: s.Pprof.Copy(), Pprof: s.Pprof.Copy(),
BoringPoll: s.BoringPoll.Copy(),
IPv6: s.IPv6.copy(),
} }
} }
@@ -111,8 +106,6 @@ func (s *Settings) OverrideWith(other Settings,
patchedSettings.Version.overrideWith(other.Version) patchedSettings.Version.overrideWith(other.Version)
patchedSettings.VPN.OverrideWith(other.VPN) patchedSettings.VPN.OverrideWith(other.VPN)
patchedSettings.Pprof.OverrideWith(other.Pprof) patchedSettings.Pprof.OverrideWith(other.Pprof)
patchedSettings.BoringPoll.overrideWith(other.BoringPoll)
patchedSettings.IPv6.overrideWith(other.IPv6)
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner) err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
if err != nil { if err != nil {
return err return err
@@ -124,12 +117,10 @@ func (s *Settings) OverrideWith(other Settings,
func (s *Settings) SetDefaults() { func (s *Settings) SetDefaults() {
s.ControlServer.setDefaults() s.ControlServer.setDefaults()
s.DNS.setDefaults() s.DNS.setDefaults()
s.Log.setDefaults() s.Firewall.setDefaults()
s.Firewall.setDefaults(s.Log.Level)
s.Health.SetDefaults() s.Health.SetDefaults()
s.HTTPProxy.setDefaults() s.HTTPProxy.setDefaults()
s.Log.setDefaults() s.Log.setDefaults()
s.IPv6.setDefaults()
s.PublicIP.setDefaults() s.PublicIP.setDefaults()
s.Shadowsocks.setDefaults() s.Shadowsocks.setDefaults()
s.Storage.setDefaults() s.Storage.setDefaults()
@@ -138,7 +129,6 @@ func (s *Settings) SetDefaults() {
s.VPN.setDefaults() s.VPN.setDefaults()
s.Updater.SetDefaults(s.VPN.Provider.Name) s.Updater.SetDefaults(s.VPN.Provider.Name)
s.Pprof.SetDefaults() s.Pprof.SetDefaults()
s.BoringPoll.setDefaults()
} }
func (s Settings) String() string { func (s Settings) String() string {
@@ -152,7 +142,6 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.DNS.toLinesNode()) node.AppendNode(s.DNS.toLinesNode())
node.AppendNode(s.Firewall.toLinesNode()) node.AppendNode(s.Firewall.toLinesNode())
node.AppendNode(s.Log.toLinesNode()) node.AppendNode(s.Log.toLinesNode())
node.AppendNode(s.IPv6.toLinesNode())
node.AppendNode(s.Health.toLinesNode()) node.AppendNode(s.Health.toLinesNode())
node.AppendNode(s.Shadowsocks.toLinesNode()) node.AppendNode(s.Shadowsocks.toLinesNode())
node.AppendNode(s.HTTPProxy.toLinesNode()) node.AppendNode(s.HTTPProxy.toLinesNode())
@@ -163,7 +152,6 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.Updater.toLinesNode()) node.AppendNode(s.Updater.toLinesNode())
node.AppendNode(s.Version.toLinesNode()) node.AppendNode(s.Version.toLinesNode())
node.AppendNode(s.Pprof.ToLinesNode()) node.AppendNode(s.Pprof.ToLinesNode())
node.AppendNode(s.BoringPoll.toLinesNode())
return node return node
} }
@@ -186,11 +174,13 @@ func (s Settings) Warnings() (warnings []string) {
"by creating an issue, attaching the new certificate and we will update Gluetun.") "by creating an issue, attaching the new certificate and we will update Gluetun.")
} }
for _, upstreamAddress := range s.DNS.UpstreamPlainAddresses { // TODO remove in v4
if upstreamAddress.Addr().IsPrivate() { if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
warnings = append(warnings, "DNS upstream address "+upstreamAddress.String()+" is private: "+ warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
"DNS traffic might leak out of the VPN tunnel to that address.") " so the local forwarding DNS server will not be used."+
} " The default value changed to 127.0.0.1 so it uses the internal DNS server."+
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
" corresponding to the first DNS provider chosen is used.")
} }
return warnings return warnings
@@ -218,9 +208,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
"updater": s.Updater.read, "updater": s.Updater.read,
"version": s.Version.read, "version": s.Version.read,
"VPN": s.VPN.read, "VPN": s.VPN.read,
"IPv6": s.IPv6.read,
"profiling": s.Pprof.Read, "profiling": s.Pprof.Read,
"boring poll": s.BoringPoll.read,
} }
for name, read := range readFunctions { for name, read := range readFunctions {
@@ -29,28 +29,18 @@ func Test_Settings_String(t *testing.T) {
| | OpenVPN server selection settings: | | OpenVPN server selection settings:
| | Protocol: UDP | | Protocol: UDP
| | Private Internet Access encryption preset: strong | | Private Internet Access encryption preset: strong
| OpenVPN settings: | OpenVPN settings:
| | OpenVPN version: 2.6 | OpenVPN version: 2.6
| | User: [not set] | User: [not set]
| | Password: [not set] | Password: [not set]
| | Private Internet Access encryption preset: strong | Private Internet Access encryption preset: strong
| | Network interface: tun0 | Network interface: tun0
| | Run OpenVPN as: root | Run OpenVPN as: root
| | Verbosity level: 1 | Verbosity level: 1
| Path MTU discovery:
| ICMP addresses:
| | 1.1.1.1
| | 8.8.8.8
| TCP addresses:
| 1.1.1.1:53
| 8.8.8.8:53
| 1.1.1.1:443
| 8.8.8.8:443
| [2606:4700:4700::1111]:53
| [2001:4860:4860::8888]:53
| [2606:4700:4700::1111]:443
| [2001:4860:4860::8888]:443
DNS settings: DNS settings:
| Keep existing nameserver(s): no
| DNS server address to use: 127.0.0.1
| DNS forwarder server enabled: yes
| Upstream resolver type: dot | Upstream resolver type: dot
| Upstream resolvers: | Upstream resolvers:
| | Cloudflare | | Cloudflare
@@ -62,15 +52,9 @@ func Test_Settings_String(t *testing.T) {
| Block ads: no | Block ads: no
| Block surveillance: yes | Block surveillance: yes
Firewall settings: Firewall settings:
| Enabled: yes | Enabled: yes
| Iptables settings:
| Log level: INFO
Log settings: Log settings:
| Log level: INFO | Log level: INFO
IPv6 settings:
| Check addresses:
| [2001:4860:4860::8888]:53
| [2606:4700:4700::1111]:53
Health settings: Health settings:
| Server listening address: 127.0.0.1:9999 | Server listening address: 127.0.0.1:9999
| Target addresses: | Target addresses:
@@ -21,6 +21,10 @@ type Updater struct {
// updater. It cannot be nil in the internal state. // updater. It cannot be nil in the internal state.
// TODO change to value and add Enabled field. // TODO change to value and add Enabled field.
Period *time.Duration Period *time.Duration
// DNSAddress is the DNS server address to use
// to resolve VPN server hostnames to IP addresses.
// It cannot be the empty string in the internal state.
DNSAddress string
// MinRatio is the minimum ratio of servers to // MinRatio is the minimum ratio of servers to
// find per provider, compared to the total current // find per provider, compared to the total current
// number of servers. It defaults to 0.8. // number of servers. It defaults to 0.8.
@@ -72,6 +76,7 @@ func (u Updater) Validate() (err error) {
func (u *Updater) copy() (copied Updater) { func (u *Updater) copy() (copied Updater) {
return Updater{ return Updater{
Period: gosettings.CopyPointer(u.Period), Period: gosettings.CopyPointer(u.Period),
DNSAddress: u.DNSAddress,
MinRatio: u.MinRatio, MinRatio: u.MinRatio,
Providers: gosettings.CopySlice(u.Providers), Providers: gosettings.CopySlice(u.Providers),
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail), ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
@@ -84,6 +89,7 @@ func (u *Updater) copy() (copied Updater) {
// settings. // settings.
func (u *Updater) overrideWith(other Updater) { func (u *Updater) overrideWith(other Updater) {
u.Period = gosettings.OverrideWithPointer(u.Period, other.Period) u.Period = gosettings.OverrideWithPointer(u.Period, other.Period)
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio) u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers) u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail) u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
@@ -92,6 +98,7 @@ func (u *Updater) overrideWith(other Updater) {
func (u *Updater) SetDefaults(vpnProvider string) { func (u *Updater) SetDefaults(vpnProvider string) {
u.Period = gosettings.DefaultPointer(u.Period, 0) u.Period = gosettings.DefaultPointer(u.Period, 0)
u.DNSAddress = gosettings.DefaultComparable(u.DNSAddress, "1.1.1.1:53")
if u.MinRatio == 0 { if u.MinRatio == 0 {
const defaultMinRatio = 0.8 const defaultMinRatio = 0.8
@@ -118,6 +125,7 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server data updater settings:") node = gotree.New("Server data updater settings:")
node.Appendf("Update period: %s", *u.Period) node.Appendf("Update period: %s", *u.Period)
node.Appendf("DNS address: %s", u.DNSAddress)
node.Appendf("Minimum ratio: %.1f", u.MinRatio) node.Appendf("Minimum ratio: %.1f", u.MinRatio)
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", ")) node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
if slices.Contains(u.Providers, providers.Protonvpn) { if slices.Contains(u.Providers, providers.Protonvpn) {
@@ -134,6 +142,11 @@ func (u *Updater) read(r *reader.Reader) (err error) {
return err return err
} }
u.DNSAddress, err = readUpdaterDNSAddress()
if err != nil {
return err
}
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO") u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
if err != nil { if err != nil {
return err return err
@@ -153,3 +166,12 @@ func (u *Updater) read(r *reader.Reader) (err error) {
return nil return nil
} }
func readUpdaterDNSAddress() (address string, err error) {
// TODO this is currently using Cloudflare in
// plaintext to not be blocked by DNS over TLS by default.
// If a plaintext address is set in the DNS settings, this one will be used.
// use custom future encrypted DNS written in Go without blocking
// as it's too much trouble to start another parallel unbound instance for now.
return "", nil
}
+7 -69
View File
@@ -16,26 +16,14 @@ type VPN struct {
// empty string in the internal state. // empty string in the internal state.
Type string `json:"type"` Type string `json:"type"`
Provider Provider `json:"provider"` Provider Provider `json:"provider"`
AmneziaWg AmneziaWg `json:"amneziawg"`
OpenVPN OpenVPN `json:"openvpn"` OpenVPN OpenVPN `json:"openvpn"`
Wireguard Wireguard `json:"wireguard"` Wireguard Wireguard `json:"wireguard"`
PMTUD PMTUD `json:"pmtud"`
// UpCommand is the command to use when the VPN connection is up.
// It can be the empty string to indicate not to run a command.
// It cannot be nil in the internal state.
UpCommand *string `json:"up_command"`
// DownCommand is the command to use after the VPN connection goes down.
// It can be the empty string to indicate to NOT run a command.
// It cannot be nil in the internal state.
DownCommand *string `json:"down_command"`
} }
// Validate validates VPN settings, using the filter choices getter (aka servers data storage),
// and if IPv6 is supported or not.
// TODO v4 remove pointer for receiver (because of Surfshark). // TODO v4 remove pointer for receiver (because of Surfshark).
func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) { func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) {
// Validate Type // Validate Type
validVPNTypes := []string{vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard} validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil { if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err) return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err)
} }
@@ -45,30 +33,18 @@ func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bo
return fmt.Errorf("provider settings: %w", err) return fmt.Errorf("provider settings: %w", err)
} }
switch v.Type { if v.Type == vpn.OpenVPN {
case vpn.AmneziaWg:
err = v.AmneziaWg.validate(v.Provider.Name, ipv6Supported)
if err != nil {
return fmt.Errorf("AmneziaWG settings: %w", err)
}
case vpn.OpenVPN:
err := v.OpenVPN.validate(v.Provider.Name) err := v.OpenVPN.validate(v.Provider.Name)
if err != nil { if err != nil {
return fmt.Errorf("OpenVPN settings: %w", err) return fmt.Errorf("OpenVPN settings: %w", err)
} }
case vpn.Wireguard: } else {
const amneziawg = false err := v.Wireguard.validate(v.Provider.Name, ipv6Supported)
err := v.Wireguard.validate(v.Provider.Name, ipv6Supported, amneziawg)
if err != nil { if err != nil {
return fmt.Errorf("Wireguard settings: %w", err) return fmt.Errorf("Wireguard settings: %w", err)
} }
} }
err = v.PMTUD.validate()
if err != nil {
return fmt.Errorf("PMTUD settings: %w", err)
}
return nil return nil
} }
@@ -76,35 +52,23 @@ func (v *VPN) Copy() (copied VPN) {
return VPN{ return VPN{
Type: v.Type, Type: v.Type,
Provider: v.Provider.copy(), Provider: v.Provider.copy(),
AmneziaWg: v.AmneziaWg.copy(),
OpenVPN: v.OpenVPN.copy(), OpenVPN: v.OpenVPN.copy(),
Wireguard: v.Wireguard.copy(), Wireguard: v.Wireguard.copy(),
PMTUD: v.PMTUD.copy(),
UpCommand: gosettings.CopyPointer(v.UpCommand),
DownCommand: gosettings.CopyPointer(v.DownCommand),
} }
} }
func (v *VPN) OverrideWith(other VPN) { func (v *VPN) OverrideWith(other VPN) {
v.Type = gosettings.OverrideWithComparable(v.Type, other.Type) v.Type = gosettings.OverrideWithComparable(v.Type, other.Type)
v.Provider.overrideWith(other.Provider) v.Provider.overrideWith(other.Provider)
v.AmneziaWg.overrideWith(other.AmneziaWg)
v.OpenVPN.overrideWith(other.OpenVPN) v.OpenVPN.overrideWith(other.OpenVPN)
v.Wireguard.overrideWith(other.Wireguard) v.Wireguard.overrideWith(other.Wireguard)
v.PMTUD.overrideWith(other.PMTUD)
v.UpCommand = gosettings.OverrideWithPointer(v.UpCommand, other.UpCommand)
v.DownCommand = gosettings.OverrideWithPointer(v.DownCommand, other.DownCommand)
} }
func (v *VPN) setDefaults() { func (v *VPN) setDefaults() {
v.Type = gosettings.DefaultComparable(v.Type, vpn.OpenVPN) v.Type = gosettings.DefaultComparable(v.Type, vpn.OpenVPN)
v.Provider.setDefaults() v.Provider.setDefaults()
v.AmneziaWg.setDefaults(v.Provider.Name)
v.OpenVPN.setDefaults(v.Provider.Name) v.OpenVPN.setDefaults(v.Provider.Name)
v.Wireguard.setDefaults(v.Provider.Name) v.Wireguard.setDefaults(v.Provider.Name)
v.PMTUD.setDefaults()
v.UpCommand = gosettings.DefaultPointer(v.UpCommand, "")
v.DownCommand = gosettings.DefaultPointer(v.DownCommand, "")
} }
func (v VPN) String() string { func (v VPN) String() string {
@@ -116,22 +80,11 @@ func (v VPN) toLinesNode() (node *gotree.Node) {
node.AppendNode(v.Provider.toLinesNode()) node.AppendNode(v.Provider.toLinesNode())
switch v.Type { if v.Type == vpn.OpenVPN {
case vpn.AmneziaWg:
node.AppendNode(v.AmneziaWg.toLinesNode())
case vpn.OpenVPN:
node.AppendNode(v.OpenVPN.toLinesNode()) node.AppendNode(v.OpenVPN.toLinesNode())
case vpn.Wireguard: } else {
node.AppendNode(v.Wireguard.toLinesNode()) node.AppendNode(v.Wireguard.toLinesNode())
} }
node.AppendNode(v.PMTUD.toLinesNode())
if *v.UpCommand != "" {
node.Appendf("Up command: %s", *v.UpCommand)
}
if *v.DownCommand != "" {
node.Appendf("Down command: %s", *v.DownCommand)
}
return node return node
} }
@@ -144,30 +97,15 @@ func (v *VPN) read(r *reader.Reader) (err error) {
return fmt.Errorf("VPN provider: %w", err) return fmt.Errorf("VPN provider: %w", err)
} }
err = v.AmneziaWg.read(r)
if err != nil {
return fmt.Errorf("AmneziaWG: %w", err)
}
err = v.OpenVPN.read(r) err = v.OpenVPN.read(r)
if err != nil { if err != nil {
return fmt.Errorf("OpenVPN: %w", err) return fmt.Errorf("OpenVPN: %w", err)
} }
const amneziawg = false err = v.Wireguard.read(r)
err = v.Wireguard.read(r, amneziawg)
if err != nil { if err != nil {
return fmt.Errorf("wireguard: %w", err) return fmt.Errorf("wireguard: %w", err)
} }
err = v.PMTUD.read(r)
if err != nil {
return fmt.Errorf("PMTUD: %w", err)
}
v.UpCommand = r.Get("VPN_UP_COMMAND", reader.ForceLowercase(false))
v.DownCommand = r.Get("VPN_DOWN_COMMAND", reader.ForceLowercase(false))
return nil return nil
} }
+39 -28
View File
@@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader" "github.com/qdm12/gosettings/reader"
@@ -37,9 +38,14 @@ type Wireguard struct {
Interface string `json:"interface"` Interface string `json:"interface"`
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"` PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
// Maximum Transmission Unit (MTU) of the Wireguard interface. // Maximum Transmission Unit (MTU) of the Wireguard interface.
// It cannot be nil in the internal state, and defaults to // It cannot be zero in the internal state, and defaults to
// 0 indicating to use PMTUD. // 1320. Note it is not the wireguard-go MTU default of 1420
MTU *uint32 `json:"mtu"` // because this impacts bandwidth a lot on some VPN providers,
// see https://github.com/qdm12/gluetun/issues/1650.
// It has been lowered to 1320 following quite a bit of
// investigation in the issue:
// https://github.com/qdm12/gluetun/issues/2533.
MTU uint16 `json:"mtu"`
// Implementation is the Wireguard implementation to use. // Implementation is the Wireguard implementation to use.
// It can be "auto", "userspace" or "kernelspace". // It can be "auto", "userspace" or "kernelspace".
// It defaults to "auto" and cannot be the empty string // It defaults to "auto" and cannot be the empty string
@@ -50,8 +56,23 @@ type Wireguard struct {
var regexpInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) var regexpInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
// Validate validates Wireguard settings. // Validate validates Wireguard settings.
// It should only be ran if the VPN type chosen is Wireguard or AmneziaWg. // It should only be ran if the VPN type chosen is Wireguard.
func (w Wireguard) validate(vpnProvider string, ipv6Supported, amneziawg bool) (err error) { func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error) {
if !helpers.IsOneOf(vpnProvider,
providers.Airvpn,
providers.Custom,
providers.Fastestvpn,
providers.Ivpn,
providers.Mullvad,
providers.Nordvpn,
providers.Protonvpn,
providers.Surfshark,
providers.Windscribe,
) {
// do not validate for VPN provider not supporting Wireguard
return nil
}
// Validate PrivateKey // Validate PrivateKey
if *w.PrivateKey == "" { if *w.PrivateKey == "" {
return fmt.Errorf("%w", ErrWireguardPrivateKeyNotSet) return fmt.Errorf("%w", ErrWireguardPrivateKeyNotSet)
@@ -120,12 +141,10 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported, amneziawg bool) (
ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName) ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName)
} }
if !amneziawg { // amneziawg should have its own Implementation field and ignore this one
validImplementations := []string{"auto", "userspace", "kernelspace"} validImplementations := []string{"auto", "userspace", "kernelspace"}
if err := validate.IsOneOf(w.Implementation, validImplementations...); err != nil { if err := validate.IsOneOf(w.Implementation, validImplementations...); err != nil {
return fmt.Errorf("%w: %w", ErrWireguardImplementationNotValid, err) return fmt.Errorf("%w: %w", ErrWireguardImplementationNotValid, err)
} }
}
return nil return nil
} }
@@ -175,7 +194,8 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs) w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0) w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0") w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
w.MTU = gosettings.DefaultPointer(w.MTU, 0) const defaultMTU = 1320
w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto") w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
} }
@@ -211,11 +231,7 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
} }
interfaceNode := node.Appendf("Network interface: %s", w.Interface) interfaceNode := node.Appendf("Network interface: %s", w.Interface)
if *w.MTU == 0 { interfaceNode.Appendf("MTU: %d", w.MTU)
interfaceNode.Append("MTU: use path MTU discovery")
} else {
interfaceNode.Appendf("MTU: %d", *w.MTU)
}
if w.Implementation != "auto" { if w.Implementation != "auto" {
node.Appendf("Implementation: %s", w.Implementation) node.Appendf("Implementation: %s", w.Implementation)
@@ -224,21 +240,14 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (w *Wireguard) read(r *reader.Reader, amneziaWG bool) (err error) { func (w *Wireguard) read(r *reader.Reader) (err error) {
prefix := "WIREGUARD" w.PrivateKey = r.Get("WIREGUARD_PRIVATE_KEY", reader.ForceLowercase(false))
if amneziaWG { w.PreSharedKey = r.Get("WIREGUARD_PRESHARED_KEY", reader.ForceLowercase(false))
prefix = "AMNEZIAWG"
}
w.PrivateKey = r.Get(prefix+"_PRIVATE_KEY", reader.ForceLowercase(false))
w.PreSharedKey = r.Get(prefix+"_PRESHARED_KEY", reader.ForceLowercase(false))
w.Interface = r.String("VPN_INTERFACE", w.Interface = r.String("VPN_INTERFACE",
reader.RetroKeys(prefix+"_INTERFACE"), reader.ForceLowercase(false)) reader.RetroKeys("WIREGUARD_INTERFACE"), reader.ForceLowercase(false))
if !amneziaWG {
w.Implementation = r.String("WIREGUARD_IMPLEMENTATION") w.Implementation = r.String("WIREGUARD_IMPLEMENTATION")
}
addressStrings := r.CSV(prefix+"_ADDRESSES", reader.RetroKeys(prefix+"_ADDRESS")) addressStrings := r.CSV("WIREGUARD_ADDRESSES", reader.RetroKeys("WIREGUARD_ADDRESS"))
// WARNING: do not initialize w.Addresses to an empty slice // WARNING: do not initialize w.Addresses to an empty slice
// or the defaults for nordvpn will not work. // or the defaults for nordvpn will not work.
for _, addressString := range addressStrings { for _, addressString := range addressStrings {
@@ -253,19 +262,21 @@ func (w *Wireguard) read(r *reader.Reader, amneziaWG bool) (err error) {
w.Addresses = append(w.Addresses, address) w.Addresses = append(w.Addresses, address)
} }
w.AllowedIPs, err = r.CSVNetipPrefixes(prefix + "_ALLOWED_IPS") w.AllowedIPs, err = r.CSVNetipPrefixes("WIREGUARD_ALLOWED_IPS")
if err != nil { if err != nil {
return err // already wrapped return err // already wrapped
} }
w.PersistentKeepaliveInterval, err = r.DurationPtr(prefix + "_PERSISTENT_KEEPALIVE_INTERVAL") w.PersistentKeepaliveInterval, err = r.DurationPtr("WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL")
if err != nil { if err != nil {
return err return err
} }
w.MTU, err = r.Uint32Ptr(prefix + "_MTU") mtuPtr, err := r.Uint16Ptr("WIREGUARD_MTU")
if err != nil { if err != nil {
return err return err
} else if mtuPtr != nil {
w.MTU = *mtuPtr
} }
return nil return nil
} }
@@ -152,22 +152,18 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (w *WireguardSelection) read(r *reader.Reader, amneziaWG bool) (err error) { func (w *WireguardSelection) read(r *reader.Reader) (err error) {
prefix := "WIREGUARD" w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
if amneziaWG {
prefix = "AMNEZIAWG"
}
w.EndpointIP, err = r.NetipAddr(prefix+"_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
if err != nil { if err != nil {
return fmt.Errorf("%w - note this MUST be an IP address, "+ return fmt.Errorf("%w - note this MUST be an IP address, "+
"see https://github.com/qdm12/gluetun/issues/788", err) "see https://github.com/qdm12/gluetun/issues/788", err)
} }
w.EndpointPort, err = r.Uint16Ptr(prefix+"_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT")) w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
if err != nil { if err != nil {
return err return err
} }
w.PublicKey = r.String(prefix+"_PUBLIC_KEY", reader.ForceLowercase(false)) w.PublicKey = r.String("WIREGUARD_PUBLIC_KEY", reader.ForceLowercase(false))
return nil return nil
} }
@@ -1,84 +0,0 @@
package files
import (
"errors"
"fmt"
"os"
"path/filepath"
"gopkg.in/ini.v1"
)
func (s *Source) lazyLoadAmneziawgConf() AmneziawgConfig {
if s.cached.amneziawgLoaded {
return s.cached.amneziawgConf
}
s.cached.amneziawgLoaded = true
var err error
s.cached.amneziawgConf, err = ParseAmneziawgConf(filepath.Join(s.rootDirectory, "amneziawg", "awg0.conf"))
if err != nil {
s.warner.Warnf("skipping Amneziawg config: %s", err)
}
return s.cached.amneziawgConf
}
type AmneziawgConfig struct {
Wireguard WireguardConfig
Jc *string
Jmin *string
Jmax *string
S1 *string
S2 *string
S3 *string
S4 *string
H1 *string
H2 *string
H3 *string
H4 *string
I1 *string
I2 *string
I3 *string
I4 *string
I5 *string
}
func ParseAmneziawgConf(path string) (config AmneziawgConfig, err error) {
iniFile, err := ini.InsensitiveLoad(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return AmneziawgConfig{}, nil
}
return AmneziawgConfig{}, fmt.Errorf("loading ini from reader: %w", err)
}
config.Wireguard, err = ParseWireguardConf(path)
if err != nil {
return AmneziawgConfig{}, err
}
interfaceSection, err := iniFile.GetSection("Interface")
if err != nil {
// can never happen
return AmneziawgConfig{}, fmt.Errorf("getting interface section: %w", err)
}
config.Jc = getINIKeyFromSection(interfaceSection, "Jc")
config.Jmin = getINIKeyFromSection(interfaceSection, "Jmin")
config.Jmax = getINIKeyFromSection(interfaceSection, "Jmax")
config.S1 = getINIKeyFromSection(interfaceSection, "S1")
config.S2 = getINIKeyFromSection(interfaceSection, "S2")
config.S3 = getINIKeyFromSection(interfaceSection, "S3")
config.S4 = getINIKeyFromSection(interfaceSection, "S4")
config.H1 = getINIKeyFromSection(interfaceSection, "H1")
config.H2 = getINIKeyFromSection(interfaceSection, "H2")
config.H3 = getINIKeyFromSection(interfaceSection, "H3")
config.H4 = getINIKeyFromSection(interfaceSection, "H4")
config.I1 = getINIKeyFromSection(interfaceSection, "I1")
config.I2 = getINIKeyFromSection(interfaceSection, "I2")
config.I3 = getINIKeyFromSection(interfaceSection, "I3")
config.I4 = getINIKeyFromSection(interfaceSection, "I4")
config.I5 = getINIKeyFromSection(interfaceSection, "I5")
return config, nil
}
@@ -1,82 +0,0 @@
package files
import (
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Source_ParseAmneziawgConf(t *testing.T) {
t.Parallel()
t.Run("no_file", func(t *testing.T) {
t.Parallel()
noFile := filepath.Join(t.TempDir(), "doesnotexist")
wireguard, err := ParseAmneziawgConf(noFile)
assert.Equal(t, AmneziawgConfig{}, wireguard)
assert.NoError(t, err)
})
testCases := map[string]struct {
fileContent string
amneziawg AmneziawgConfig
errMessage string
}{
"ini_load_error": {
fileContent: "invalid",
errMessage: "loading ini from reader: key-value delimiter not found: invalid",
},
"empty_file": {
errMessage: `getting interface section: section "interface" does not exist`,
},
"success": {
fileContent: `
[Interface]
PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
Address = 10.38.22.35/32
DNS = 193.138.218.74
Jc = 4
H1 = 721391205
I1 = <b 0x1234>
[Peer]
PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
`,
amneziawg: AmneziawgConfig{
Wireguard: WireguardConfig{
PrivateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
PreSharedKey: ptrTo("YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g="),
Addresses: ptrTo("10.38.22.35/32"),
},
Jc: ptrTo("4"),
H1: ptrTo("721391205"),
I1: ptrTo("<b 0x1234>"),
},
},
}
for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
t.Parallel()
configFile := filepath.Join(t.TempDir(), "awg.conf")
const permission = fs.FileMode(0o600)
err := os.WriteFile(configFile, []byte(testCase.fileContent), permission)
require.NoError(t, err)
wireguard, err := ParseAmneziawgConf(configFile)
assert.Equal(t, testCase.amneziawg, wireguard)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
}
@@ -13,8 +13,6 @@ type Source struct {
cached struct { cached struct {
wireguardLoaded bool wireguardLoaded bool
wireguardConf WireguardConfig wireguardConf WireguardConfig
amneziawgLoaded bool
amneziawgConf AmneziawgConfig
} }
} }
@@ -73,11 +71,6 @@ func (s *Source) Get(key string) (value string, isSet bool) {
return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointPort) return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointPort)
} }
value, isSet, matched := s.getAmneziawgKey(key)
if matched {
return value, isSet
}
value, isSet, err := ReadFromFile(path) value, isSet, err := ReadFromFile(path)
if err != nil { if err != nil {
s.warner.Warnf("skipping %s: reading file: %s", path, err) s.warner.Warnf("skipping %s: reading file: %s", path, err)
@@ -85,58 +78,6 @@ func (s *Source) Get(key string) (value string, isSet bool) {
return value, isSet return value, isSet
} }
func (s *Source) getAmneziawgKey(key string) (value string, isSet, matched bool) {
switch key {
case "amnezia_private_key":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PrivateKey)
case "amnezia_preshared_key":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PreSharedKey)
case "amnezia_addresses":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.Addresses)
case "amnezia_public_key":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PublicKey)
case "amnezia_endpoint_ip":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.EndpointIP)
case "amnezia_endpoint_port":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.EndpointPort)
case "amnezia_jc":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jc)
case "amnezia_jmin":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jmin)
case "amnezia_jmax":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jmax)
case "amnezia_s1":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S1)
case "amnezia_s2":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S2)
case "amnezia_s3":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S3)
case "amnezia_s4":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S4)
case "amnezia_h1":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H1)
case "amnezia_h2":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H2)
case "amnezia_h3":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H3)
case "amnezia_h4":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H4)
case "amnezia_i1":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I1)
case "amnezia_i2":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I2)
case "amnezia_i3":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I3)
case "amnezia_i4":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I4)
case "amnezia_i5":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I5)
default:
return "", false, false
}
return value, isSet, true
}
func (s *Source) KeyTransform(key string) string { func (s *Source) KeyTransform(key string) string {
switch key { switch key {
// TODO v4 remove these irregular cases // TODO v4 remove these irregular cases
@@ -3,7 +3,6 @@ package files
import ( import (
"errors" "errors"
"fmt" "fmt"
"net"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -83,15 +82,12 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
publicKey = getINIKeyFromSection(peerSection, "PublicKey") publicKey = getINIKeyFromSection(peerSection, "PublicKey")
endpoint := getINIKeyFromSection(peerSection, "Endpoint") endpoint := getINIKeyFromSection(peerSection, "Endpoint")
if endpoint != nil { if endpoint != nil {
host, port, err := net.SplitHostPort(*endpoint) parts := strings.Split(*endpoint, ":")
if err == nil { endpointIP = &parts[0]
endpointIP = &host const partsWithPort = 2
// IPv6 hosts contain colons; port is managed by the provider for those if len(parts) >= partsWithPort {
if !strings.Contains(host, ":") { endpointPort = new(string)
endpointPort = &port *endpointPort = strings.Join(parts[1:], ":")
}
} else {
endpointIP = endpoint
} }
} }
@@ -179,11 +179,6 @@ Endpoint = 1.2.3.4:51820`,
endpointIP: ptrTo("1.2.3.4"), endpointIP: ptrTo("1.2.3.4"),
endpointPort: ptrTo("51820"), endpointPort: ptrTo("51820"),
}, },
"ipv6_endpoint": {
iniData: `[Peer]
Endpoint = [2a02:bbbb:aaaa:8075::10]:51820`,
endpointIP: ptrTo("2a02:bbbb:aaaa:8075::10"),
},
} }
for testName, testCase := range testCases { for testName, testCase := range testCases {
@@ -1,27 +0,0 @@
package secrets
import (
"os"
"path/filepath"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
)
func (s *Source) lazyLoadAmneziawgConf() files.AmneziawgConfig {
if s.cached.amneziawgLoaded {
return s.cached.amneziawgConf
}
path := os.Getenv("AMNEZIAWG_CONF_SECRETFILE")
if path == "" {
path = filepath.Join(s.rootDirectory, "amneziawg", "awg0.conf")
}
s.cached.amneziawgLoaded = true
var err error
s.cached.amneziawgConf, err = files.ParseAmneziawgConf(path)
if err != nil {
s.warner.Warnf("skipping Amneziawg config: %s", err)
}
return s.cached.amneziawgConf
}
@@ -15,8 +15,6 @@ type Source struct {
cached struct { cached struct {
wireguardLoaded bool wireguardLoaded bool
wireguardConf files.WireguardConfig wireguardConf files.WireguardConfig
amneziawgLoaded bool
amneziawgConf files.AmneziawgConfig
} }
} }
@@ -85,11 +83,6 @@ func (s *Source) Get(key string) (value string, isSet bool) {
return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointPort) return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointPort)
} }
value, isSet, matched := s.getAmneziaWg(key)
if matched {
return value, isSet
}
value, isSet, err := files.ReadFromFile(path) value, isSet, err := files.ReadFromFile(path)
if err != nil { if err != nil {
s.warner.Warnf("skipping %s: reading file: %s", path, err) s.warner.Warnf("skipping %s: reading file: %s", path, err)
@@ -111,55 +104,3 @@ func (s *Source) KeyTransform(key string) string {
return key return key
} }
} }
func (s *Source) getAmneziaWg(key string) (value string, isSet, matched bool) {
switch key {
case "amneziawg_private_key":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PrivateKey)
case "amneziawg_preshared_key":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PreSharedKey)
case "amneziawg_addresses":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.Addresses)
case "amneziawg_public_key":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PublicKey)
case "amneziawg_endpoint_ip":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.EndpointIP)
case "amneziawg_endpoint_port":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.EndpointPort)
case "amneziawg_jc":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jc)
case "amneziawg_jmin":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jmin)
case "amneziawg_jmax":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jmax)
case "amneziawg_s1":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S1)
case "amneziawg_s2":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S2)
case "amneziawg_s3":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S3)
case "amneziawg_s4":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S4)
case "amneziawg_h1":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H1)
case "amneziawg_h2":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H2)
case "amneziawg_h3":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H3)
case "amneziawg_h4":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H4)
case "amneziawg_i1":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I1)
case "amneziawg_i2":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I2)
case "amneziawg_i3":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I3)
case "amneziawg_i4":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I4)
case "amneziawg_i5":
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I5)
default:
return "", false, false
}
return value, isSet, true
}
-1
View File
@@ -1,7 +1,6 @@
package vpn package vpn
const ( const (
AmneziaWg = "amneziawg"
OpenVPN = "openvpn" OpenVPN = "openvpn"
Wireguard = "wireguard" Wireguard = "wireguard"
) )
-131
View File
@@ -1,131 +0,0 @@
package dns
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"math/rand/v2"
"net/http"
"sort"
"strings"
)
func leakCheck(ctx context.Context, client *http.Client) (report string, err error) {
const sessionLength = 40
session := generateRandomString(sessionLength)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
type result struct {
dnsToCount map[string]uint
err error
}
resultsCh := make(chan result)
const requestsCount = 5
for range requestsCount {
go func() {
dnsToCount, err := triggerDNSQuery(ctx, client, session)
resultsCh <- result{dnsToCount: dnsToCount, err: err}
}()
}
dnsToCount := make(map[string]uint)
for range requestsCount {
result := <-resultsCh
if result.err != nil {
if err == nil {
cancel()
err = fmt.Errorf("request failed: %w", result.err)
}
continue
}
for dns, count := range result.dnsToCount {
dnsToCount[dns] += count
}
}
if err != nil {
return "", err
}
return formatPercentages(dnsToCount), nil
}
func generateRandomString(length uint) string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.IntN(len(charset))] //nolint:gosec
}
return string(b)
}
var errIPLeakSessionMismatch = errors.New("ipleak.net session mismatch")
func triggerDNSQuery(ctx context.Context, client *http.Client, session string) (
dnsToCount map[string]uint, err error,
) {
const randomLength = 12
randomPart := generateRandomString(randomLength)
url := fmt.Sprintf("https://%s-%s.ipleak.net/dnsdetection/", session, randomPart)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("performing request: %w", err)
}
defer response.Body.Close()
type ipLeakData struct {
Session string `json:"session"`
IP map[string]uint `json:"ip"`
}
decoder := json.NewDecoder(response.Body)
var data ipLeakData
err = decoder.Decode(&data)
if err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
} else if data.Session != session {
return nil, fmt.Errorf("%w: expected %s, got %s", errIPLeakSessionMismatch, session, data.Session)
}
return data.IP, nil
}
func formatPercentages(data map[string]uint) string {
if len(data) == 0 {
return ""
}
var total uint
keys := make([]string, 0, len(data))
for k, v := range data {
total += v
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if data[keys[i]] == data[keys[j]] {
return keys[i] < keys[j] // Tie-breaker: alphabetical
}
return data[keys[i]] > data[keys[j]]
})
results := make([]string, len(keys))
for i, key := range keys {
var pct float64
if total > 0 {
pct = math.Ceil((float64(data[key]) / float64(total)) * 100) //nolint:mnd
}
results[i] = fmt.Sprintf("%s (%.0f%%)", key, pct)
}
return strings.Join(results, ", ")
}
-22
View File
@@ -1,22 +0,0 @@
package dns
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func Test_leakCheck(t *testing.T) {
t.Parallel()
const timeout = 10 * time.Second
ctx, cancel := context.WithTimeout(t.Context(), timeout)
t.Cleanup(cancel)
client := http.DefaultClient
report, err := leakCheck(ctx, client)
require.NoError(t, err)
require.NotEmpty(t, report)
}
-2
View File
@@ -3,8 +3,6 @@ package dns
type Logger interface { type Logger interface {
Debug(s string) Debug(s string)
Info(s string) Info(s string)
Infof(format string, args ...any)
Warn(s string) Warn(s string)
Warnf(format string, args ...any)
Error(s string) Error(s string)
} }
+1 -3
View File
@@ -22,7 +22,6 @@ type Loop struct {
server *server.Server server *server.Server
filter *mapfilter.Filter filter *mapfilter.Filter
localResolvers []netip.Addr localResolvers []netip.Addr
localSubnets []netip.Prefix
resolvConf string resolvConf string
client *http.Client client *http.Client
logger Logger logger Logger
@@ -40,7 +39,7 @@ type Loop struct {
const defaultBackoffTime = 10 * time.Second const defaultBackoffTime = 10 * time.Second
func NewLoop(settings settings.DNS, func NewLoop(settings settings.DNS,
client *http.Client, logger Logger, localSubnets []netip.Prefix, client *http.Client, logger Logger,
) (loop *Loop, err error) { ) (loop *Loop, err error) {
start := make(chan struct{}) start := make(chan struct{})
running := make(chan models.LoopStatus) running := make(chan models.LoopStatus)
@@ -63,7 +62,6 @@ func NewLoop(settings settings.DNS,
state: state, state: state,
server: nil, server: nil,
filter: filter, filter: filter,
localSubnets: localSubnets,
resolvConf: "/etc/resolv.conf", resolvConf: "/etc/resolv.conf",
client: client, client: client,
logger: logger, logger: logger,
+37
View File
@@ -0,0 +1,37 @@
package dns
import (
"net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/nameserver"
)
func (l *Loop) useUnencryptedDNS(fallback bool) {
settings := l.GetSettings()
targetIP := settings.GetFirstPlaintextIPv4()
if fallback {
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
} else {
l.logger.Info("using plaintext DNS at address " + targetIP.String())
}
const dialTimeout = 3 * time.Second
const defaultDNSPort = 53
settingsInternalDNS := nameserver.SettingsInternalDNS{
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
Timeout: dialTimeout,
}
nameserver.UseDNSInternally(settingsInternalDNS)
settingsSystemWide := nameserver.SettingsSystemDNS{
IPs: []netip.Addr{targetIP},
ResolvPath: l.resolvConf,
}
err := nameserver.UseDNSSystemWide(settingsSystemWide)
if err != nil {
l.logger.Error(err.Error())
}
}
+38 -20
View File
@@ -2,9 +2,9 @@ package dns
import ( import (
"context" "context"
"errors"
"github.com/qdm12/dns/v2/pkg/nameserver" "github.com/qdm12/dns/v2/pkg/nameserver"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
) )
@@ -18,6 +18,15 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
return return
} }
if *l.GetSettings().KeepNameserver {
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
"this will likely leak DNS traffic outside the VPN " +
"and go through your container network DNS outside the VPN tunnel!")
} else {
const fallback = false
l.useUnencryptedDNS(fallback)
}
select { select {
case <-l.start: case <-l.start:
case <-ctx.Done(): case <-ctx.Done():
@@ -29,40 +38,39 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
// Their values are to be used if DOT=off // Their values are to be used if DOT=off
var runError <-chan error var runError <-chan error
var settings settings.DNS settings := l.GetSettings()
for { for !*settings.KeepNameserver && *settings.ServerEnabled {
settings = l.GetSettings()
var err error var err error
runError, err = l.setupServer(ctx, settings) runError, err = l.setupServer(ctx)
if err == nil { if err == nil {
l.backoffTime = defaultBackoffTime
l.logger.Info("ready")
break break
} }
l.signalOrSetStatus(constants.Crashed) l.signalOrSetStatus(constants.Crashed)
if ctx.Err() != nil { if ctx.Err() != nil {
return return
} }
l.logAndWait(ctx, err)
if !errors.Is(err, errUpdateBlockLists) {
const fallback = true
l.useUnencryptedDNS(fallback)
} }
l.logAndWait(ctx, err)
l.backoffTime = defaultBackoffTime settings = l.GetSettings()
l.logger.Infof("ready and using DNS server with %s upstream resolvers", settings.UpstreamType)
err = l.updateFiles(ctx, settings)
if err != nil {
l.logger.Warn("downloading block lists failed, skipping: " + err.Error())
} }
l.signalOrSetStatus(constants.Running) l.signalOrSetStatus(constants.Running)
l.userTrigger = false settings = l.GetSettings()
if !*settings.KeepNameserver && !*settings.ServerEnabled {
report, err := leakCheck(ctx, l.client) const fallback = false
if err != nil { l.useUnencryptedDNS(fallback)
l.logger.Warnf("running leak check: %s", err)
} else {
l.logger.Infof("leak check report: %s", report)
} }
l.userTrigger = false
exitLoop := l.runWait(ctx, runError) exitLoop := l.runWait(ctx, runError)
if exitLoop { if exitLoop {
return return
@@ -74,13 +82,21 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
settings := l.GetSettings()
if !*settings.KeepNameserver && *settings.ServerEnabled {
l.stopServer() l.stopServer()
// TODO revert OS and Go nameserver when exiting // TODO revert OS and Go nameserver when exiting
}
return true return true
case <-l.stop: case <-l.stop:
l.userTrigger = true l.userTrigger = true
l.logger.Info("stopping") l.logger.Info("stopping")
settings := l.GetSettings()
if !*settings.KeepNameserver && *settings.ServerEnabled {
const fallback = false
l.useUnencryptedDNS(fallback)
l.stopServer() l.stopServer()
}
l.stopped <- struct{}{} l.stopped <- struct{}{}
case <-l.start: case <-l.start:
l.userTrigger = true l.userTrigger = true
@@ -88,6 +104,8 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
return false return false
case err := <-runError: // unexpected error case err := <-runError: // unexpected error
l.statusManager.SetStatus(constants.Crashed) l.statusManager.SetStatus(constants.Crashed)
const fallback = true
l.useUnencryptedDNS(fallback)
l.logAndWait(ctx, err) l.logAndWait(ctx, err)
return false return false
} }
+18 -54
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"net/netip" "net/netip"
"slices"
"github.com/qdm12/dns/v2/pkg/doh" "github.com/qdm12/dns/v2/pkg/doh"
"github.com/qdm12/dns/v2/pkg/dot" "github.com/qdm12/dns/v2/pkg/dot"
@@ -27,23 +26,31 @@ func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
return l.state.SetSettings(ctx, settings) return l.state.SetSettings(ctx, settings)
} }
func buildServerSettings(userSettings settings.DNS, func buildServerSettings(settings settings.DNS,
filter *mapfilter.Filter, localResolvers []netip.Addr, filter *mapfilter.Filter, localResolvers []netip.Addr,
localSubnets []netip.Prefix, logger Logger) ( logger Logger) (
serverSettings server.Settings, err error, serverSettings server.Settings, err error,
) { ) {
serverSettings.Logger = logger serverSettings.Logger = logger
upstreamResolvers := buildProviders(userSettings, localSubnets, logger) providersData := provider.NewProviders()
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
for i := range settings.Providers {
var err error
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
if err != nil {
panic(err) // this should already had been checked
}
}
ipVersion := "ipv4" ipVersion := "ipv4"
if *userSettings.IPv6 { if *settings.IPv6 {
ipVersion = "ipv6" ipVersion = "ipv6"
} }
var dialer server.Dialer var dialer server.Dialer
switch userSettings.UpstreamType { switch settings.UpstreamType {
case settings.DNSUpstreamTypeDot: case "dot":
dialerSettings := dot.Settings{ dialerSettings := dot.Settings{
UpstreamResolvers: upstreamResolvers, UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion, IPVersion: ipVersion,
@@ -52,7 +59,7 @@ func buildServerSettings(userSettings settings.DNS,
if err != nil { if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err) return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
} }
case settings.DNSUpstreamTypeDoh: case "doh":
dialerSettings := doh.Settings{ dialerSettings := doh.Settings{
UpstreamResolvers: upstreamResolvers, UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion, IPVersion: ipVersion,
@@ -61,7 +68,7 @@ func buildServerSettings(userSettings settings.DNS,
if err != nil { if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err) return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err)
} }
case settings.DNSUpstreamTypePlain: case "plain":
dialerSettings := plain.Settings{ dialerSettings := plain.Settings{
UpstreamResolvers: upstreamResolvers, UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion, IPVersion: ipVersion,
@@ -71,11 +78,11 @@ func buildServerSettings(userSettings settings.DNS,
return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err) return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err)
} }
default: default:
panic("unknown upstream type: " + userSettings.UpstreamType) panic("unknown upstream type: " + settings.UpstreamType)
} }
serverSettings.Dialer = dialer serverSettings.Dialer = dialer
if *userSettings.Caching { if *settings.Caching {
lruCache, err := lru.New(lru.Settings{}) lruCache, err := lru.New(lru.Settings{})
if err != nil { if err != nil {
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err) return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
@@ -116,46 +123,3 @@ func buildServerSettings(userSettings settings.DNS,
return serverSettings, nil return serverSettings, nil
} }
func buildProviders(userSettings settings.DNS, localSubnets []netip.Prefix,
logger Logger,
) (providers []provider.Provider) {
userDefinedPlainAddresses := userSettings.UpstreamType == settings.DNSUpstreamTypePlain &&
len(userSettings.UpstreamPlainAddresses) > 0
if !userDefinedPlainAddresses {
providers = make([]provider.Provider, len(userSettings.Providers))
providersData := provider.NewProviders()
for i, providerName := range userSettings.Providers {
var err error
providers[i], err = providersData.Get(providerName)
if err != nil {
panic(err) // this should already had been checked
}
}
return providers
}
providers = make([]provider.Provider, len(userSettings.UpstreamPlainAddresses))
for i, addrPort := range userSettings.UpstreamPlainAddresses {
addr := addrPort.Addr()
if addr.IsPrivate() && !addr.IsLoopback() &&
!slices.ContainsFunc(localSubnets, func(prefix netip.Prefix) bool {
return prefix.Contains(addr)
}) {
logger.Warnf("DNS server address %s is not in local subnets, "+
"make sure to specify it in FIREWALL_OUTBOUND_SUBNETS as %s",
addr, netip.PrefixFrom(addr, addr.BitLen()))
}
providers[i] = provider.Provider{
Name: addrPort.String(),
}
if addr.Is4() {
providers[i].Plain.IPv4 = []netip.AddrPort{addrPort}
} else {
providers[i].Plain.IPv6 = []netip.AddrPort{addrPort}
}
}
return providers
}
+22 -9
View File
@@ -2,23 +2,26 @@ package dns
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/netip"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update" "github.com/qdm12/dns/v2/pkg/check"
"github.com/qdm12/dns/v2/pkg/nameserver" "github.com/qdm12/dns/v2/pkg/nameserver"
"github.com/qdm12/dns/v2/pkg/server" "github.com/qdm12/dns/v2/pkg/server"
"github.com/qdm12/gluetun/internal/configuration/settings"
) )
func (l *Loop) setupServer(ctx context.Context, settings settings.DNS) (runError <-chan error, err error) { var errUpdateBlockLists = errors.New("cannot update filter block lists")
var updateSettings update.Settings
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames) func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) {
err = l.filter.Update(updateSettings) err = l.updateFiles(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("updating filter for rebinding protection: %w", err) return nil, fmt.Errorf("%w: %w", errUpdateBlockLists, err)
} }
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.localSubnets, l.logger) settings := l.GetSettings()
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
if err != nil { if err != nil {
return nil, fmt.Errorf("building server settings: %w", err) return nil, fmt.Errorf("building server settings: %w", err)
} }
@@ -35,13 +38,23 @@ func (l *Loop) setupServer(ctx context.Context, settings settings.DNS) (runError
l.server = server l.server = server
// use internal DNS server // use internal DNS server
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{}) const defaultDNSPort = 53
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
})
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{ err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
IPs: []netip.Addr{settings.ServerAddress},
ResolvPath: l.resolvConf, ResolvPath: l.resolvConf,
}) })
if err != nil { if err != nil {
l.logger.Error(err.Error()) l.logger.Error(err.Error())
} }
err = check.WaitForDNS(ctx, check.Settings{})
if err != nil {
l.stopServer()
return nil, err
}
return runError, nil return runError, nil
} }
+2
View File
@@ -40,6 +40,8 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
// Restart // Restart
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped) _, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
if *settings.ServerEnabled {
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running) outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
}
return outcome return outcome
} }
+14 -3
View File
@@ -28,12 +28,23 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
return return
case <-timer.C: case <-timer.C:
lastTick = l.timeNow() lastTick = l.timeNow()
status := l.GetStatus()
if status == constants.Running {
if err := l.updateFiles(ctx); err != nil {
l.statusManager.SetStatus(constants.Crashed)
l.logger.Error(err.Error())
l.logger.Warn("skipping DNS server restart due to failed files update")
settings := l.GetSettings() settings := l.GetSettings()
if l.GetStatus() == constants.Running { timer.Reset(*settings.UpdatePeriod)
if err := l.updateFiles(ctx, settings); err != nil { continue
l.logger.Warn("updating block lists failed, skipping: " + err.Error())
} }
} }
_, _ = l.statusManager.ApplyStatus(ctx, constants.Stopped)
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
settings := l.GetSettings()
timer.Reset(*settings.UpdatePeriod) timer.Reset(*settings.UpdatePeriod)
case <-l.updateTicker: case <-l.updateTicker:
if !timer.Stop() { if !timer.Stop() {
+4 -2
View File
@@ -6,10 +6,11 @@ import (
"github.com/qdm12/dns/v2/pkg/blockbuilder" "github.com/qdm12/dns/v2/pkg/blockbuilder"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update" "github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
"github.com/qdm12/gluetun/internal/configuration/settings"
) )
func (l *Loop) updateFiles(ctx context.Context, settings settings.DNS) (err error) { func (l *Loop) updateFiles(ctx context.Context) (err error) {
settings := l.GetSettings()
l.logger.Info("downloading hostnames and IP block lists") l.logger.Info("downloading hostnames and IP block lists")
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client) blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
@@ -36,6 +37,7 @@ func (l *Loop) updateFiles(ctx context.Context, settings settings.DNS) (err erro
IPPrefixes: result.BlockedIPPrefixes, IPPrefixes: result.BlockedIPPrefixes,
} }
updateSettings.BlockHostnames(result.BlockedHostnames) updateSettings.BlockHostnames(result.BlockedHostnames)
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
err = l.filter.Update(updateSettings) err = l.filter.Update(updateSettings)
if err != nil { if err != nil {
return fmt.Errorf("updating filter: %w", err) return fmt.Errorf("updating filter: %w", err)
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"fmt" "fmt"
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"context" "context"
@@ -69,8 +69,8 @@ func Test_deleteIPTablesRule(t *testing.T) {
"invalid_instruction": { "invalid_instruction": {
instruction: "invalid", instruction: "invalid",
errWrapped: ErrIptablesCommandMalformed, errWrapped: ErrIptablesCommandMalformed,
errMessage: "parsing iptables command: parsing \"invalid\": " + errMessage: "parsing iptables command: iptables command is malformed: " +
"iptables command is malformed: flag \"invalid\" requires a value, but got none", "fields count 1 is not even: \"invalid\"",
}, },
"list_error": { "list_error": {
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
+60 -34
View File
@@ -22,7 +22,9 @@ func (c *Config) SetEnabled(ctx context.Context, enabled bool) (err error) {
if !enabled { if !enabled {
c.logger.Info("disabling...") c.logger.Info("disabling...")
c.restore(ctx) if err = c.disable(ctx); err != nil {
return fmt.Errorf("disabling firewall: %w", err)
}
c.enabled = false c.enabled = false
c.logger.Info("disabled successfully") c.logger.Info("disabled successfully")
return nil return nil
@@ -39,37 +41,64 @@ func (c *Config) SetEnabled(ctx context.Context, enabled bool) (err error) {
return nil return nil
} }
func (c *Config) disable(ctx context.Context) (err error) {
if err = c.clearAllRules(ctx); err != nil {
return fmt.Errorf("clearing all rules: %w", err)
}
if err = c.setIPv4AllPolicies(ctx, "ACCEPT"); err != nil {
return fmt.Errorf("setting ipv4 policies: %w", err)
}
if err = c.setIPv6AllPolicies(ctx, "ACCEPT"); err != nil {
return fmt.Errorf("setting ipv6 policies: %w", err)
}
const remove = true
err = c.redirectPorts(ctx, remove)
if err != nil {
return fmt.Errorf("removing port redirections: %w", err)
}
return nil
}
// To use in defered call when enabling the firewall.
func (c *Config) fallbackToDisabled(ctx context.Context) {
if ctx.Err() != nil {
return
}
if err := c.disable(ctx); err != nil {
c.logger.Error("failed reversing firewall changes: " + err.Error())
}
}
func (c *Config) enable(ctx context.Context) (err error) { func (c *Config) enable(ctx context.Context) (err error) {
c.restore, err = c.impl.SaveAndRestore(ctx) touched := false
if err != nil { if err = c.setIPv4AllPolicies(ctx, "DROP"); err != nil {
return fmt.Errorf("saving firewall rules: %w", err)
}
defer func() {
if err != nil {
c.restore(context.Background())
}
}()
if err = c.impl.SetIPv4AllPolicies(ctx, "DROP"); err != nil {
return err return err
} }
touched = true
if err = c.impl.SetIPv6AllPolicies(ctx, "DROP"); err != nil { if err = c.setIPv6AllPolicies(ctx, "DROP"); err != nil {
return err
}
// Loopback traffic
if err = c.impl.AcceptInputThroughInterface(ctx, "lo"); err != nil {
return err return err
} }
const remove = false const remove = false
if err = c.impl.AcceptOutputThroughInterface(ctx, "lo", remove); err != nil {
defer func() {
if touched && err != nil {
c.fallbackToDisabled(ctx)
}
}()
// Loopback traffic
if err = c.acceptInputThroughInterface(ctx, "lo", remove); err != nil {
return err
}
if err = c.acceptOutputThroughInterface(ctx, "lo", remove); err != nil {
return err return err
} }
if err = c.impl.AcceptEstablishedRelatedTraffic(ctx); err != nil { if err = c.acceptEstablishedRelatedTraffic(ctx, remove); err != nil {
return err return err
} }
@@ -79,9 +108,7 @@ func (c *Config) enable(ctx context.Context) (err error) {
localInterfaces := make(map[string]struct{}, len(c.localNetworks)) localInterfaces := make(map[string]struct{}, len(c.localNetworks))
for _, network := range c.localNetworks { for _, network := range c.localNetworks {
err = c.impl.AcceptOutputFromIPToSubnet(ctx, if err := c.acceptOutputFromIPToSubnet(ctx, network.InterfaceName, network.IP, network.IPNet, remove); err != nil {
network.InterfaceName, network.IP, network.IPNet, remove)
if err != nil {
return err return err
} }
@@ -90,7 +117,7 @@ func (c *Config) enable(ctx context.Context) (err error) {
continue continue
} }
localInterfaces[network.InterfaceName] = struct{}{} localInterfaces[network.InterfaceName] = struct{}{}
err = c.impl.AcceptIpv6MulticastOutput(ctx, network.InterfaceName) err = c.acceptIpv6MulticastOutput(ctx, network.InterfaceName, remove)
if err != nil { if err != nil {
return fmt.Errorf("accepting IPv6 multicast output: %w", err) return fmt.Errorf("accepting IPv6 multicast output: %w", err)
} }
@@ -103,7 +130,7 @@ func (c *Config) enable(ctx context.Context) (err error) {
// Allows packets from any IP address to go through eth0 / local network // Allows packets from any IP address to go through eth0 / local network
// to reach Gluetun. // to reach Gluetun.
for _, network := range c.localNetworks { for _, network := range c.localNetworks {
if err := c.impl.AcceptInputToSubnet(ctx, network.InterfaceName, network.IPNet); err != nil { if err := c.acceptInputToSubnet(ctx, network.InterfaceName, network.IPNet, remove); err != nil {
return err return err
} }
} }
@@ -112,12 +139,12 @@ func (c *Config) enable(ctx context.Context) (err error) {
return err return err
} }
err = c.redirectPorts(ctx) err = c.redirectPorts(ctx, remove)
if err != nil { if err != nil {
return fmt.Errorf("redirecting ports: %w", err) return fmt.Errorf("redirecting ports: %w", err)
} }
if err := c.impl.RunUserPostRules(ctx, c.customRulesPath); err != nil { if err := c.runUserPostRules(ctx, c.customRulesPath, remove); err != nil {
return fmt.Errorf("running user defined post firewall rules: %w", err) return fmt.Errorf("running user defined post firewall rules: %w", err)
} }
@@ -137,7 +164,7 @@ func (c *Config) allowVPNIP(ctx context.Context) (err error) {
continue continue
} }
interfacesSeen[defaultRoute.NetInterface] = struct{}{} interfacesSeen[defaultRoute.NetInterface] = struct{}{}
err = c.impl.AcceptOutputTrafficToVPN(ctx, defaultRoute.NetInterface, c.vpnConnection, remove) err = c.acceptOutputTrafficToVPN(ctx, defaultRoute.NetInterface, c.vpnConnection, remove)
if err != nil { if err != nil {
return fmt.Errorf("accepting output traffic through VPN: %w", err) return fmt.Errorf("accepting output traffic through VPN: %w", err)
} }
@@ -159,7 +186,7 @@ func (c *Config) allowOutboundSubnets(ctx context.Context) (err error) {
firewallUpdated = true firewallUpdated = true
const remove = false const remove = false
err := c.impl.AcceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface, err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
defaultRoute.AssignedIP, subnet, remove) defaultRoute.AssignedIP, subnet, remove)
if err != nil { if err != nil {
return err return err
@@ -177,7 +204,7 @@ func (c *Config) allowInputPorts(ctx context.Context) (err error) {
for port, netInterfaces := range c.allowedInputPorts { for port, netInterfaces := range c.allowedInputPorts {
for netInterface := range netInterfaces { for netInterface := range netInterfaces {
const remove = false const remove = false
err = c.impl.AcceptInputToPort(ctx, netInterface, port, remove) err = c.acceptInputToPort(ctx, netInterface, port, remove)
if err != nil { if err != nil {
return fmt.Errorf("accepting input port %d on interface %s: %w", return fmt.Errorf("accepting input port %d on interface %s: %w",
port, netInterface, err) port, netInterface, err)
@@ -187,10 +214,9 @@ func (c *Config) allowInputPorts(ctx context.Context) (err error) {
return nil return nil
} }
func (c *Config) redirectPorts(ctx context.Context) (err error) { func (c *Config) redirectPorts(ctx context.Context, remove bool) (err error) {
for _, portRedirection := range c.portRedirections { for _, portRedirection := range c.portRedirections {
const remove = false err = c.redirectPort(ctx, portRedirection.interfaceName, portRedirection.sourcePort,
err = c.impl.RedirectPort(ctx, portRedirection.interfaceName, portRedirection.sourcePort,
portRedirection.destinationPort, remove) portRedirection.destinationPort, remove)
if err != nil { if err != nil {
return err return err
+16 -10
View File
@@ -2,11 +2,9 @@ package firewall
import ( import (
"context" "context"
"fmt"
"net/netip" "net/netip"
"sync" "sync"
"github.com/qdm12/gluetun/internal/firewall/iptables"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/routing" "github.com/qdm12/gluetun/internal/routing"
) )
@@ -14,16 +12,18 @@ import (
type Config struct { type Config struct {
runner CmdRunner runner CmdRunner
logger Logger logger Logger
iptablesMutex sync.Mutex
ip6tablesMutex sync.Mutex
defaultRoutes []routing.DefaultRoute defaultRoutes []routing.DefaultRoute
localNetworks []routing.LocalNetwork localNetworks []routing.LocalNetwork
// Fixed // Fixed state
impl firewallImpl ipTables string
ip6Tables string
customRulesPath string customRulesPath string
// State // State
enabled bool enabled bool
restore func(context.Context)
vpnConnection models.Connection vpnConnection models.Connection
vpnIntf string vpnIntf string
outboundSubnets []netip.Prefix outboundSubnets []netip.Prefix
@@ -34,23 +34,29 @@ type Config struct {
// NewConfig creates a new Config instance and returns an error // NewConfig creates a new Config instance and returns an error
// if no iptables implementation is available. // if no iptables implementation is available.
func NewConfig(ctx context.Context, logger, iptablesLogger Logger, func NewConfig(ctx context.Context, logger Logger,
runner CmdRunner, defaultRoutes []routing.DefaultRoute, runner CmdRunner, defaultRoutes []routing.DefaultRoute,
localNetworks []routing.LocalNetwork, localNetworks []routing.LocalNetwork,
) (config *Config, err error) { ) (config *Config, err error) {
impl, err := iptables.New(ctx, runner, iptablesLogger) iptables, err := checkIptablesSupport(ctx, runner, "iptables", "iptables-nft", "iptables-legacy")
if err != nil { if err != nil {
return nil, fmt.Errorf("creating iptables firewall: %w", err) return nil, err
}
ip6tables, err := findIP6tablesSupported(ctx, runner)
if err != nil {
return nil, err
} }
return &Config{ return &Config{
runner: runner, runner: runner,
logger: logger, logger: logger,
allowedInputPorts: make(map[uint16]map[string]struct{}), allowedInputPorts: make(map[uint16]map[string]struct{}),
ipTables: iptables,
ip6Tables: ip6tables,
customRulesPath: "/iptables/post-rules.txt",
// Obtained from routing // Obtained from routing
defaultRoutes: defaultRoutes, defaultRoutes: defaultRoutes,
localNetworks: localNetworks, localNetworks: localNetworks,
impl: impl,
customRulesPath: "/iptables/post-rules.txt",
}, nil }, nil
} }
+1 -31
View File
@@ -1,12 +1,6 @@
package firewall package firewall
import ( import "os/exec"
"context"
"net/netip"
"os/exec"
"github.com/qdm12/gluetun/internal/models"
)
type CmdRunner interface { type CmdRunner interface {
Run(cmd *exec.Cmd) (output string, err error) Run(cmd *exec.Cmd) (output string, err error)
@@ -18,27 +12,3 @@ type Logger interface {
Warn(s string) Warn(s string)
Error(s string) Error(s string)
} }
type firewallImpl interface { //nolint:interfacebloat
SaveAndRestore(ctx context.Context) (restore func(context.Context), err error)
AcceptEstablishedRelatedTraffic(ctx context.Context) error
AcceptInputThroughInterface(ctx context.Context, intf string) error
AcceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error
AcceptInputToSubnet(ctx context.Context, intf string, subnet netip.Prefix) error
AcceptIpv6MulticastOutput(ctx context.Context, intf string) error
AcceptOutput(ctx context.Context, protocol, intf string,
ip netip.Addr, port uint16, remove bool) error
AcceptOutputFromIPToSubnet(ctx context.Context, intf string, assignedIP netip.Addr,
subnet netip.Prefix, remove bool) error
AcceptOutputThroughInterface(ctx context.Context, intf string, remove bool) error
AcceptOutputTrafficToVPN(ctx context.Context, intf string,
connection models.Connection, remove bool) error
RedirectPort(ctx context.Context, intf string, sourcePort,
destinationPort uint16, remove bool) error
RunUserPostRules(ctx context.Context, customRulesPath string) error
SetIPv4AllPolicies(ctx context.Context, policy string) error
SetIPv6AllPolicies(ctx context.Context, policy string) error
TempDropOutputTCPRST(ctx context.Context, src, dst netip.AddrPort, excludeMark int) (
revert func(ctx context.Context) error, err error)
Version(ctx context.Context) (version string, err error)
}
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"context" "context"
@@ -14,8 +14,8 @@ import (
func findIP6tablesSupported(ctx context.Context, runner CmdRunner) ( func findIP6tablesSupported(ctx context.Context, runner CmdRunner) (
ip6tablesPath string, err error, ip6tablesPath string, err error,
) { ) {
ip6tablesPath, err = checkIptablesSupport(ctx, runner, "ip6tables", "ip6tables-legacy") ip6tablesPath, err = checkIptablesSupport(ctx, runner, "ip6tables", "ip6tables-nft", "ip6tables-legacy")
if errors.Is(err, ErrNotSupported) { if errors.Is(err, ErrIPTablesNotSupported) {
return "", nil return "", nil
} else if err != nil { } else if err != nil {
return "", err return "", err
@@ -24,23 +24,8 @@ func findIP6tablesSupported(ctx context.Context, runner CmdRunner) (
} }
func (c *Config) runIP6tablesInstructions(ctx context.Context, instructions []string) error { func (c *Config) runIP6tablesInstructions(ctx context.Context, instructions []string) error {
c.iptablesMutex.Lock() // only one iptables command at once
defer c.iptablesMutex.Unlock()
restore, err := c.saveAndRestoreIPv6(ctx)
if err != nil {
return err
}
err = c.runIP6tablesInstructionsNoSave(ctx, instructions)
if err != nil {
restore(ctx)
}
return err
}
func (c *Config) runIP6tablesInstructionsNoSave(ctx context.Context, instructions []string) error {
for _, instruction := range instructions { for _, instruction := range instructions {
if err := c.runIP6tablesInstructionNoSave(ctx, instruction); err != nil { if err := c.runIP6tablesInstruction(ctx, instruction); err != nil {
return err return err
} }
} }
@@ -48,24 +33,11 @@ func (c *Config) runIP6tablesInstructionsNoSave(ctx context.Context, instruction
} }
func (c *Config) runIP6tablesInstruction(ctx context.Context, instruction string) error { func (c *Config) runIP6tablesInstruction(ctx context.Context, instruction string) error {
c.iptablesMutex.Lock() // only one iptables command at once
defer c.iptablesMutex.Unlock()
restore, err := c.saveAndRestoreIPv6(ctx)
if err != nil {
return err
}
err = c.runIP6tablesInstructionNoSave(ctx, instruction)
if err != nil {
restore(ctx)
}
return err
}
func (c *Config) runIP6tablesInstructionNoSave(ctx context.Context, instruction string) error {
if c.ip6Tables == "" { if c.ip6Tables == "" {
return nil return nil
} }
c.ip6tablesMutex.Lock() // only one ip6tables command at once
defer c.ip6tablesMutex.Unlock()
if isDeleteMatchInstruction(instruction) { if isDeleteMatchInstruction(instruction) {
return deleteIPTablesRule(ctx, c.ip6Tables, instruction, return deleteIPTablesRule(ctx, c.ip6Tables, instruction,
@@ -84,7 +56,7 @@ func (c *Config) runIP6tablesInstructionNoSave(ctx context.Context, instruction
var ErrPolicyNotValid = errors.New("policy is not valid") var ErrPolicyNotValid = errors.New("policy is not valid")
func (c *Config) SetIPv6AllPolicies(ctx context.Context, policy string) error { func (c *Config) setIPv6AllPolicies(ctx context.Context, policy string) error {
switch policy { switch policy {
case "ACCEPT", "DROP": case "ACCEPT", "DROP":
default: default:
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"context" "context"
@@ -26,6 +26,22 @@ func appendOrDelete(remove bool) string {
return "--append" return "--append"
} }
// flipRule changes an append rule in a delete rule or a delete rule into an
// append rule.
func flipRule(rule string) string {
switch {
case strings.HasPrefix(rule, "-A"):
return strings.Replace(rule, "-A", "-D", 1)
case strings.HasPrefix(rule, "--append"):
return strings.Replace(rule, "--append", "-D", 1)
case strings.HasPrefix(rule, "-D"):
return strings.Replace(rule, "-D", "-A", 1)
case strings.HasPrefix(rule, "--delete"):
return strings.Replace(rule, "--delete", "-A", 1)
}
return rule
}
// Version obtains the version of the installed iptables. // Version obtains the version of the installed iptables.
func (c *Config) Version(ctx context.Context) (string, error) { func (c *Config) Version(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, c.ipTables, "--version") //nolint:gosec cmd := exec.CommandContext(ctx, c.ipTables, "--version") //nolint:gosec
@@ -38,28 +54,12 @@ func (c *Config) Version(ctx context.Context) (string, error) {
if len(words) < minWords { if len(words) < minWords {
return "", fmt.Errorf("%w: %s", ErrIPTablesVersionTooShort, output) return "", fmt.Errorf("%w: %s", ErrIPTablesVersionTooShort, output)
} }
return "iptables " + words[1], nil return words[1], nil
} }
func (c *Config) runIptablesInstructions(ctx context.Context, instructions []string) error { func (c *Config) runIptablesInstructions(ctx context.Context, instructions []string) error {
c.iptablesMutex.Lock()
defer c.iptablesMutex.Unlock()
restore, err := c.saveAndRestoreIPv4(ctx)
if err != nil {
return err
}
err = c.runIptablesInstructionsNoSave(ctx, instructions)
if err != nil {
restore(ctx)
}
return err
}
func (c *Config) runIptablesInstructionsNoSave(ctx context.Context, instructions []string) error {
for _, instruction := range instructions { for _, instruction := range instructions {
if err := c.runIptablesInstructionNoSave(ctx, instruction); err != nil { if err := c.runIptablesInstruction(ctx, instruction); err != nil {
return err return err
} }
} }
@@ -70,19 +70,6 @@ func (c *Config) runIptablesInstruction(ctx context.Context, instruction string)
c.iptablesMutex.Lock() // only one iptables command at once c.iptablesMutex.Lock() // only one iptables command at once
defer c.iptablesMutex.Unlock() defer c.iptablesMutex.Unlock()
restore, err := c.saveAndRestoreIPv4(ctx)
if err != nil {
return err
}
err = c.runIptablesInstructionNoSave(ctx, instruction)
if err != nil {
restore(ctx)
}
return err
}
func (c *Config) runIptablesInstructionNoSave(ctx context.Context, instruction string) error {
if isDeleteMatchInstruction(instruction) { if isDeleteMatchInstruction(instruction) {
return deleteIPTablesRule(ctx, c.ipTables, instruction, return deleteIPTablesRule(ctx, c.ipTables, instruction,
c.runner, c.logger) c.runner, c.logger)
@@ -98,7 +85,14 @@ func (c *Config) runIptablesInstructionNoSave(ctx context.Context, instruction s
return nil return nil
} }
func (c *Config) SetIPv4AllPolicies(ctx context.Context, policy string) error { func (c *Config) clearAllRules(ctx context.Context) error {
return c.runMixedIptablesInstructions(ctx, []string{
"--flush", // flush all chains
"--delete-chain", // delete all chains
})
}
func (c *Config) setIPv4AllPolicies(ctx context.Context, policy string) error {
switch policy { switch policy {
case "ACCEPT", "DROP": case "ACCEPT", "DROP":
default: default:
@@ -111,19 +105,22 @@ func (c *Config) SetIPv4AllPolicies(ctx context.Context, policy string) error {
}) })
} }
func (c *Config) AcceptInputThroughInterface(ctx context.Context, intf string) error { func (c *Config) acceptInputThroughInterface(ctx context.Context, intf string, remove bool) error {
return c.runMixedIptablesInstruction(ctx, fmt.Sprintf( return c.runMixedIptablesInstruction(ctx, fmt.Sprintf(
"--append INPUT -i %s -j ACCEPT", intf)) "%s INPUT -i %s -j ACCEPT", appendOrDelete(remove), intf,
))
} }
func (c *Config) AcceptInputToSubnet(ctx context.Context, intf string, destination netip.Prefix) error { func (c *Config) acceptInputToSubnet(ctx context.Context, intf string,
destination netip.Prefix, remove bool,
) error {
interfaceFlag := "-i " + intf interfaceFlag := "-i " + intf
if intf == "*" { // all interfaces if intf == "*" { // all interfaces
interfaceFlag = "" interfaceFlag = ""
} }
instruction := fmt.Sprintf("--append INPUT %s -d %s -j ACCEPT", instruction := fmt.Sprintf("%s INPUT %s -d %s -j ACCEPT",
interfaceFlag, destination.String()) appendOrDelete(remove), interfaceFlag, destination.String())
if destination.Addr().Is4() { if destination.Addr().Is4() {
return c.runIptablesInstruction(ctx, instruction) return c.runIptablesInstruction(ctx, instruction)
@@ -134,23 +131,26 @@ func (c *Config) AcceptInputToSubnet(ctx context.Context, intf string, destinati
return c.runIP6tablesInstruction(ctx, instruction) return c.runIP6tablesInstruction(ctx, instruction)
} }
func (c *Config) AcceptOutputThroughInterface(ctx context.Context, intf string, remove bool) error { func (c *Config) acceptOutputThroughInterface(ctx context.Context, intf string, remove bool) error {
return c.runMixedIptablesInstruction(ctx, fmt.Sprintf( return c.runMixedIptablesInstruction(ctx, fmt.Sprintf(
"%s OUTPUT -o %s -j ACCEPT", appendOrDelete(remove), intf, "%s OUTPUT -o %s -j ACCEPT", appendOrDelete(remove), intf,
)) ))
} }
func (c *Config) AcceptEstablishedRelatedTraffic(ctx context.Context) error { func (c *Config) acceptEstablishedRelatedTraffic(ctx context.Context, remove bool) error {
return c.runMixedIptablesInstructions(ctx, []string{ return c.runMixedIptablesInstructions(ctx, []string{
"--append OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT", fmt.Sprintf("%s OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT", appendOrDelete(remove)),
"--append INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT", fmt.Sprintf("%s INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT", appendOrDelete(remove)),
}) })
} }
func (c *Config) AcceptOutputTrafficToVPN(ctx context.Context, func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
defaultInterface string, connection models.Connection, remove bool, defaultInterface string, connection models.Connection, remove bool,
) error { ) error {
protocol := connection.Protocol protocol := connection.Protocol
if protocol == "tcp-client" {
protocol = "tcp"
}
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT", instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
appendOrDelete(remove), connection.IP, defaultInterface, protocol, appendOrDelete(remove), connection.IP, defaultInterface, protocol,
protocol, connection.Port) protocol, connection.Port)
@@ -162,29 +162,8 @@ func (c *Config) AcceptOutputTrafficToVPN(ctx context.Context,
return c.runIP6tablesInstruction(ctx, instruction) return c.runIP6tablesInstruction(ctx, instruction)
} }
func (c *Config) AcceptOutput(ctx context.Context,
protocol, intf string, ip netip.Addr, port uint16, remove bool,
) error {
interfaceFlag := "-o " + intf
if intf == "*" { // all interfaces
interfaceFlag = ""
}
instruction := fmt.Sprintf("%s OUTPUT -d %s %s -p %s -m %s --dport %d -j ACCEPT",
appendOrDelete(remove), ip, interfaceFlag, protocol, protocol, port)
if ip.Is4() {
return c.runIptablesInstruction(ctx, instruction)
} else if c.ip6Tables == "" {
return fmt.Errorf("accept output to VPN server: %w", ErrNeedIP6Tables)
}
return c.runIP6tablesInstruction(ctx, instruction)
}
// AcceptOutputFromIPToSubnet accepts outgoing traffic from sourceIP to destinationSubnet
// on the interface intf. If intf is empty, it is set to "*" which means all interfaces.
// If remove is true, the rule is removed instead of added.
// Thanks to @npawelek. // Thanks to @npawelek.
func (c *Config) AcceptOutputFromIPToSubnet(ctx context.Context, func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context,
intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool, intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool,
) error { ) error {
doIPv4 := sourceIP.Is4() && destinationSubnet.Addr().Is4() doIPv4 := sourceIP.Is4() && destinationSubnet.Addr().Is4()
@@ -205,24 +184,21 @@ func (c *Config) AcceptOutputFromIPToSubnet(ctx context.Context,
return c.runIP6tablesInstruction(ctx, instruction) return c.runIP6tablesInstruction(ctx, instruction)
} }
// AcceptIpv6MulticastOutput accepts outgoing traffic to the IPv6 multicast address // NDP uses multicast address (theres no broadcast in IPv6 like ARP uses in IPv4).
// ff02::1:ff00:0/104, which is used for NDP (Neighbor Discovery Protocol) to resolve func (c *Config) acceptIpv6MulticastOutput(ctx context.Context,
// IPv6 addresses to MAC addresses. If intf is empty, it is set to "*" which means intf string, remove bool,
// all interfaces. If remove is true, the rule is removed instead of added. ) error {
func (c *Config) AcceptIpv6MulticastOutput(ctx context.Context, intf string) error {
interfaceFlag := "-o " + intf interfaceFlag := "-o " + intf
if intf == "*" { // all interfaces if intf == "*" { // all interfaces
interfaceFlag = "" interfaceFlag = ""
} }
instruction := fmt.Sprintf("--append OUTPUT %s -d ff02::1:ff00:0/104 -j ACCEPT", interfaceFlag) instruction := fmt.Sprintf("%s OUTPUT %s -d ff02::1:ff00:0/104 -j ACCEPT",
appendOrDelete(remove), interfaceFlag)
return c.runIP6tablesInstruction(ctx, instruction) return c.runIP6tablesInstruction(ctx, instruction)
} }
// AcceptInputToPort accepts incoming traffic on the specified port, for both TCP and UDP // Used for port forwarding, with intf set to tun.
// protocols, on the interface intf. If intf is empty, it is set to "*" which means all interfaces. func (c *Config) acceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error {
// If remove is true, the rule is removed instead of added. This is used for port forwarding, with
// intf set to the VPN tunnel interface.
func (c *Config) AcceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error {
interfaceFlag := "-i " + intf interfaceFlag := "-i " + intf
if intf == "*" { // all interfaces if intf == "*" { // all interfaces
interfaceFlag = "" interfaceFlag = ""
@@ -233,12 +209,8 @@ func (c *Config) AcceptInputToPort(ctx context.Context, intf string, port uint16
}) })
} }
// RedirectPort redirects incoming traffic on the specified source port to the // Used for VPN server side port forwarding, with intf set to the VPN tunnel interface.
// specified destination port, for both TCP and UDP protocols, on the interface intf. func (c *Config) redirectPort(ctx context.Context, intf string,
// If intf is empty, it is set to "*" which means all interfaces. If remove is true,
// the redirection is removed instead of added. This is used for VPN server side
// port forwarding, with intf set to the VPN tunnel interface.
func (c *Config) RedirectPort(ctx context.Context, intf string,
sourcePort, destinationPort uint16, remove bool, sourcePort, destinationPort uint16, remove bool,
) (err error) { ) (err error) {
interfaceFlag := "-i " + intf interfaceFlag := "-i " + intf
@@ -246,15 +218,7 @@ func (c *Config) RedirectPort(ctx context.Context, intf string,
interfaceFlag = "" interfaceFlag = ""
} }
c.iptablesMutex.Lock() err = c.runIptablesInstructions(ctx, []string{
defer c.iptablesMutex.Unlock()
restore, err := c.saveAndRestore(ctx)
if err != nil {
return err
}
err = c.runIptablesInstructionsNoSave(ctx, []string{
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d", fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort), appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
fmt.Sprintf("%s INPUT %s -p tcp -m tcp --dport %d -j ACCEPT", fmt.Sprintf("%s INPUT %s -p tcp -m tcp --dport %d -j ACCEPT",
@@ -265,12 +229,11 @@ func (c *Config) RedirectPort(ctx context.Context, intf string,
appendOrDelete(remove), interfaceFlag, destinationPort), appendOrDelete(remove), interfaceFlag, destinationPort),
}) })
if err != nil { if err != nil {
restore(ctx)
return fmt.Errorf("redirecting IPv4 source port %d to destination port %d on interface %s: %w", return fmt.Errorf("redirecting IPv4 source port %d to destination port %d on interface %s: %w",
sourcePort, destinationPort, intf, err) sourcePort, destinationPort, intf, err)
} }
err = c.runIP6tablesInstructionsNoSave(ctx, []string{ err = c.runIP6tablesInstructions(ctx, []string{
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d", fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort), appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
fmt.Sprintf("%s INPUT %s -p tcp -m tcp --dport %d -j ACCEPT", fmt.Sprintf("%s INPUT %s -p tcp -m tcp --dport %d -j ACCEPT",
@@ -281,7 +244,6 @@ func (c *Config) RedirectPort(ctx context.Context, intf string,
appendOrDelete(remove), interfaceFlag, destinationPort), appendOrDelete(remove), interfaceFlag, destinationPort),
}) })
if err != nil { if err != nil {
restore(ctx) // just in case
errMessage := err.Error() errMessage := err.Error()
if strings.Contains(errMessage, "can't initialize ip6tables table `nat': Table does not exist") { if strings.Contains(errMessage, "can't initialize ip6tables table `nat': Table does not exist") {
if !remove { if !remove {
@@ -295,7 +257,7 @@ func (c *Config) RedirectPort(ctx context.Context, intf string,
return nil return nil
} }
func (c *Config) RunUserPostRules(ctx context.Context, filepath string) error { func (c *Config) runUserPostRules(ctx context.Context, filepath string, remove bool) error {
file, err := os.OpenFile(filepath, os.O_RDONLY, 0) file, err := os.OpenFile(filepath, os.O_RDONLY, 0)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil return nil
@@ -311,15 +273,16 @@ func (c *Config) RunUserPostRules(ctx context.Context, filepath string) error {
return err return err
} }
lines := strings.Split(string(b), "\n") lines := strings.Split(string(b), "\n")
successfulRules := []string{}
c.iptablesMutex.Lock() defer func() {
defer c.iptablesMutex.Unlock() // transaction-like rollback
if err == nil || ctx.Err() != nil {
restore, err := c.saveAndRestore(ctx) return
if err != nil {
return err
} }
for _, rule := range successfulRules {
_ = c.runIptablesInstruction(ctx, flipRule(rule))
}
}()
for _, line := range lines { for _, line := range lines {
var ipv4 bool var ipv4 bool
var rule string var rule string
@@ -346,18 +309,23 @@ func (c *Config) RunUserPostRules(ctx context.Context, filepath string) error {
continue continue
} }
if remove {
rule = flipRule(rule)
}
switch { switch {
case ipv4: case ipv4:
err = c.runIptablesInstructionNoSave(ctx, rule) err = c.runIptablesInstruction(ctx, rule)
case c.ip6Tables == "": case c.ip6Tables == "":
err = fmt.Errorf("running user ip6tables rule: %w", ErrNeedIP6Tables) err = fmt.Errorf("running user ip6tables rule: %w", ErrNeedIP6Tables)
default: // ipv6 default: // ipv6
err = c.runIP6tablesInstructionNoSave(ctx, rule) err = c.runIP6tablesInstruction(ctx, rule)
} }
if err != nil { if err != nil {
restore(ctx)
return err return err
} }
successfulRules = append(successfulRules, rule)
} }
return nil return nil
} }
-87
View File
@@ -1,87 +0,0 @@
package iptables
import (
"context"
"fmt"
"os/exec"
"strings"
)
// SaveAndRestore saves the current iptables and ip6tables rules and
// returns a restore function that can be called to restore the saved rules.
func (c *Config) SaveAndRestore(ctx context.Context) (restore func(context.Context), err error) {
c.iptablesMutex.Lock()
defer c.iptablesMutex.Unlock()
return c.saveAndRestore(ctx)
}
// callers MUST always lock both the [Config] iptablesMutex and the ip6tablesMutex
// before calling this function. Note the restore function does not interact with mutexes
// so the caller must make sure the mutexes are locked when calling the restore function.
func (c *Config) saveAndRestore(ctx context.Context) (restore func(context.Context), err error) {
restoreIPv4, err := c.saveAndRestoreIPv4(ctx)
if err != nil {
return nil, err
}
restoreIPv6, err := c.saveAndRestoreIPv6(ctx)
if err != nil {
return nil, err
}
restore = func(ctx context.Context) {
restoreIPv4(ctx)
if restoreIPv6 != nil {
restoreIPv6(ctx)
}
}
return restore, nil
}
// Callers of saveAndRestoreIPv4 MUST always lock the [Config] iptablesMutex
// before calling this function.
func (c *Config) saveAndRestoreIPv4(ctx context.Context) (restore func(context.Context), err error) {
cmd := exec.CommandContext(ctx, c.ipTables+"-save") //nolint:gosec
data, err := c.runner.Run(cmd)
if err != nil {
return nil, fmt.Errorf("saving IPv4 iptables: %w", err)
}
restore = func(ctx context.Context) {
cmd := exec.CommandContext(ctx, c.ipTables+"-restore") //nolint:gosec
cmd.Stdin = strings.NewReader(data)
output, err := c.runner.Run(cmd)
if err != nil {
c.logger.Warn(fmt.Sprintf("restoring IPv4 iptables failed: %s", makeRestoreErrorMessage(err, output, data)))
}
}
return restore, nil
}
// Callers of saveAndRestoreIPv6 MUST always lock the [Config] ip6tablesMutex
// before calling this function.
func (c *Config) saveAndRestoreIPv6(ctx context.Context) (restore func(context.Context), err error) {
if c.ip6Tables == "" {
return nil, nil //nolint:nilnil
}
cmd := exec.CommandContext(ctx, c.ip6Tables+"-save") //nolint:gosec
data, err := c.runner.Run(cmd)
if err != nil {
return nil, fmt.Errorf("saving IPv6 iptables: %w", err)
}
restore = func(ctx context.Context) {
cmd = exec.CommandContext(ctx, c.ip6Tables+"-restore") //nolint:gosec
cmd.Stdin = strings.NewReader(data)
output, err := c.runner.Run(cmd)
if err != nil {
c.logger.Warn(fmt.Sprintf("restoring IPv6 iptables failed: %s", makeRestoreErrorMessage(err, output, data)))
}
}
return restore, nil
}
func makeRestoreErrorMessage(err error, output, data string) string {
return fmt.Sprintf("%s: %s: restoring from data:\n%s", err, output, data)
}
-35
View File
@@ -1,35 +0,0 @@
package iptables
import (
"context"
"sync"
)
type Config struct {
runner CmdRunner
logger Logger
iptablesMutex sync.Mutex
// Fixed state
ipTables string
ip6Tables string
}
func New(ctx context.Context, runner CmdRunner, logger Logger) (*Config, error) {
iptables, err := checkIptablesSupport(ctx, runner, "iptables", "iptables-nft", "iptables-legacy")
if err != nil {
return nil, err
}
ip6tables, err := findIP6tablesSupported(ctx, runner)
if err != nil {
return nil, err
}
return &Config{
runner: runner,
logger: logger,
ipTables: iptables,
ip6Tables: ip6tables,
}, nil
}
-12
View File
@@ -1,12 +0,0 @@
package iptables
import "os/exec"
type CmdRunner interface {
Run(cmd *exec.Cmd) (output string, err error)
}
type Logger interface {
Debug(s string)
Warn(s string)
}
-45
View File
@@ -1,45 +0,0 @@
package iptables
import (
"context"
)
func (c *Config) runMixedIptablesInstructions(ctx context.Context, instructions []string) error {
c.iptablesMutex.Lock()
defer c.iptablesMutex.Unlock()
restore, err := c.saveAndRestore(ctx)
if err != nil {
return err
}
for _, instruction := range instructions {
if err := c.runMixedIptablesInstructionNoSave(ctx, instruction); err != nil {
restore(ctx)
return err
}
}
return nil
}
func (c *Config) runMixedIptablesInstruction(ctx context.Context, instruction string) error {
c.iptablesMutex.Lock()
defer c.iptablesMutex.Unlock()
restore, err := c.saveAndRestore(ctx)
if err != nil {
return err
}
err = c.runMixedIptablesInstructionNoSave(ctx, instruction)
if err != nil {
restore(ctx)
}
return err
}
func (c *Config) runMixedIptablesInstructionNoSave(ctx context.Context, instruction string) error {
if err := c.runIptablesInstructionNoSave(ctx, instruction); err != nil {
return err
}
return c.runIP6tablesInstructionNoSave(ctx, instruction)
}
-98
View File
@@ -1,98 +0,0 @@
package iptables
import (
"context"
"errors"
"fmt"
"net/netip"
"os"
)
type tcpFlags struct {
mask []tcpFlag
comparison []tcpFlag
}
type tcpFlag uint8
const (
tcpFlagFIN tcpFlag = 1 << iota
tcpFlagSYN
tcpFlagRST
tcpFlagPSH
tcpFlagACK
tcpFlagURG
tcpFlagECE
tcpFlagCWR
)
func (f tcpFlag) String() string {
switch f {
case tcpFlagFIN:
return "FIN"
case tcpFlagSYN:
return "SYN"
case tcpFlagRST:
return "RST"
case tcpFlagPSH:
return "PSH"
case tcpFlagACK:
return "ACK"
case tcpFlagURG:
return "URG"
case tcpFlagECE:
return "ECE"
case tcpFlagCWR:
return "CWR"
default:
panic(fmt.Sprintf("%s: %d", errTCPFlagUnknown, f))
}
}
var errTCPFlagUnknown = errors.New("unknown TCP flag")
func parseTCPFlag(s string) (tcpFlag, error) {
allFlags := []tcpFlag{
tcpFlagFIN, tcpFlagSYN, tcpFlagRST, tcpFlagPSH,
tcpFlagACK, tcpFlagURG, tcpFlagECE, tcpFlagCWR,
}
for _, flag := range allFlags {
if s == fmt.Sprintf("%#02x", uint8(flag)) || s == flag.String() {
return flag, nil
}
}
return 0, fmt.Errorf("%w: %s", errTCPFlagUnknown, s)
}
var ErrMarkMatchModuleMissing = errors.New("kernel is missing the mark module libxt_mark.so")
// TempDropOutputTCPRST temporarily drops outgoing TCP RST packets to the specified address and port,
// for any TCP packets not marked with the excludeMark given.
// This is necessary for TCP path MTU discovery to work, as the kernel will try to terminate the connection
// by sending a TCP RST packet, although we want to handle the connection manually.
func (c *Config) TempDropOutputTCPRST(ctx context.Context,
src, dst netip.AddrPort, excludeMark int) (
revert func(ctx context.Context) error, err error,
) {
_, err = os.Stat("/usr/lib/xtables/libxt_mark.so")
if err != nil && errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w", ErrMarkMatchModuleMissing)
}
const template = "%s OUTPUT -p tcp -s %s --sport %d -d %s --dport %d " +
"--tcp-flags RST RST -m mark ! --mark %d -j DROP" //nolint:dupword
instruction := fmt.Sprintf(template, "--append", src.Addr(), src.Port(), dst.Addr(), dst.Port(), excludeMark)
revertInstruction := fmt.Sprintf(template, "--delete", src.Addr(), src.Port(), dst.Addr(), dst.Port(), excludeMark)
run := c.runIptablesInstruction
if dst.Addr().Is6() {
run = c.runIP6tablesInstruction
}
revert = func(ctx context.Context) error {
return run(ctx, revertInstruction)
}
err = run(ctx, instruction)
if err != nil {
return nil, fmt.Errorf("running instruction: %w", err)
}
return revert, nil
}
+21
View File
@@ -0,0 +1,21 @@
package firewall
import (
"context"
)
func (c *Config) runMixedIptablesInstructions(ctx context.Context, instructions []string) error {
for _, instruction := range instructions {
if err := c.runMixedIptablesInstruction(ctx, instruction); err != nil {
return err
}
}
return nil
}
func (c *Config) runMixedIptablesInstruction(ctx context.Context, instruction string) error {
if err := c.runIptablesInstruction(ctx, instruction); err != nil {
return err
}
return c.runIP6tablesInstruction(ctx, instruction)
}
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"errors" "errors"
@@ -26,18 +26,10 @@ type chainRule struct {
inputInterface string // input interface, for example "tun0" or "*"" inputInterface string // input interface, for example "tun0" or "*""
outputInterface string // output interface, for example "eth0" or "*"" outputInterface string // output interface, for example "eth0" or "*""
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid. source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
sourcePort uint16 // Not specified if set to zero.
destination netip.Prefix // destination IP CIDR, for example 0.0.0.0/0. Must be valid. destination netip.Prefix // destination IP CIDR, for example 0.0.0.0/0. Must be valid.
destinationPort uint16 // Not specified if set to zero. destinationPort uint16 // Not specified if set to zero.
redirPorts []uint16 // Not specified if empty. redirPorts []uint16 // Not specified if empty.
ctstate []string // for example ["RELATED","ESTABLISHED"]. Can be empty. ctstate []string // for example ["RELATED","ESTABLISHED"]. Can be empty.
tcpFlags tcpFlags
mark mark
}
type mark struct {
invert bool
value uint
} }
var ErrChainListMalformed = errors.New("iptables chain list output is malformed") var ErrChainListMalformed = errors.New("iptables chain list output is malformed")
@@ -249,23 +241,19 @@ func parseChainRuleField(fieldIndex int, field string, rule *chainRule) (err err
} }
func parseChainRuleOptionalFields(optionalFields []string, rule *chainRule) (err error) { func parseChainRuleOptionalFields(optionalFields []string, rule *chainRule) (err error) {
i := 0 for i := 0; i < len(optionalFields); i++ {
for i < len(optionalFields) { key := optionalFields[i]
switch optionalFields[i] { switch key {
case "udp": case "tcp", "udp":
i++ i++
consumed, err := parseUDPOptional(optionalFields[i:], rule) value := optionalFields[i]
value = strings.TrimPrefix(value, "dpt:")
const base, bitLength = 10, 16
destinationPort, err := strconv.ParseUint(value, base, bitLength)
if err != nil { if err != nil {
return fmt.Errorf("parsing UDP optional fields: %w", err) return fmt.Errorf("parsing destination port %q: %w", value, err)
} }
i += consumed rule.destinationPort = uint16(destinationPort)
case "tcp":
i++
consumed, err := parseTCPOptional(optionalFields[i:], rule)
if err != nil {
return fmt.Errorf("parsing TCP optional fields: %w", err)
}
i += consumed
case "redir": case "redir":
i++ i++
switch optionalFields[i] { switch optionalFields[i] {
@@ -276,136 +264,20 @@ func parseChainRuleOptionalFields(optionalFields []string, rule *chainRule) (err
return fmt.Errorf("parsing redirection ports: %w", err) return fmt.Errorf("parsing redirection ports: %w", err)
} }
rule.redirPorts = ports rule.redirPorts = ports
i++
default:
return fmt.Errorf("%w: unexpected %q after redir",
ErrChainRuleMalformed, optionalFields[1])
}
case "ctstate":
i++
rule.ctstate = strings.Split(optionalFields[i], ",")
i++
case "mark":
i++
mark, consumed, err := parseMark(optionalFields[i:])
if err != nil {
return fmt.Errorf("parsing mark: %w", err)
}
rule.mark = mark
i += consumed
default: default:
return fmt.Errorf("%w: unexpected optional field: %s", return fmt.Errorf("%w: unexpected optional field: %s",
ErrChainRuleMalformed, optionalFields[i]) ErrChainRuleMalformed, optionalFields[i])
} }
case "ctstate":
i++
rule.ctstate = strings.Split(optionalFields[i], ",")
default:
return fmt.Errorf("%w: unexpected optional field: %s", ErrChainRuleMalformed, key)
}
} }
return nil return nil
} }
var errUDPOptionalUnknown = errors.New("unknown UDP optional field")
func parseUDPOptional(optionalFields []string, rule *chainRule) (consumed int, err error) {
for _, value := range optionalFields {
if !strings.ContainsRune(value, ':') {
// no longer a UDP-associated option
return consumed, nil
}
switch {
case strings.HasPrefix(value, "dpt:"):
rule.destinationPort, err = parseDestinationPort(value)
if err != nil {
return 0, fmt.Errorf("parsing destination port: %w", err)
}
consumed++
case strings.HasPrefix(value, "spt:"):
rule.sourcePort, err = parseSourcePort(value)
if err != nil {
return 0, fmt.Errorf("parsing source port: %w", err)
}
consumed++
default:
return 0, fmt.Errorf("%w: %s", errUDPOptionalUnknown, value)
}
}
return consumed, nil
}
var errTCPOptionalUnknown = errors.New("unknown TCP optional field")
func parseTCPOptional(optionalFields []string, rule *chainRule) (consumed int, err error) {
for _, value := range optionalFields {
if !strings.ContainsRune(value, ':') {
// no longer a TCP-associated option
return consumed, nil
}
switch {
case strings.HasPrefix(value, "dpt:"):
rule.destinationPort, err = parseDestinationPort(value)
if err != nil {
return 0, fmt.Errorf("parsing destination port: %w", err)
}
consumed++
case strings.HasPrefix(value, "spt:"):
rule.sourcePort, err = parseSourcePort(value)
if err != nil {
return 0, fmt.Errorf("parsing source port: %w", err)
}
consumed++
case strings.HasPrefix(value, "flags:"):
rule.tcpFlags, err = parseTCPFlags(value)
if err != nil {
return 0, fmt.Errorf("parsing TCP flags: %w", err)
}
consumed++
default:
return 0, fmt.Errorf("%w: %s", errTCPOptionalUnknown, value)
}
}
return consumed, nil
}
func parseDestinationPort(value string) (port uint16, err error) {
value = strings.TrimPrefix(value, "dpt:")
return parsePort(value)
}
func parseSourcePort(value string) (port uint16, err error) {
value = strings.TrimPrefix(value, "spt:")
return parsePort(value)
}
var errTCPFlagsMalformed = errors.New("TCP flags are malformed")
func parseTCPFlags(value string) (tcpFlags, error) {
value = strings.TrimPrefix(value, "flags:")
fields := strings.Split(value, "/")
const expectedFields = 2
if len(fields) != expectedFields {
return tcpFlags{}, fmt.Errorf("%w: expected format 'flags:<mask>/<comparison>' in %q",
errTCPFlagsMalformed, value)
}
maskFlags := strings.Split(fields[0], ",")
mask := make([]tcpFlag, len(maskFlags))
var err error
for i, maskFlag := range maskFlags {
mask[i], err = parseTCPFlag(maskFlag)
if err != nil {
return tcpFlags{}, fmt.Errorf("parsing TCP mask flags: %w", err)
}
}
comparisonFlags := strings.Split(fields[1], ",")
comparison := make([]tcpFlag, len(comparisonFlags))
for i, comparisonFlag := range comparisonFlags {
comparison[i], err = parseTCPFlag(comparisonFlag)
if err != nil {
return tcpFlags{}, fmt.Errorf("parsing TCP comparison flags: %w", err)
}
}
return tcpFlags{
mask: mask,
comparison: comparison,
}, nil
}
func parsePortsCSV(s string) (ports []uint16, err error) { func parsePortsCSV(s string) (ports []uint16, err error) {
if s == "" { if s == "" {
return nil, nil return nil, nil
@@ -414,40 +286,16 @@ func parsePortsCSV(s string) (ports []uint16, err error) {
fields := strings.Split(s, ",") fields := strings.Split(s, ",")
ports = make([]uint16, len(fields)) ports = make([]uint16, len(fields))
for i, field := range fields { for i, field := range fields {
ports[i], err = parsePort(field) const base, bitLength = 10, 16
port, err := strconv.ParseUint(field, base, bitLength)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("parsing port %q: %w", field, err)
} }
ports[i] = uint16(port)
} }
return ports, nil return ports, nil
} }
var errMarkValueMalformed = errors.New("mark value is malformed")
func parseMark(optionalFields []string) (m mark, consumed int, err error) {
switch optionalFields[consumed] {
case "match":
consumed++
if optionalFields[consumed] == "!" {
m.invert = true
consumed++
}
const base = 0 // auto-detect
const bits = 32
value, err := strconv.ParseUint(optionalFields[consumed], base, bits)
if err != nil {
return mark{}, 0, fmt.Errorf("%w: %s", errMarkValueMalformed, optionalFields[consumed])
}
m.value = uint(value)
consumed++
default:
return mark{}, 0, fmt.Errorf("%w: unexpected mark mode field: %s",
ErrChainRuleMalformed, optionalFields[consumed])
}
return m, consumed, nil
}
var ErrLineNumberIsZero = errors.New("line number is zero") var ErrLineNumberIsZero = errors.New("line number is zero")
func parseLineNumber(s string) (n uint16, err error) { func parseLineNumber(s string) (n uint16, err error) {
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"net/netip" "net/netip"
@@ -1,3 +1,3 @@
package iptables package firewall
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . CmdRunner,Logger //go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . CmdRunner,Logger
@@ -1,8 +1,8 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/firewall/iptables (interfaces: CmdRunner,Logger) // Source: github.com/qdm12/gluetun/internal/firewall (interfaces: CmdRunner,Logger)
// Package iptables is a generated GoMock package. // Package firewall is a generated GoMock package.
package iptables package firewall
import ( import (
exec "os/exec" exec "os/exec"
@@ -84,6 +84,30 @@ func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
} }
// Error mocks base method.
func (m *MockLogger) Error(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Error", arg0)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
}
// Info mocks base method.
func (m *MockLogger) Info(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Info", arg0)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
}
// Warn mocks base method. // Warn mocks base method.
func (m *MockLogger) Warn(arg0 string) { func (m *MockLogger) Warn(arg0 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
+2 -2
View File
@@ -48,7 +48,7 @@ func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Pref
} }
firewallUpdated = true firewallUpdated = true
err := c.impl.AcceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface, err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
defaultRoute.AssignedIP, subNet, remove) defaultRoute.AssignedIP, subNet, remove)
if err != nil { if err != nil {
c.logger.Error("cannot remove outdated outbound subnet: " + err.Error()) c.logger.Error("cannot remove outdated outbound subnet: " + err.Error())
@@ -77,7 +77,7 @@ func (c *Config) addOutboundSubnets(ctx context.Context, subnets []netip.Prefix)
} }
firewallUpdated = true firewallUpdated = true
err := c.impl.AcceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface, err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
defaultRoute.AssignedIP, subnet, remove) defaultRoute.AssignedIP, subnet, remove)
if err != nil { if err != nil {
return err return err
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"errors" "errors"
@@ -18,13 +18,10 @@ type iptablesInstruction struct {
inputInterface string // for example "tun0" or "" for any interface. inputInterface string // for example "tun0" or "" for any interface.
outputInterface string // for example "tun0" or "" for any interface. outputInterface string // for example "tun0" or "" for any interface.
source netip.Prefix // if not valid, then it is unspecified. source netip.Prefix // if not valid, then it is unspecified.
sourcePort uint16 // if zero, there is no source port
destination netip.Prefix // if not valid, then it is unspecified. destination netip.Prefix // if not valid, then it is unspecified.
destinationPort uint16 // if zero, there is no destination port destinationPort uint16 // if zero, there is no destination port
toPorts []uint16 // if empty, there is no redirection toPorts []uint16 // if empty, there is no redirection
ctstate []string // if empty, there is no ctstate ctstate []string // if empty, there is no ctstate
tcpFlags tcpFlags
mark mark
} }
func (i *iptablesInstruction) setDefaults() { func (i *iptablesInstruction) setDefaults() {
@@ -46,8 +43,6 @@ func (i *iptablesInstruction) equalToRule(table, chain string, rule chainRule) (
return false return false
case i.destinationPort != rule.destinationPort: case i.destinationPort != rule.destinationPort:
return false return false
case i.sourcePort != rule.sourcePort:
return false
case !slices.Equal(i.toPorts, rule.redirPorts): case !slices.Equal(i.toPorts, rule.redirPorts):
return false return false
case !slices.Equal(i.ctstate, rule.ctstate): case !slices.Equal(i.ctstate, rule.ctstate):
@@ -60,11 +55,6 @@ func (i *iptablesInstruction) equalToRule(table, chain string, rule chainRule) (
return false return false
case !ipPrefixesEqual(i.destination, rule.destination): case !ipPrefixesEqual(i.destination, rule.destination):
return false return false
case !slices.Equal(i.tcpFlags.mask, rule.tcpFlags.mask) ||
!slices.Equal(i.tcpFlags.comparison, rule.tcpFlags.comparison):
return false
case i.mark != rule.mark:
return false
default: default:
return true return true
} }
@@ -87,29 +77,26 @@ func parseIptablesInstruction(s string) (instruction iptablesInstruction, err er
return iptablesInstruction{}, fmt.Errorf("%w: empty instruction", ErrIptablesCommandMalformed) return iptablesInstruction{}, fmt.Errorf("%w: empty instruction", ErrIptablesCommandMalformed)
} }
fields := strings.Fields(s) fields := strings.Fields(s)
if len(fields)%2 != 0 {
return iptablesInstruction{}, fmt.Errorf("%w: fields count %d is not even: %q",
ErrIptablesCommandMalformed, len(fields), s)
}
i := 0 for i := 0; i < len(fields); i += 2 {
for i < len(fields) { key := fields[i]
consumed, err := parseInstructionFlag(fields[i:], &instruction) value := fields[i+1]
err = parseInstructionFlag(key, value, &instruction)
if err != nil { if err != nil {
return iptablesInstruction{}, fmt.Errorf("parsing %q: %w", s, err) return iptablesInstruction{}, fmt.Errorf("parsing %q: %w", s, err)
} }
i += consumed
} }
instruction.setDefaults() instruction.setDefaults()
return instruction, nil return instruction, nil
} }
func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (consumed int, err error) { func parseInstructionFlag(key, value string, instruction *iptablesInstruction) (err error) {
consumed, err = preCheckInstructionFields(fields) switch key {
if err != nil {
return 0, err
}
flag := fields[0]
value := fields[1]
switch flag {
case "-t", "--table": case "-t", "--table":
instruction.table = value instruction.table = value
case "-D", "--delete": case "-D", "--delete":
@@ -122,19 +109,7 @@ func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (co
instruction.target = value instruction.target = value
case "-p", "--protocol": case "-p", "--protocol":
instruction.protocol = value instruction.protocol = value
case "-m", "--match": case "-m", "--match": // ignore match
consumed, err = parseMatchModule(fields, instruction)
if err != nil {
return 0, fmt.Errorf("parsing match module: %w", err)
}
case "--mark":
const base = 0 // auto-detect
const bits = 32
value, err := strconv.ParseUint(value, base, bits)
if err != nil {
return 0, fmt.Errorf("parsing mark value %q: %w", fields[2], err)
}
instruction.mark.value = uint(value)
case "-i", "--in-interface": case "-i", "--in-interface":
instruction.inputInterface = value instruction.inputInterface = value
case "-o", "--out-interface": case "-o", "--out-interface":
@@ -142,61 +117,37 @@ func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (co
case "-s", "--source": case "-s", "--source":
instruction.source, err = parseIPPrefix(value) instruction.source, err = parseIPPrefix(value)
if err != nil { if err != nil {
return 0, fmt.Errorf("parsing source IP CIDR: %w", err) return fmt.Errorf("parsing source IP CIDR: %w", err)
}
case "--sport":
instruction.sourcePort, err = parsePort(value)
if err != nil {
return 0, fmt.Errorf("parsing source port: %w", err)
} }
case "-d", "--destination": case "-d", "--destination":
instruction.destination, err = parseIPPrefix(value) instruction.destination, err = parseIPPrefix(value)
if err != nil { if err != nil {
return 0, fmt.Errorf("parsing destination IP CIDR: %w", err) return fmt.Errorf("parsing destination IP CIDR: %w", err)
} }
case "--dport": case "--dport":
instruction.destinationPort, err = parsePort(value) const base, bitLength = 10, 16
destinationPort, err := strconv.ParseUint(value, base, bitLength)
if err != nil { if err != nil {
return 0, fmt.Errorf("parsing destination port: %w", err) return fmt.Errorf("parsing destination port: %w", err)
} }
instruction.destinationPort = uint16(destinationPort)
case "--ctstate": case "--ctstate":
instruction.ctstate = strings.Split(value, ",") instruction.ctstate = strings.Split(value, ",")
case "--to-ports": case "--to-ports":
instruction.toPorts, err = parseToPorts(value) portStrings := strings.Split(value, ",")
instruction.toPorts = make([]uint16, len(portStrings))
for i, portString := range portStrings {
const base, bitLength = 10, 16
port, err := strconv.ParseUint(portString, base, bitLength)
if err != nil { if err != nil {
return 0, fmt.Errorf("parsing port redirection: %w", err) return fmt.Errorf("parsing port redirection: %w", err)
} }
case "--tcp-flags": instruction.toPorts[i] = uint16(port)
mask, comparison := value, fields[2]
instruction.tcpFlags, err = parseTCPFlags(mask + "/" + comparison)
if err != nil {
return 0, fmt.Errorf("parsing TCP flags: %w", err)
} }
default: default:
return 0, fmt.Errorf("%w: unknown key %q", ErrIptablesCommandMalformed, flag) return fmt.Errorf("%w: unknown key %q", ErrIptablesCommandMalformed, key)
}
return consumed, nil
}
func preCheckInstructionFields(fields []string) (consumed int, err error) {
flag := fields[0]
// All flags use one value after the flag, except the following:
switch flag {
case "--tcp-flags": // -m can have 1 or 2 values
const expected = 3
if len(fields) < expected {
return 0, fmt.Errorf("%w: flag %q requires at least 2 values, but got %s",
ErrIptablesCommandMalformed, flag, strings.Join(fields, " "))
}
return expected, nil
default:
const expected = 2
if len(fields) < expected {
return 0, fmt.Errorf("%w: flag %q requires a value, but got none",
ErrIptablesCommandMalformed, flag)
}
return expected, nil
} }
return nil
} }
func parseIPPrefix(value string) (prefix netip.Prefix, err error) { func parseIPPrefix(value string) (prefix netip.Prefix, err error) {
@@ -211,52 +162,3 @@ func parseIPPrefix(value string) (prefix netip.Prefix, err error) {
} }
return netip.PrefixFrom(ip, ip.BitLen()), nil return netip.PrefixFrom(ip, ip.BitLen()), nil
} }
func parsePort(value string) (port uint16, err error) {
const base, bitLength = 10, 16
portValue, err := strconv.ParseUint(value, base, bitLength)
if err != nil {
return 0, err
}
return uint16(portValue), nil
}
func parseMatchModule(fields []string, instruction *iptablesInstruction) (
consumed int, err error,
) {
_ = fields[consumed] // -m or --match flag already detected
consumed++
switch fields[consumed] {
case "tcp", "udp":
consumed++
// for now ignore the protocol match since it's auto-loaded
// when parsing the -p/--protocol flag, and we don't need to
// parse it twice.
case "mark":
consumed++
switch fields[consumed] {
case "!":
consumed++
instruction.mark.invert = true
default:
return consumed, fmt.Errorf("%w: unsupported match mark with value: %s",
ErrIptablesCommandMalformed, fields[2])
}
default:
return 0, fmt.Errorf("%w: unknown match value: %s",
ErrIptablesCommandMalformed, fields[consumed])
}
return consumed, nil
}
func parseToPorts(value string) (toPorts []uint16, err error) {
portStrings := strings.Split(value, ",")
toPorts = make([]uint16, len(portStrings))
for i, portString := range portStrings {
toPorts[i], err = parsePort(portString)
if err != nil {
return nil, err
}
}
return toPorts, nil
}
@@ -1,4 +1,4 @@
package iptables package firewall
import ( import (
"net/netip" "net/netip"
@@ -23,7 +23,7 @@ func Test_parseIptablesInstruction(t *testing.T) {
"uneven_fields": { "uneven_fields": {
s: "-A", s: "-A",
errWrapped: ErrIptablesCommandMalformed, errWrapped: ErrIptablesCommandMalformed,
errMessage: "parsing \"-A\": iptables command is malformed: flag \"-A\" requires a value, but got none", errMessage: "iptables command is malformed: fields count 1 is not even: \"-A\"",
}, },
"unknown_key": { "unknown_key": {
s: "-x something", s: "-x something",

Some files were not shown because too many files have changed in this diff Show More