Compare commits

..

1 Commits

Author SHA1 Message Date
Quentin McGaw 801c9a3086 wip 2024-11-08 16:52:47 +00:00
576 changed files with 309916 additions and 21741 deletions
+2 -3
View File
@@ -1,3 +1,2 @@
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
RUN apk add wireguard-tools htop openssl tcpdump iptables
RUN apk add nodejs npm && npm install markdownlint-cli2 --global && apk del npm
FROM qmcgaw/godevcontainer:v0.20-alpine
RUN apk add wireguard-tools htop openssl
+3 -3
View File
@@ -19,16 +19,16 @@ It works on Linux, Windows (WSL2) and OSX.
mkdir -p ~/.ssh
```
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker.
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
1. Select `Dev Containers: Open Folder in Container...` and choose the project directory.
## Customization
For any customization to take effect, you should "rebuild and reopen":
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
2. Select `Dev-Containers: Rebuild Container`
2. Select `Dev Containers: Rebuild Container`
Changes you can make are notably:
-3
View File
@@ -82,9 +82,6 @@
"gopls": {
"usePlaceholders": false,
"staticcheck": true,
"ui.diagnostic.analyses": {
"ST1000": false
},
"formatting.gofumpt": true,
},
"go.lintTool": "golangci-lint",
-1
View File
@@ -7,4 +7,3 @@ Dockerfile
LICENSE
README.md
title.svg
devrun
+2 -2
View File
@@ -4,12 +4,12 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
## Submitting a pull request
1. [Fork](https://github.com/passteque/gluetun/fork) and clone the repository
1. [Fork](https://github.com/qdm12/gluetun/fork) and clone the repository
1. Create a new branch `git checkout -b my-branch-name`
1. Modify the code
1. Ensure the docker build succeeds `docker build .` (you might need `export DOCKER_BUILDKIT=1`)
1. Commit your modifications
1. Push to your fork and [submit a pull request](https://github.com/passteque/gluetun/compare)
1. Push to your fork and [submit a pull request](https://github.com/qdm12/gluetun/compare)
## Resources
+1
View File
@@ -67,6 +67,7 @@ body:
- VPNSecure.me
- VPNUnlimited
- VyprVPN
- WeVPN
- Windscribe
validations:
required: true
+2 -2
View File
@@ -4,8 +4,8 @@ contact_links:
url: https://github.com/qdm12/gluetun-wiki/issues/new/choose
about: Please create an issue on the gluetun-wiki repository.
- name: Configuration help?
url: https://github.com/passteque/gluetun/discussions/new/choose
url: https://github.com/qdm12/gluetun/discussions/new/choose
about: Please create a Github discussion.
- name: Unraid template issue
url: https://github.com/passteque/gluetun/discussions/550
url: https://github.com/qdm12/gluetun/discussions/550
about: Please read the relevant Github discussion.
+7 -30
View File
@@ -6,35 +6,12 @@ labels: ":bulb: New provider"
---
Important notes:
One of the following is required:
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
## For Wireguard
Wireguard can be natively supported ONLY if:
- the `PrivateKey` field value is the same across all servers for one user account
- the `Address` field value is:
- can be found in a structured (JSON etc.) list of servers publicly available; OR
- the same across all servers for one user account
- the `PublicKey` field value is:
- can be found in a structured (JSON etc.) list of servers publicly available; OR
- the same across all servers for one user account
- the `Endpoint` field value:
- can be found in a structured (JSON etc.) list of servers publicly available
- can be determined using a pattern, for example using country codes in hostnames
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
If **all** of these conditions are met, please provide an answer for each of them.
## For OpenVPN
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
- Publicly accessible URL to a zip file containing the Openvpn configuration files
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
If the list of servers requires to login **or** is hidden behind an interactive configurator,
you can only use a custom Openvpn configuration file.
[The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.
+5 -31
View File
@@ -4,38 +4,12 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
interval: "daily"
- package-ecosystem: docker
directory: /
schedule:
interval: "weekly"
- # Servers data dependency that should be updated as soon as
# possible when a new version is released, to have the latest
# servers available
package-ecosystem: "gomod"
directory: "/"
interval: "daily"
- package-ecosystem: gomod
directory: /
schedule:
interval: "weekly"
ignore:
- # In particular avoid amneziawg-go which have v1.x.y versions available
# on the Go modules proxy, but are not in the Github repository tags
# and are not the latest releases either. Most likely a mistake from the
# maintainers, which is persisted on the Go proxy.
dependency-name: "github.com/amnezia-vpn/amneziawg-go"
versions: ["1.x"]
groups:
low-importance:
patterns:
- "github.com/breml/rootcerts"
- "github.com/fatih/color"
- "github.com/golang/mock"
- "github.com/klauspost/compress"
- "github.com/klauspost/pgzip"
- "github.com/pelletier/go-toml/v2"
- "github.com/qdm12/goshutdown"
- "github.com/qdm12/gosplash"
- "github.com/qdm12/gotree"
- "github.com/qdm12/log"
- "github.com/stretchr/testify"
- "github.com/ulikunitz/xz"
- "gopkg.in/ini.v1"
interval: "daily"
+2 -6
View File
@@ -23,8 +23,6 @@
color: "959a9c"
- name: "Closed: ☠️ cannot be done"
color: "959a9c"
- name: "Closed: 🤖🍺 drunk AI"
color: "959a9c"
- name: "Priority: 🚨 Urgent"
color: "03adfc"
@@ -88,6 +86,8 @@
color: "cfe8d4"
- name: "☁️ Vyprvpn"
color: "cfe8d4"
- name: "☁️ WeVPN"
color: "cfe8d4"
- name: "☁️ Windscribe"
color: "cfe8d4"
@@ -128,8 +128,6 @@
color: "ffc7ea"
- name: "Category: Firewall ⛓️"
color: "ffc7ea"
- name: "Category: MTU discovery 🔦"
color: "ffc7ea"
- name: "Category: Routing 🛤️"
color: "ffc7ea"
- name: "Category: IPv6 🛰️"
@@ -140,8 +138,6 @@
color: "ffc7ea"
- name: "Category: Shadowsocks 🔁"
color: "ffc7ea"
- name: "Category: Socks5 proxy 🔁"
color: "ffc7ea"
- name: "Category: control server ⚙️"
color: "ffc7ea"
- name: "Category: kernel 🧠"
-11
View File
@@ -1,11 +0,0 @@
# Description
<!-- Please describe the reason for the changes being proposed. -->
# Issue
<!-- Please link to the issue(s) this change relates to. -->
# Assertions
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
+45 -107
View File
@@ -28,10 +28,6 @@ on:
- go.mod
- go.sum
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-latest
@@ -41,14 +37,14 @@ jobs:
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
exclude: |
./.golangci.yml
./internal/storage/servers.json
*.md
- name: Linting
@@ -63,92 +59,39 @@ jobs:
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm --cap-add=NET_ADMIN --device /dev/net/tun \
docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Verify dev cross platform compatibility
run: docker build --target xcompile .
- name: Build final image
run: docker build -t final-image .
verify-tools:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@v6
- name: Run Wireguard
if: |
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
)
run: |
docker run -it --rm --cap-add=NET_ADMIN -e VPNSP=mullvad -e VPN_TYPE=wireguard \
-e WIREGUARD_PRIVATE_KEY=${{ secrets.WIREGUARD_PRIVATE_KEY }} \
-e WIREGUARD_ADDRESS=${{ secrets.WIREGUARD_ADDRESS }} \
final-image \
exit-once-connected
- uses: actions/setup-go@v6
with:
go-version-file: ./devrun/go.mod
- run: go test ./...
working-directory: ./devrun
- uses: actions/setup-go@v6
with:
go-version-file: ./ci/go.mod
- run: go test ./...
working-directory: ./ci
verify-private:
if: |
github.repository == 'passteque/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify]
runs-on: ubuntu-latest
environment: secrets
steps:
- uses: actions/checkout@v6
- run: docker build -t qmcgaw/gluetun .
- name: Setup Go for CI utility
uses: actions/setup-go@v6
with:
go-version-file: ci/go.mod
- name: Build utility
run: go build -C ./ci -o runner ./cmd/main.go
- name: Run Gluetun container with Mullvad configuration
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{
secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
- name: Run Gluetun container with ProtonVPN Wireguard and port forwarding
configuration
run:
echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner
protonvpn-wireguard-port-forwarding
- name: Run Gluetun container with ProtonVPN OpenVPN and port forwarding
configuration
run: echo -e "${{ secrets.PROTONVPN_OPENVPN_USER }}\n${{
secrets.PROTONVPN_OPENVPN_PASSWORD }}" | ./ci/runner
protonvpn-openvpn-port-forwarding
# - name:
# Run Gluetun container with Private Internet Access OpenVPN and port
# forwarding configuration
# run: echo -e "${{ secrets.PRIVATEINTERNETACCESS_OPENVPN_USER }}\n${{
# secrets.PRIVATEINTERNETACCESS_OPENVPN_PASSWORD }}" | ./ci/runner
# private-internet-access-openvpn-port-forwarding
- name: Run Gluetun container with AirVPN Wireguard configuration
run: echo -e "${{ secrets.AIRVPN_WIREGUARD_PRIVATE_KEY }}\n${{
secrets.AIRVPN_WIREGUARD_PRESHARED_KEY }}\n${{
secrets.AIRVPN_WIREGUARD_ADDRESSES }}" | ./ci/runner airvpn-wireguard
- name: Run Gluetun container with AirVPN OpenVPN configuration
run:
echo -e "${{ secrets.AIRVPN_OPENVPN_KEY }}\n${{ secrets.AIRVPN_OPENVPN_CERT
}}" | ./ci/runner airvpn-openvpn
- name: Run OpenVPN
if: |
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
)
run: |
docker run -it --rm --cap-add=NET_ADMIN -e VPNSP=mullvad -e VPN_TYPE=openvpn \
-e OPENVPN_PASSWORD=${{ secrets.OPENVPN_PASSWORD }} \
final-image \
exit-once-connected
codeql:
runs-on: ubuntu-latest
@@ -157,39 +100,38 @@ jobs:
contents: read
security-events: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: github/codeql-action/init@v4
go-version: "^1.23"
- uses: github/codeql-action/init@v3
with:
languages: go
- uses: github/codeql-action/autobuild@v4
- uses: github/codeql-action/analyze@v4
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
publish:
if: |
github.repository == 'passteque/gluetun' &&
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify, verify-private, codeql]
needs: [verify, codeql]
permissions:
actions: read
contents: read
packages: write
runs-on: ubuntu-latest
environment: secrets
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
@@ -204,30 +146,26 @@ jobs:
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) }}
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v4
- uses: docker/login-action@v3
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- uses: docker/login-action@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: qdm12
password: ${{ secrets.GHCR_PAT }}
password: ${{ github.token }}
- name: Short commit
id: shortcommit
run: |
# Use the PR head SHA if it exists, otherwise fallback to GITHUB_SHA
FULL_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
SHORT_SHA=
echo "value=$(echo $FULL_SHA | cut -c1-7)" >> $GITHUB_OUTPUT
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
- name: Build and push final image
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
labels: ${{ steps.meta.outputs.labels }}
+4 -8
View File
@@ -9,17 +9,13 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v5
- uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
body: |
Closed issues are **NOT** monitored, so commenting here will likely NOT be seen.
If you think this is *still unresolved* and have **more information** to bring, please either
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)
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 create another issue.
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 -5
View File
@@ -2,16 +2,12 @@
"ignorePatterns": [
{
"pattern": "^https://console.substack.com/p/console-72$"
},
{
"pattern": "^https://github.com/passteque/gluetun$"
}
],
"timeout": "20s",
"retryOn429": false,
"fallbackRetryDelay": "30s",
"aliveStatusCodes": [
200,
429
200
]
}
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: crazy-max/ghaction-github-labeler@v6
- uses: actions/checkout@v4
- uses: crazy-max/ghaction-github-labeler@v5
with:
yaml-file: .github/labels.yml
+5 -10
View File
@@ -11,24 +11,19 @@ on:
- "**.md"
- .github/workflows/markdown.yml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
markdown:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
environment: secrets
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v22
- uses: DavidAnson/markdownlint-cli2-action@v16
with:
globs: "**.md"
config: .markdownlint-cli2.jsonc
config: .markdownlint.json
- uses: reviewdog/action-misspell@v1
with:
@@ -42,8 +37,8 @@ jobs:
use-quiet-mode: yes
config-file: .github/workflows/configs/mlc-config.json
- uses: peter-evans/dockerhub-description@v5
if: github.repository == 'passteque/gluetun' && github.event_name == 'push'
- uses: peter-evans/dockerhub-description@v4
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v5
- uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
-1
View File
@@ -1,2 +1 @@
scratch.txt
.DS_Store
+25 -79
View File
@@ -1,85 +1,27 @@
version: "2"
linters-settings:
misspell:
locale: US
formatters:
enable:
- gci
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
run:
build-tags:
- integration
issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
- err113
- containedctx
- maintidx
- path: "internal\\/server\\/.+\\.go"
linters:
- dupl
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters:
- ireturn
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
linters:
- ireturn
linters:
settings:
misspell:
locale: US
goconst:
ignore-string-values:
# commonly used settings strings
- "^disabled$"
# Firewall and routing strings
- "^(ACCEPT|DROP)$"
- "^--append$"
- "^--delete$"
- "^all$"
- "^(tcp|udp)$"
# Server route strings
- "^/status$"
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- containedctx
- dupl
- err113
- maintidx
path: _test\.go
- linters:
- dupl
path: internal\/server\/.+\.go
- linters:
- ireturn
text: returns interface \(golang\.org\/x\/sys\/unix\.Sockaddr\)
- linters:
- ireturn
path: internal\/openvpn\/pkcs8\/descbc\.go
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
- linters:
- revive
path: internal\/provider\/(common|utils)\/.+\.go
text: "var-naming: avoid (bad|meaningless) package names"
- linters:
- lll
source: "^// https://.+$"
- linters:
- mnd
source: "^ cleanups\\.Add.+$"
path: internal\/(wireguard|amneziawg)\/run\.go
- linters:
- err113
- mnd
path: ci\/.+\.go
- linters:
- err113
text: "do not define dynamic errors, use wrapped static errors instead"
paths:
- third_party$
- builtin$
- examples$
enable:
# - cyclop
# - errorlint
@@ -100,6 +42,7 @@ linters:
- exhaustive
- fatcontext
- forcetypeassert
- gci
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
@@ -108,7 +51,9 @@ linters:
- gocritic
- gocyclo
- godot
- gofumpt
- goheader
- goimports
- gomoddirectives
- goprintffuncname
- gosec
@@ -141,6 +86,7 @@ linters:
- rowserrcheck
- sqlclosecheck
- tagalign
- tenv
- thelper
- tparallel
- unconvert
-9
View File
@@ -1,9 +0,0 @@
{
"config": {
"default": true,
"MD013": false,
},
"ignores": [
".github/pull_request_template.md"
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"MD013": false
}
+35
View File
@@ -0,0 +1,35 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Update a VPN provider servers data",
"type": "go",
"request": "launch",
"cwd": "${workspaceFolder}",
"program": "cmd/gluetun/main.go",
"args": [
"update",
"${input:updateMode}",
"-providers",
"${input:provider}"
],
}
],
"inputs": [
{
"id": "provider",
"type": "promptString",
"description": "Please enter a provider (or comma separated list of providers)",
},
{
"id": "updateMode",
"type": "pickString",
"description": "Update mode to use",
"options": [
"-maintainer",
"-enduser"
],
"default": "-maintainer"
},
]
}
+1 -1
View File
@@ -3,7 +3,7 @@
// to develop this project.
"files.eol": "\n",
"editor.formatOnSave": true,
"go.buildTags": "linux,integration",
"go.buildTags": "linux",
"go.toolsEnvVars": {
"CGO_ENABLED": "0"
},
-81
View File
@@ -1,81 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Update a VPN provider servers data",
"type": "shell",
"command": "go",
"args": [
"run",
"./cmd/gluetun/main.go",
"update",
"${input:updateMode}",
"-providers",
"${input:provider}"
],
},
{
"label": "Add a Gluetun Github Git remote",
"type": "shell",
"command": "git",
"args": [
"remote",
"add",
"${input:githubRemoteUsername}",
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
],
},
{
"label": "Devrun",
"type": "shell",
"command": "go run ./cmd/main.go run ${input:devrunProvider} ${input:devrunVPNProtocol} ${input:devrunExtraFlags}",
"options": {
"cwd": "${workspaceFolder}/devrun"
},
"problemMatcher": []
}
],
"inputs": [
{
"id": "provider",
"type": "promptString",
"description": "Please enter a provider (or comma separated list of providers)",
},
{
"id": "updateMode",
"type": "pickString",
"description": "Update mode to use",
"options": [
"-maintainer",
"-enduser"
],
"default": "-maintainer"
},
{
"id": "githubRemoteUsername",
"type": "promptString",
"description": "Please enter a Github username",
},
{
"id": "devrunProvider",
"type": "promptString",
"description": "Please enter a single provider",
},
{
"id": "devrunVPNProtocol",
"type": "pickString",
"description": "VPN protocol to use",
"options": [
"wireguard",
"openvpn"
],
"default": "wireguard"
},
{
"id": "devrunExtraFlags",
"type": "promptString",
"description": "Extra flags (optional)",
"default": ""
},
]
}
-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.
+36 -92
View File
@@ -1,19 +1,19 @@
ARG ALPINE_VERSION=3.23
ARG GO_ALPINE_VERSION=3.23
ARG GO_VERSION=1.25
ARG XCPUTRANSLATE_VERSION=v0.9.0
ARG GOLANGCI_LINT_VERSION=v2.4.0
ARG ALPINE_VERSION=3.20
ARG GO_ALPINE_VERSION=3.20
ARG GO_VERSION=1.23
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.61.0
ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
# 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
COPY --from=golangci-lint /bin /go/bin/golangci-lint
COPY --from=mockgen /bin /go/bin/mockgen
@@ -32,7 +32,7 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
FROM --platform=${BUILDPLATFORM} base AS lint
COPY .golangci.yml ./
RUN golangci-lint run
RUN golangci-lint run --timeout=10m
FROM --platform=${BUILDPLATFORM} base AS mocks
RUN git init && \
@@ -46,10 +46,6 @@ RUN git init && \
git diff --exit-code && \
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
ARG TARGETPLATFORM
ARG VERSION=unknown
@@ -72,9 +68,9 @@ LABEL \
org.opencontainers.image.created=$CREATED \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$COMMIT \
org.opencontainers.image.url="https://github.com/passteque/gluetun" \
org.opencontainers.image.documentation="https://github.com/passteque/gluetun" \
org.opencontainers.image.source="https://github.com/passteque/gluetun" \
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
org.opencontainers.image.documentation="https://github.com/qdm12/gluetun" \
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
ENV VPN_SERVICE_PROVIDER=pia \
@@ -110,49 +106,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
WIREGUARD_ADDRESSES= \
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
WIREGUARD_MTU= \
WIREGUARD_MTU=1320 \
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= \
# 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
SERVER_REGIONS= \
SERVER_COUNTRIES= \
@@ -164,6 +119,10 @@ ENV VPN_SERVICE_PROVIDER=pia \
OWNED_ONLY=no \
# # Private Internet Access only:
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_PASSWORD= \
# # Cyberghost only:
@@ -197,33 +156,28 @@ ENV VPN_SERVICE_PROVIDER=pia \
FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_IPTABLES_LOG_LEVEL=info \
# IPv6
IPV6_CHECK_ADDRESSES=[2001:4860:4860::8888]:53,[2606:4700:4700::1111]:53 \
FIREWALL_DEBUG=off \
# Logging
LOG_LEVEL=info \
# Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_TARGET_ADDRESSES=cloudflare.com:443,github.com:443 \
HEALTH_ICMP_TARGET_IPS=1.1.1.1,8.8.8.8 \
HEALTH_SMALL_CHECK_TYPE=icmp \
HEALTH_RESTART_VPN=on \
# DNS
DNS_SERVER=on \
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
# Note: DNS_UPSTREAM_RESOLVERS defaults to cloudflare in code if DNS_UPSTREAM_PLAIN_ADDRESSES is empty
DNS_UPSTREAM_RESOLVERS= \
DNS_BLOCK_IPS= \
DNS_BLOCK_IP_PREFIXES= \
DNS_CACHING=on \
DNS_UPSTREAM_IPV6=off \
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
HEALTH_SUCCESS_WAIT_DURATION=5s \
HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
DOT_CACHING=on \
DOT_IPV6=off \
BLOCK_MALICIOUS=on \
BLOCK_SURVEILLANCE=off \
BLOCK_ADS=off \
DNS_UNBLOCK_HOSTNAMES= \
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
UNBLOCK= \
DNS_UPDATE_PERIOD=24h \
DNS_UPSTREAM_PLAIN_ADDRESSES= \
DNS_ADDRESS=127.0.0.1 \
DNS_KEEP_NAMESERVER=off \
# HTTP proxy
HTTPPROXY= \
HTTPPROXY_LOG=off \
@@ -240,31 +194,21 @@ ENV VPN_SERVICE_PROVIDER=pia \
SHADOWSOCKS_PASSWORD= \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
# Socks5
SOCKS5_ENABLED=off \
SOCKS5_LISTENING_ADDRESS=":1080" \
SOCKS5_USER= \
SOCKS5_PASSWORD= \
# Control server
HTTP_CONTROL_SERVER_LOG=on \
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
# Server data updater
UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \
UPDATER_PREFER_DIRECT_DOWNLOAD=no \
UPDATER_PROTONVPN_EMAIL= \
UPDATER_PROTONVPN_PASSWORD= \
# Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_ENABLED=on \
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
PUBLICIP_API_TOKEN= \
# Storage
STORAGE_SERVERS_ENABLED=on \
STORAGE_SERVERS_DIRECTORY_PATH=/gluetun/servers/ \
STORAGE_FILEPATH=/gluetun/servers.json \
# Pprof
PPROF_ENABLED=no \
PPROF_BLOCK_PROFILE_RATE=0 \
@@ -273,10 +217,10 @@ ENV VPN_SERVICE_PROVIDER=pia \
# Extras
VERSION_INFORMATION=on \
TZ= \
PUID=1000 \
PGID=1000
PUID= \
PGID=
ENTRYPOINT ["/gluetun-entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp 1080/tcp 1080/udp
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM
RUN apk add --no-cache --update -l wget && \
+24 -33
View File
@@ -1,14 +1,10 @@
# Gluetun VPN client
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
Lightweight swiss-knife-like VPN client to multiple VPN service providers
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
🗯️ this repository will be migrated to [github.com/passteque/gluetun](https://github.com/passteque/gluetun) on 2026-05-21, which is a Github organization under my sole control, so don't get alarmed if you get redirected in the coming days 😉 Reason being migrating Github sponsors to the Open source collective due to my personal situation, basically annoying paperwork. On the plus side, it will be more transparent and funds donated will only be used for the project. The Docker image names will remain the same.
![Title image](https://raw.githubusercontent.com/passteque/gluetun/master/title.svg)
[![Build status](https://github.com/passteque/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/passteque/gluetun/actions/workflows/ci.yml)
[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
[![Docker pulls qmcgaw/gluetun](https://img.shields.io/docker/pulls/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker pulls qmcgaw/private-internet-access](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
@@ -16,23 +12,24 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
[![Docker stars qmcgaw/gluetun](https://img.shields.io/docker/stars/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker stars qmcgaw/private-internet-access](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
![Last release](https://img.shields.io/github/release/passteque/gluetun?label=Last%20release)
![Last release](https://img.shields.io/github/release/qdm12/gluetun?label=Last%20release)
![Last Docker tag](https://img.shields.io/docker/v/qmcgaw/gluetun?sort=semver&label=Last%20Docker%20tag)
[![Last release size](https://img.shields.io/docker/image-size/qmcgaw/gluetun?sort=semver&label=Last%20released%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
![GitHub last release date](https://img.shields.io/github/release-date/passteque/gluetun?label=Last%20release%20date)
![Commits since release](https://img.shields.io/github/commits-since/passteque/gluetun/latest?sort=semver)
![GitHub last release date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date)
![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver)
[![Latest size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags)
[![GitHub last commit](https://img.shields.io/github/last-commit/passteque/gluetun.svg)](https://github.com/passteque/gluetun/commits/master)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/passteque/gluetun.svg)](https://github.com/passteque/gluetun/graphs/contributors)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/passteque/gluetun.svg)](https://github.com/passteque/gluetun/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues/passteque/gluetun.svg)](https://github.com/passteque/gluetun/issues)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/passteque/gluetun.svg)](https://github.com/passteque/gluetun/issues?q=is%3Aissue+is%3Aclosed)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits/master)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/graphs/contributors)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
![Code size](https://img.shields.io/github/languages/code-size/passteque/gluetun)
![GitHub repo size](https://img.shields.io/github/repo-size/passteque/gluetun)
![Go version](https://img.shields.io/github/go-mod/go-version/passteque/gluetun)
[![Lines of code](https://img.shields.io/tokei/lines/github/qdm12/gluetun)](https://github.com/qdm12/gluetun)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun)
![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun)
![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/gluetun)
![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme)
@@ -42,10 +39,10 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
- [Features](#features)
- Problem?
- Check the Wiki [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
- [Start a discussion](https://github.com/passteque/gluetun/discussions)
- [Fix the Unraid template](https://github.com/passteque/gluetun/discussions/550)
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
- Suggestion?
- [Create an issue](https://github.com/passteque/gluetun/issues)
- [Create an issue](https://github.com/qdm12/gluetun/issues)
- Happy?
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
@@ -59,21 +56,19 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
## Features
- Based on Alpine 3.23 for a small Docker image of 43.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
- Based on Alpine 3.20 for a small Docker image of 35.6MB
- 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**, **WeVPN**, **Windscribe** servers
- Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace
- 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**, **VyprVPN** and **WeVPN** 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/passteque/gluetun/issues/134)
- Supports AmneziaWG only with the custom provider for now
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- 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
- Choose the vpn network protocol, `udp` or `tcp`
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Built in Socks5 proxy server (tunnels TCP+UDP) - partial credits to @angelakis and @adjscent
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
@@ -93,7 +88,7 @@ Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
Here's a docker-compose.yml for the laziest:
```yml
---
version: "3"
services:
gluetun:
image: qmcgaw/gluetun
@@ -129,10 +124,6 @@ services:
🆕 Image also available as `ghcr.io/qdm12/gluetun`
## Fun graphs
[![Star History Chart](https://api.star-history.com/svg?repos=passteque/gluetun&type=date&legend=top-left)](https://www.star-history.com/#passteque/gluetun&type=date&legend=top-left)
## License
[![MIT](https://img.shields.io/github/license/passteque/gluetun)](https://github.com/passteque/gluetun/blob/master/LICENSE)
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/blob/master/LICENSE)
-45
View File
@@ -1,45 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/qdm12/gluetun/ci/internal"
"github.com/qdm12/log"
)
func main() {
logger := log.New()
if len(os.Args) < 2 {
logger.Error("Usage: " + os.Args[0] + " <command>")
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
var err error
switch os.Args[1] {
case "mullvad":
err = internal.MullvadTest(ctx, logger)
case "protonvpn-wireguard-port-forwarding":
err = internal.ProtonVPNWireguardPortForwardingTest(ctx, logger)
case "protonvpn-openvpn-port-forwarding":
err = internal.ProtonVPNOpenVPNPortForwardingTest(ctx, logger)
case "private-internet-access-openvpn-port-forwarding":
err = internal.PrivateInternetAccessOpenVPNPortForwardingTest(ctx, logger)
case "airvpn-wireguard":
err = internal.AirVPNWireguardTest(ctx, logger)
case "airvpn-openvpn":
err = internal.AirVPNOpenVPNTest(ctx, logger)
default:
err = fmt.Errorf("unknown command: %s", os.Args[1])
}
stop()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
logger.Info("test completed successfully")
}
-40
View File
@@ -1,40 +0,0 @@
module github.com/qdm12/gluetun/ci
go 1.25.0
require (
github.com/docker/docker v28.5.1+incompatible
github.com/opencontainers/image-spec v1.1.1
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/qdm12/log v0.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/time v0.14.0 // indirect
gotest.tools/v3 v3.5.2 // indirect
)
-109
View File
@@ -1,109 +0,0 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
-54
View File
@@ -1,54 +0,0 @@
package internal
import (
"context"
"fmt"
"regexp"
"time"
)
func AirVPNWireguardTest(ctx context.Context, logger Logger) error {
expectedSecrets := []string{
"Wireguard private key",
"Wireguard preshared key",
"Wireguard addresses",
}
secrets, err := readSecrets(ctx, expectedSecrets, logger)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=airvpn",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=United States",
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
"WIREGUARD_PRESHARED_KEY=" + secrets[1],
"WIREGUARD_ADDRESSES=" + secrets[2],
}
const timeout = 60 * time.Second
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp}, timeout, logger)
}
func AirVPNOpenVPNTest(ctx context.Context, logger Logger) error {
expectedSecrets := []string{
"OpenVPN key",
"OpenVPN cert",
}
secrets, err := readSecrets(ctx, expectedSecrets, logger)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=airvpn",
"VPN_TYPE=openvpn",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=United States",
"OPENVPN_KEY=" + secrets[0],
"OPENVPN_CERT=" + secrets[1],
}
const timeout = 60 * time.Second
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp}, timeout, logger)
}
-30
View File
@@ -1,30 +0,0 @@
package internal
import (
"context"
"fmt"
"regexp"
"time"
)
func MullvadTest(ctx context.Context, logger Logger) error {
expectedSecrets := []string{
"Wireguard private key",
"Wireguard address",
}
secrets, err := readSecrets(ctx, expectedSecrets, logger)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=mullvad",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=USA",
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
"WIREGUARD_ADDRESSES=" + secrets[1],
}
const timeout = 60 * time.Second
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp}, timeout, logger)
}
-31
View File
@@ -1,31 +0,0 @@
package internal
import (
"context"
"fmt"
"regexp"
"time"
)
func PrivateInternetAccessOpenVPNPortForwardingTest(ctx context.Context, logger Logger) error {
expectedSecrets := []string{
"OpenVPN username",
"OpenVPN password",
}
secrets, err := readSecrets(ctx, expectedSecrets, logger)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=private internet access",
"VPN_TYPE=openvpn",
"LOG_LEVEL=debug",
"SERVER_REGIONS=CA Montreal",
"OPENVPN_USER=" + secrets[0],
"OPENVPN_PASSWORD=" + secrets[1],
"VPN_PORT_FORWARDING=on",
}
const timeout = 80 * time.Second
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp, portForwardingRegexp}, timeout, logger)
}
-52
View File
@@ -1,52 +0,0 @@
package internal
import (
"context"
"fmt"
"regexp"
"time"
)
func ProtonVPNWireguardPortForwardingTest(ctx context.Context, logger Logger) error {
expectedSecrets := []string{
"Wireguard private key",
}
secrets, err := readSecrets(ctx, expectedSecrets, logger)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=protonvpn",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=United States",
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
"VPN_PORT_FORWARDING=on",
}
const timeout = 80 * time.Second
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp, portForwardingRegexp}, timeout, logger)
}
func ProtonVPNOpenVPNPortForwardingTest(ctx context.Context, logger Logger) error {
expectedSecrets := []string{
"OpenVPN username",
"OpenVPN password",
}
secrets, err := readSecrets(ctx, expectedSecrets, logger)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=protonvpn",
"VPN_TYPE=openvpn",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=United States",
"OPENVPN_USER=" + secrets[0],
"OPENVPN_PASSWORD=" + secrets[1],
"VPN_PORT_FORWARDING=on",
}
const timeout = 80 * time.Second
return runContainerTest(ctx, env, []*regexp.Regexp{successRegexp, portForwardingRegexp}, timeout, logger)
}
-49
View File
@@ -1,49 +0,0 @@
package internal
import (
"bufio"
"context"
"fmt"
"os"
"strings"
)
type Logger interface {
Info(msg string)
Infof(format string, args ...any)
}
func readSecrets(ctx context.Context, expectedSecrets []string,
logger Logger,
) (lines []string, err error) {
scanner := bufio.NewScanner(os.Stdin)
lines = make([]string, 0, len(expectedSecrets))
for i := range expectedSecrets {
logger.Infof("🤫 reading %s from Stdin...", expectedSecrets[i])
if !scanner.Scan() {
break
}
lines = append(lines, strings.TrimSpace(scanner.Text()))
logger.Infof("🤫 %s secret read successfully", expectedSecrets[i])
if ctx.Err() != nil {
return nil, ctx.Err()
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("reading secrets from stdin: %w", err)
}
if len(lines) < len(expectedSecrets) {
return nil, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
len(expectedSecrets), len(lines))
}
for i, line := range lines {
if line == "" {
return nil, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
}
}
return lines, nil
}
-144
View File
@@ -1,144 +0,0 @@
package internal
import (
"bufio"
"context"
"fmt"
"io"
"regexp"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func ptrTo[T any](v T) *T { return &v }
var (
successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
portForwardingRegexp = regexp.MustCompile(`port forwarded is \d`)
)
func runContainerTest(ctx context.Context, env []string,
regexps []*regexp.Regexp, timeout time.Duration, logger Logger,
) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("creating Docker client: %w", err)
}
defer client.Close()
config := &container.Config{
Image: "qmcgaw/gluetun",
StopTimeout: ptrTo(3),
Env: env,
}
hostConfig := &container.HostConfig{
AutoRemove: true,
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
}
networkConfig := (*network.NetworkingConfig)(nil)
platform := (*v1.Platform)(nil)
const containerName = "" // auto-generated name
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
if err != nil {
return fmt.Errorf("creating container: %w", err)
}
for _, warning := range response.Warnings {
fmt.Println("Warning during container creation:", warning)
}
containerID := response.ID
defer stopContainer(client, containerID)
beforeStartTime := time.Now()
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
if err != nil {
return fmt.Errorf("starting container: %w", err)
}
return waitForLogLines(ctx, client, containerID, beforeStartTime, regexps, logger)
}
func stopContainer(client *client.Client, containerID string) {
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
defer stopCancel()
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
if err != nil {
fmt.Println("failed to stop container:", err)
}
}
func waitForLogLines(ctx context.Context, client *client.Client, containerID string,
beforeStartTime time.Time, regexps []*regexp.Regexp, logger Logger,
) error {
logOptions := container.LogsOptions{
ShowStdout: true,
Follow: true,
Since: beforeStartTime.Format(time.RFC3339Nano),
}
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
if err != nil {
return fmt.Errorf("error getting container logs: %w", err)
}
defer reader.Close()
regexpMatched := 0
var linesSeen []string
scanner := bufio.NewScanner(reader)
for ctx.Err() == nil {
if scanner.Scan() {
line := scanner.Text()
if len(line) > 8 { // remove Docker log prefix
line = line[8:]
}
linesSeen = append(linesSeen, line)
regex := regexps[regexpMatched]
if regex.MatchString(line) {
fmt.Println("✅ Expected line logged:", line)
if regexpMatched == len(regexps)-1 {
return nil
}
regexpMatched++
}
continue
}
err := scanner.Err()
if err != nil && err != io.EOF {
logSeenLines(linesSeen)
return fmt.Errorf("reading log stream: %w", err)
}
// The scanner is either done or cannot read because of EOF
logger.Info("the log scanner stopped")
logSeenLines(linesSeen)
// Check if the container is still running
inspect, err := client.ContainerInspect(ctx, containerID)
if err != nil {
return fmt.Errorf("inspecting container: %w", err)
}
if !inspect.State.Running {
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
}
}
return ctx.Err()
}
func logSeenLines(lines []string) {
fmt.Println("Logs seen so far:")
for _, line := range lines {
fmt.Println(" " + line)
}
}
+76 -134
View File
@@ -2,10 +2,10 @@ package main
import (
"context"
"errors"
"fmt"
"io/fs"
"net/http"
"net/netip"
"os"
"os/exec"
"os/signal"
@@ -15,17 +15,13 @@ import (
_ "time/tzdata"
_ "github.com/breml/rootcerts"
"github.com/qdm12/dns/v2/pkg/doh"
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/boringpoll"
"github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/command"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/constants"
copenvpn "github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/healthcheck"
@@ -41,8 +37,8 @@ import (
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/socks5"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun"
updater "github.com/qdm12/gluetun/internal/updater/loop"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
@@ -79,6 +75,7 @@ func main() {
logger := log.New(log.SetLevel(log.LevelInfo))
args := os.Args
tun := tun.New()
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
netLinker := netlink.New(netLinkDebugLogger)
cli := cli.New()
@@ -98,7 +95,7 @@ func main() {
errorCh := make(chan error)
go func() {
errorCh <- _main(ctx, buildInfo, args, logger, reader, netLinker, cmder, cli)
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
}()
// Wait for OS signal or run error
@@ -140,12 +137,15 @@ func main() {
}
}
var errCommandUnknown = errors.New("command is unknown")
//nolint:gocognit,gocyclo,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger log.LoggerInterface, reader *reader.Reader,
netLinker netLinker, cmder RunStarter,
tun Tun, netLinker netLinker, cmder RunStarter,
cli clier,
) error {
var exitOnceConnected bool
if len(args) > 1 { // cli operation
switch args[1] {
case "healthcheck":
@@ -160,14 +160,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return cli.FormatServers(args[2:])
case "genkey":
return cli.GenKey(args[2:])
case "exit-once-connected":
exitOnceConnected = true
default:
return fmt.Errorf("command is unknown: %s", args[1])
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
}
}
defer fmt.Println(gluetunLogo)
announcementExp, err := time.Parse(time.RFC3339, "2026-06-30T00:00:00Z")
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
if err != nil {
return err
}
@@ -178,7 +178,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
Version: buildInfo.Version,
Commit: buildInfo.Commit,
Created: buildInfo.Created,
Announcement: "Your servers data files are now migrated to /gluetun/servers/",
Announcement: "All control server routes will become private by default after the v3.41.0 release",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
@@ -206,6 +206,9 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
netLinker.PatchLoggerLevel(logLevel)
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)
defaultRoutes, err := routingConf.DefaultRoutes()
@@ -218,11 +221,11 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err
}
iptablesLogLevel, _ := log.ParseLevel(allSettings.Firewall.Iptables.LogLevel)
iptablesLogger := logger.New(log.SetComponent("iptables"), log.SetLevel(iptablesLogLevel))
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)
if err != nil {
return err
@@ -233,27 +236,19 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
if err != nil {
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
storageLogger := logger.New(log.SetComponent("storage"))
storage, err := storage.New(storageLogger, *allSettings.Storage.ServersEnabled,
allSettings.Storage.ServersPath, allSettings.Storage.LegacyServersFilepath)
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
if err != nil {
return err
}
ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel(ctx,
allSettings.IPv6.CheckAddresses, firewallConf)
ipv6Supported, err := netLinker.IsIPv6Supported()
if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err)
}
ipv6Supported := ipv6SupportLevel == netlink.IPv6Supported ||
ipv6SupportLevel == netlink.IPv6Internet
err = allSettings.Validate(storage, ipv6Supported, logger)
if err != nil {
@@ -268,22 +263,19 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
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}
// Create configurators
alpineConf := alpine.New()
ovpnConf := openvpn.New(
logger.New(log.SetComponent("openvpn configurator")),
cmder, puid, pgid)
ovpnVersion := ovpnConf.Version26
if allSettings.VPN.OpenVPN.Version == copenvpn.Openvpn25 {
ovpnVersion = ovpnConf.Version25
}
err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN", getVersion: ovpnVersion},
{name: "Firewall", getVersion: firewallConf.Version},
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
{name: "IPtables", getVersion: firewallConf.Version},
})
if err != nil {
return err
@@ -340,6 +332,19 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return fmt.Errorf("adding local rules: %w", err)
}
const tunDevice = "/dev/net/tun"
err = tun.Check(tunDevice)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
}
logger.Info(err.Error() + "; creating it...")
err = tun.Create(tunDevice)
if err != nil {
return fmt.Errorf("creating tun device: %w", err)
}
}
for _, port := range allSettings.Firewall.InputPorts {
for _, defaultRoute := range defaultRoutes {
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
@@ -378,7 +383,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
portForwardRunError, err := portForwardLooper.Start(ctx)
if err != nil {
return fmt.Errorf("starting port forwarding loop: %w", err)
@@ -386,7 +391,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
dnsLogger := logger.New(log.SetComponent("dns"))
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
dnsLogger, localNetworksToPrefixes(localNetworks))
dnsLogger)
if err != nil {
return fmt.Errorf("creating DNS loop: %w", err)
}
@@ -412,50 +417,19 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return fmt.Errorf("starting public ip loop: %w", err)
}
socks5Loop := socks5.NewLoop(socks5.Settings{
Enabled: *allSettings.Socks5.Enabled,
Username: *allSettings.Socks5.Username,
Password: *allSettings.Socks5.Password,
Address: allSettings.Socks5.ListeningAddress,
Logger: logger.New(log.SetComponent("socks5")),
})
socks5RunError, err := socks5Loop.Start(ctx)
if err != nil {
return fmt.Errorf("starting SOCKS5 server loop: %w", err)
}
healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
healthChecker := healthcheck.NewChecker(healthLogger)
// Note: we use a separate DoH dialer for the VPN servers data updater, separate from the
// main DNS local server to make sure no request is blocked by filters.
dohDialer, err := doh.New(doh.Settings{
UpstreamResolvers: []dnsprovider.Provider{dnsprovider.Cloudflare(), dnsprovider.Google()},
})
if err != nil {
return fmt.Errorf("creating updater DoH dialer: %w", err)
}
updaterLogger := logger.New(log.SetComponent("updater"))
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(dohDialer)
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
openvpnFileExtractor, allSettings.Updater)
boringPollLogger := logger.New(log.SetComponent("boring poll"))
boringPoll := boringpoll.New(httpClient, boringPollLogger, allSettings.BoringPoll)
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6SupportLevel, allSettings.Firewall.VPNInputPorts,
providers, storage, boringPoll, allSettings.Health, healthChecker, healthcheckServer,
ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, cmder, publicIPLooper,
dnsLooper, vpnLogger, httpClient, buildInfo, *allSettings.Version.Enabled)
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second))
go vpnLooper.Run(vpnCtx, vpnDone)
@@ -488,12 +462,15 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := *allSettings.ControlServer.Address
controlServerLogging := *allSettings.ControlServer.Log
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")),
allSettings.ControlServer.AuthFilePath,
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6SupportLevel.IsSupported())
storage, ipv6Supported)
if err != nil {
return fmt.Errorf("setting up control server: %w", err)
}
@@ -502,6 +479,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
<-httpServerReady
controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper,
exitOnceConnected)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess),
@@ -519,7 +503,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
String() string
Stop() error
}{
portForwardLooper, publicIPLooper, socks5Loop,
portForwardLooper, publicIPLooper,
}
for _, stopper := range stoppers {
err := stopper.Stop()
@@ -531,8 +515,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.Errorf("port forwarding loop crashed: %s", err)
case err := <-publicIPRunError:
logger.Errorf("public IP loop crashed: %s", err)
case err := <-socks5RunError:
logger.Errorf("SOCKS5 server loop crashed: %s", err)
}
return orderHandler.Shutdown(context.Background())
@@ -565,42 +547,31 @@ func printVersions(ctx context.Context, logger infoer,
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 {
Addresser
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
FindIPv6SupportLevel(ctx context.Context,
checkAddresses []netip.AddrPort, firewall netlink.Firewall,
) (level netlink.IPv6SupportLevel, err error)
FlushConntrack() error
IsIPv6Supported() (ok bool, err error)
PatchLoggerLevel(level log.Level)
}
type Addresser interface {
AddrList(linkIndex uint32, family uint8) (
addresses []netip.Prefix, err error)
AddrReplace(linkIndex uint32, addr netip.Prefix) error
AddrList(link netlink.Link, family int) (
addresses []netlink.Addr, err error)
AddrReplace(link netlink.Link, addr netlink.Addr) error
}
type Router interface {
RouteList(family uint8) (routes []netlink.Route, err error)
RouteList(family int) (routes []netlink.Route, err error)
RouteAdd(route netlink.Route) error
RouteDel(route netlink.Route) error
RouteReplace(route netlink.Route) error
}
type Ruler interface {
RuleList(family uint8) (rules []netlink.Rule, err error)
RuleList(family int) (rules []netlink.Rule, err error)
RuleAdd(rule netlink.Rule) error
RuleDel(rule netlink.Rule) error
}
@@ -608,12 +579,11 @@ type Ruler interface {
type Linker interface {
LinkList() (links []netlink.Link, err error)
LinkByName(name string) (link netlink.Link, err error)
LinkByIndex(index uint32) (link netlink.Link, err error)
LinkAdd(link netlink.Link) (linkIndex uint32, err error)
LinkDel(linkIndex uint32) (err error)
LinkSetUp(linkIndex uint32) (err error)
LinkSetDown(linkIndex uint32) (err error)
LinkSetMTU(linkIndex, mtu uint32) error
LinkByIndex(index int) (link netlink.Link, err error)
LinkAdd(link netlink.Link) (linkIndex int, err error)
LinkDel(link netlink.Link) (err error)
LinkSetUp(link netlink.Link) (linkIndex int, err error)
LinkSetDown(link netlink.Link) (err error)
}
type clier interface {
@@ -625,41 +595,13 @@ type clier interface {
GenKey(args []string) error
}
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
}
type RunStarter interface {
Run(cmd *exec.Cmd) (output string, err error)
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, err error)
RunAndLog(ctx context.Context, commandString string,
logger command.Logger) (err error)
}
const gluetunLogo = ` @@@
@@@@
@@@@@@
@@@@.@@ @@@@@@@@@@
@@@@.@@@ @@@@@@@@==@@@@
@@@.@..@@ @@@@@@@=@..==@@@@
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
@@@.--@..@@@@.-@@@@@@@==============@@@@
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
@@@@.@--@@@@@@@@@@================@@@@@@@
@@@..--@@*@@@@@@================@@@@+*@@
@@@.---@@.@@@@=================@@@@--@@
@@@-.---@@@@@@================@@@@*--@@@
@@@.:-#@@@@@@===============*@@@@.---@@
@@@.-------.@@@============@@@@@@.--@@@
@@@..--------:@@@=========@@@@@@@@.--@@@
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
@@@@@@=============@@@@@ @@@...------------.*@@@
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
@@@@@@@@ @@@@ @@@@
`
-1
View File
@@ -1 +0,0 @@
credentials
-152
View File
@@ -1,152 +0,0 @@
# devrun
`devrun` is a small development helper for starting a local `qmcgaw/gluetun` Docker container with provider credentials stored in an encrypted file.
It solves two practical problems for local development:
- keeping VPN credentials out of the shell history and out of a plaintext file once setup is complete;
- quickly starting a Gluetun container for a specific provider and VPN type with a small set of extra Docker runtime options.
The tool has four commands:
- `add-cred`: add or replace credentials for one provider and one VPN type in the encrypted store `credentials`;
- `delete-cred`: remove credentials for one provider and one VPN type from the encrypted store `credentials`;
- `dump-cred`: print credentials for one provider and one VPN type from the encrypted store `credentials`;
- `run`: decrypt credentials on demand, build the required Gluetun environment variables, and run a `qmcgaw/gluetun` container.
## Prerequisites
- Go installed locally
- Docker installed and a daemon available to the Docker client
- an interactive terminal, since the tool prompts for passwords without echoing them
The Docker client is created from the standard Docker environment, so settings such as `DOCKER_HOST` are honored.
## Quick start
### Add credentials
Add one credential entry to the encrypted store:
```sh
go run ./cmd/main.go add-cred protonvpn openvpn
go run ./cmd/main.go add-cred mullvad wireguard
```
Behavior:
- if `credentials` does not exist yet, `add-cred` asks for a new credentials password and creates the encrypted store;
- if `credentials` already exists, `add-cred` asks for the existing password first, decrypts the store, updates it, and writes it back encrypted;
- sensitive fields are read from stdin without echo.
Prompted values depend on the VPN type:
- `openvpn`: username and password
- `wireguard`: private key, optional address, optional preshared key
Running `add-cred` again for the same provider and VPN type replaces the existing values for that entry.
### Delete credentials
Remove one credential entry from the encrypted store:
```sh
go run ./cmd/main.go delete-cred protonvpn openvpn
```
This asks for the credentials password first, decrypts the store, removes the requested provider and VPN type, and writes the store back encrypted.
### Dump credentials
Print one credential entry from the encrypted store:
```sh
go run ./cmd/main.go dump-cred protonvpn openvpn
```
This asks for the credentials password first and then prints the selected provider and VPN type values.
### Container run
Run a container using the image `qmcgaw/gluetun` and the encrypted credentials with the `run` command.
For example:
```sh
go run ./cmd/main.go run mullvad wireguard
go run ./cmd/main.go run protonvpn wireguard -e PORT_FORWARDING=on -p 8000:8000/tcp
```
You will be prompted for the credentials password, the file `credentials` will be decrypted in memory, and the container will be started.
The following environment variables are always added by the tool:
- `VPN_SERVICE_PROVIDER=<provider>`
- `VPN_TYPE=<vpn-type>`
- `LOG_LEVEL=debug`
The tool also adds `NET_ADMIN` to the container capabilities by default.
## Credential model
Internally, the encrypted file stores a binary-encoded map keyed by provider name. Each provider can define `openvpn`, `wireguard`, or both.
Conceptually, the stored data looks like this:
- provider `mullvad`: contains `wireguard`
- provider `protonvpn`: contains `wireguard`
- provider `protonvpn`: contains `openvpn`
You do not edit this directly. It is stored as encrypted binary data in `credentials`.
### OpenVPN fields
- `username` is required;
- `password` is required;
At runtime these map to:
- `OPENVPN_USER`
- `OPENVPN_PASSWORD`
### WireGuard fields
- `private_key` is required and must be a valid WireGuard private key;
- `address` is optional and must be a valid network prefix if set;
- `preshared_key` is optional and must be a valid WireGuard key if set.
At runtime these map to:
- `WIREGUARD_PRIVATE_KEY`
- `WIREGUARD_ADDRESSES` when `address` is set
- `WIREGUARD_PRESHARED_KEY` when `preshared_key` is set
## Supported extra Docker flags
The `run` command only accepts a focused subset of Docker-style runtime flags. Unsupported flags return an error.
Supported flags:
- `-e`, `--env KEY=VALUE`
- `-v`, `--volume SOURCE:TARGET[:mode]`
- `-p`, `--publish HOSTPORT:CONTAINERPORT[/proto]`
- `--dns IP`
- `--device SPEC`
- `--label KEY=VALUE`
- `--cap-add CAPABILITY`
## Signals and shutdown
While the container is running:
- the first `Ctrl+C` requests a graceful stop with a 5 second timeout;
- the second `Ctrl+C` sends a kill signal to the container;
- a further interrupt exits the tool immediately.
## Notes and limitations
- The container image is fixed to `qmcgaw/gluetun`.
- The container name is fixed to `gluetun`.
- Credentials are decrypted in memory only during execution.
- If the requested provider or VPN type is not present in the encrypted credentials file, the command fails with an explicit error.
- The encrypted credential store file is named `credentials`.
- This tool is intended for local development convenience, not as a general replacement for `docker run`.
-156
View File
@@ -1,156 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/qdm12/gluetun/devrun/internal"
)
func main() {
const minArgs = 2
if len(os.Args) < minArgs {
printUsage()
os.Exit(1)
}
switch os.Args[1] {
case "add-cred":
const addCredMinArgs = 4
if len(os.Args) < addCredMinArgs {
fmt.Fprintf(os.Stderr,
`Usage: %s add-cred <provider> <vpn-type>
Example: %s add-cred protonvpn wireguard`, os.Args[0], os.Args[0])
os.Exit(1)
}
provider := os.Args[2]
vpnType := os.Args[3]
err := runWithSignals(func(ctx context.Context, _ <-chan struct{}) error {
return internal.AddCredential(ctx, provider, vpnType)
})
if err != nil {
fmt.Fprintln(os.Stderr, "add-cred failed:", err)
os.Exit(1)
}
case "delete-cred":
const deleteCredMinArgs = 4
if len(os.Args) < deleteCredMinArgs {
fmt.Fprintf(os.Stderr,
`Usage: %s delete-cred <provider> <vpn-type>
Example: %s delete-cred protonvpn openvpn`, os.Args[0], os.Args[0])
os.Exit(1)
}
provider := os.Args[2]
vpnType := os.Args[3]
err := runWithSignals(func(ctx context.Context, _ <-chan struct{}) error {
return internal.DeleteCredential(ctx, provider, vpnType)
})
if err != nil {
fmt.Fprintln(os.Stderr, "delete-cred failed:", err)
os.Exit(1)
}
case "dump-cred":
const dumpCredMinArgs = 4
if len(os.Args) < dumpCredMinArgs {
fmt.Fprintf(os.Stderr,
`Usage: %s dump-cred <provider> <vpn-type>
Example: %s dump-cred protonvpn wireguard`, os.Args[0], os.Args[0])
os.Exit(1)
}
provider := os.Args[2]
vpnType := os.Args[3]
err := runWithSignals(func(ctx context.Context, _ <-chan struct{}) error {
return internal.DumpCredential(ctx, provider, vpnType)
})
if err != nil {
fmt.Fprintln(os.Stderr, "dump-cred failed:", err)
os.Exit(1)
}
case "run":
const runMinArgs = 4
if len(os.Args) < runMinArgs {
fmt.Fprintf(os.Stderr,
`Usage: %s run <provider> <vpn-type> [extra docker flags...]
Example: %s run mullvad wireguard -e SERVER_COUNTRIES=USA`, os.Args[0], os.Args[0])
os.Exit(1)
}
provider := os.Args[2]
vpnType := os.Args[3]
extraArgs := os.Args[4:]
err := runWithSignals(func(ctx context.Context, forceKill <-chan struct{}) error {
return internal.Run(ctx, provider, vpnType, extraArgs, forceKill)
})
if err != nil {
fmt.Fprintln(os.Stderr, "run failed:", err)
os.Exit(1)
}
default:
fmt.Fprintln(os.Stderr, "unknown command:", os.Args[1])
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Fprintf(os.Stderr, `Usage: %s <command> [args...]
Commands:
add-cred <provider> <vpn-type>
Add or replace credentials in the encrypted credentials store.
delete-cred <provider> <vpn-type>
Delete credentials from the encrypted credentials store.
dump-cred <provider> <vpn-type>
Print credentials for a provider and VPN type pair.
run <provider> <vpn-type> [flags...]
Decrypt credentials and run a Gluetun container.
Extra flags (e.g. -e PORT_FORWARDING=on) are passed to docker run.`,
os.Args[0])
}
func runWithSignals(runFn func(ctx context.Context, forceKill <-chan struct{}) error) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const signalBufferSize = 3
sigCh := make(chan os.Signal, signalBufferSize)
signal.Notify(sigCh, os.Interrupt)
defer signal.Stop(sigCh)
forceKill := make(chan struct{})
stopSignalLoop := make(chan struct{})
signalLoopDone := make(chan struct{})
go func() {
defer close(signalLoopDone)
const secondInterrupt = 2
interruptCount := uint(0)
forceKillSent := false
for {
select {
case <-stopSignalLoop:
return
case <-sigCh:
interruptCount++
switch interruptCount {
case 1:
cancel()
case secondInterrupt:
if !forceKillSent {
close(forceKill)
forceKillSent = true
}
default:
os.Exit(1)
}
}
}
}()
err := runFn(ctx, forceKill)
close(stopSignalLoop)
<-signalLoopDone
return err
}
-40
View File
@@ -1,40 +0,0 @@
module github.com/qdm12/gluetun/devrun
go 1.25.0
require (
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.7.0
github.com/opencontainers/image-spec v1.1.1
golang.org/x/crypto v0.50.0
golang.org/x/term v0.42.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/time v0.15.0 // indirect
gotest.tools/v3 v3.5.2 // indirect
)
-105
View File
@@ -1,105 +0,0 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
-251
View File
@@ -1,251 +0,0 @@
package internal
import (
"bytes"
"encoding/gob"
"fmt"
"maps"
"net/netip"
"slices"
"strings"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
const credentialsFilename = "credentials"
const (
vpnTypeOpenVPN = "openvpn"
vpnTypeWireGuard = "wireguard"
)
type providerCredentials struct {
OpenVPN *openvpnCredentials
WireGuard *wireguardCredentials
}
type openvpnCredentials struct {
Username string
Password string
Key string
Cert string
}
type wireguardCredentials struct {
PrivateKey string
Address string
PresharedKey string
}
func loadCredentials(data []byte) (map[string]providerCredentials, error) {
credentials := make(map[string]providerCredentials)
err := gob.NewDecoder(bytes.NewReader(data)).Decode(&credentials)
if err != nil {
return nil, fmt.Errorf("decoding credentials: %w", err)
}
return credentials, nil
}
func marshalCredentials(credentials map[string]providerCredentials) ([]byte, error) {
buffer := bytes.NewBuffer(nil)
err := gob.NewEncoder(buffer).Encode(credentials)
if err != nil {
return nil, fmt.Errorf("encoding credentials: %w", err)
}
return buffer.Bytes(), nil
}
func validateCredentials(providerNameToCredentials map[string]providerCredentials) error {
for provider, credentials := range providerNameToCredentials {
if credentials.OpenVPN == nil && credentials.WireGuard == nil {
return fmt.Errorf("provider %q has no openvpn or wireguard credentials", provider)
}
if credentials.OpenVPN != nil {
err := validateOpenvpnCredentials(provider, credentials.OpenVPN)
if err != nil {
return err
}
}
if credentials.WireGuard != nil {
err := validateWireguardCredentials(provider, credentials.WireGuard)
if err != nil {
return err
}
}
}
return nil
}
func validateOpenvpnCredentials(provider string, creds *openvpnCredentials) error {
switch {
case creds.Username == "" && creds.Password != "":
return fmt.Errorf("provider %q openvpn credentials are missing the username", provider)
case creds.Password == "" && creds.Username != "":
return fmt.Errorf("provider %q openvpn credentials are missing the password", provider)
case creds.Username == "" && creds.Password == "" && creds.Key == "" && creds.Cert == "":
return fmt.Errorf("provider %q openvpn credentials are missing the username and password", provider)
}
return nil
}
func validateWireguardCredentials(provider string, creds *wireguardCredentials) error {
if creds.PrivateKey == "" {
return fmt.Errorf("provider %q wireguard credentials are missing the private key", provider)
} else if _, err := wgtypes.ParseKey(creds.PrivateKey); err != nil {
return fmt.Errorf("provider %q wireguard credentials have an invalid private key: %w", provider, err)
}
if creds.Address != "" {
_, err := netip.ParsePrefix(creds.Address)
if err != nil {
return fmt.Errorf("provider %q wireguard credentials have an invalid address %q: %w", provider, creds.Address, err)
}
}
if creds.PresharedKey != "" {
if _, err := wgtypes.ParseKey(creds.PresharedKey); err != nil {
return fmt.Errorf("provider %q wireguard credentials have an invalid preshared key: %w", provider, err)
}
}
return nil
}
func lookupCredentials(credentials map[string]providerCredentials, provider, vpnType string) ([]string, error) {
providerCreds, exists := credentials[provider]
if !exists {
existing := slices.Collect(maps.Keys(credentials))
return nil, fmt.Errorf("no credentials found for provider %q, available providers are: %s",
provider, strings.Join(existing, ", "))
}
switch vpnType {
case vpnTypeWireGuard:
if providerCreds.WireGuard == nil {
return nil, fmt.Errorf("no wireguard credentials found for provider %q", provider)
}
return buildWireGuardEnv(providerCreds.WireGuard), nil
case vpnTypeOpenVPN:
if providerCreds.OpenVPN == nil {
return nil, fmt.Errorf("no openvpn credentials found for provider %q", provider)
}
return buildOpenvpnEnv(providerCreds.OpenVPN), nil
default:
return nil, fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
}
}
func buildWireGuardEnv(creds *wireguardCredentials) []string {
envVars := []string{
"WIREGUARD_PRIVATE_KEY=" + creds.PrivateKey,
}
if creds.Address != "" {
envVars = append(envVars, "WIREGUARD_ADDRESSES="+creds.Address)
}
if creds.PresharedKey != "" {
envVars = append(envVars, "WIREGUARD_PRESHARED_KEY="+creds.PresharedKey)
}
return envVars
}
func buildOpenvpnEnv(creds *openvpnCredentials) []string {
return []string{
"OPENVPN_USER=" + creds.Username,
"OPENVPN_PASSWORD=" + creds.Password,
"OPENVPN_KEY=" + creds.Key,
"OPENVPN_CERT=" + creds.Cert,
}
}
func addCredential(credentials map[string]providerCredentials, provider, vpnType string,
openvpnCredentials *openvpnCredentials, wireguardCredentials *wireguardCredentials,
) error {
providerCredentials := credentials[provider]
switch vpnType {
case vpnTypeOpenVPN:
providerCredentials.OpenVPN = openvpnCredentials
case vpnTypeWireGuard:
providerCredentials.WireGuard = wireguardCredentials
default:
return fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
}
credentials[provider] = providerCredentials
return nil
}
func deleteCredential(credentials map[string]providerCredentials, provider, vpnType string) error {
providerCredentials, exists := credentials[provider]
if !exists {
return fmt.Errorf("provider %q does not exist", provider)
}
switch vpnType {
case vpnTypeOpenVPN:
if providerCredentials.OpenVPN == nil {
return fmt.Errorf("provider %q has no openvpn credentials", provider)
}
providerCredentials.OpenVPN = nil
case vpnTypeWireGuard:
if providerCredentials.WireGuard == nil {
return fmt.Errorf("provider %q has no wireguard credentials", provider)
}
providerCredentials.WireGuard = nil
default:
return fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
}
if providerCredentials.OpenVPN == nil && providerCredentials.WireGuard == nil {
delete(credentials, provider)
return nil
}
credentials[provider] = providerCredentials
return nil
}
func formatCredentialForDump(provider, vpnType string,
providerCredentials providerCredentials,
) (output string, err error) {
var builder strings.Builder
builder.WriteString("provider: ")
builder.WriteString(provider)
builder.WriteString("\n")
builder.WriteString("vpn_type: ")
builder.WriteString(vpnType)
builder.WriteString("\n")
switch vpnType {
case vpnTypeOpenVPN:
if providerCredentials.OpenVPN == nil {
return "", fmt.Errorf("no openvpn credentials found for provider %q", provider)
}
builder.WriteString("username: ")
builder.WriteString(providerCredentials.OpenVPN.Username)
builder.WriteString("\n")
builder.WriteString("password: ")
builder.WriteString(providerCredentials.OpenVPN.Password)
builder.WriteString("\nkey: ")
builder.WriteString(providerCredentials.OpenVPN.Key)
builder.WriteString("\ncert: ")
builder.WriteString(providerCredentials.OpenVPN.Cert)
builder.WriteString("\n")
case vpnTypeWireGuard:
if providerCredentials.WireGuard == nil {
return "", fmt.Errorf("no wireguard credentials found for provider %q", provider)
}
builder.WriteString("private_key: ")
builder.WriteString(providerCredentials.WireGuard.PrivateKey)
builder.WriteString("\n")
builder.WriteString("address: ")
builder.WriteString(providerCredentials.WireGuard.Address)
builder.WriteString("\n")
builder.WriteString("preshared_key: ")
builder.WriteString(providerCredentials.WireGuard.PresharedKey)
default:
return "", fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
}
return builder.String(), nil
}
-350
View File
@@ -1,350 +0,0 @@
package internal
import (
"testing"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func Test_addCredential(t *testing.T) {
t.Parallel()
wireguardPrivateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
}
testCases := map[string]struct {
initialCredentials map[string]providerCredentials
provider string
vpnType string
openvpnCredentials *openvpnCredentials
wireguardCreds *wireguardCredentials
expectedLength int
expectedOpenVPN bool
expectedWireGuard bool
}{
"adds_openvpn_credentials": {
initialCredentials: map[string]providerCredentials{},
provider: "protonvpn",
vpnType: "openvpn",
openvpnCredentials: &openvpnCredentials{Username: "user", Password: "pass"},
expectedLength: 1,
expectedOpenVPN: true,
},
"adds_wireguard_credentials": {
initialCredentials: map[string]providerCredentials{},
provider: "mullvad",
vpnType: "wireguard",
wireguardCreds: &wireguardCredentials{
PrivateKey: wireguardPrivateKey.String(),
Address: "10.0.0.2/32",
},
expectedLength: 1,
expectedWireGuard: true,
},
"preserves_other_protocol": {
initialCredentials: map[string]providerCredentials{
"protonvpn": {
WireGuard: &wireguardCredentials{PrivateKey: wireguardPrivateKey.String()},
},
},
provider: "protonvpn",
vpnType: "openvpn",
openvpnCredentials: &openvpnCredentials{Username: "user", Password: "pass"},
expectedLength: 1,
expectedOpenVPN: true,
expectedWireGuard: true,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
credentials := cloneCredentials(testCase.initialCredentials)
err := addCredential(credentials, testCase.provider, testCase.vpnType,
testCase.openvpnCredentials, testCase.wireguardCreds)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
providerCredentials := credentials[testCase.provider]
if len(credentials) != testCase.expectedLength {
t.Fatalf("expected %d providers, got %d", testCase.expectedLength, len(credentials))
}
if (providerCredentials.OpenVPN != nil) != testCase.expectedOpenVPN {
t.Fatalf("expected openvpn presence %t, got %t", testCase.expectedOpenVPN, providerCredentials.OpenVPN != nil)
}
if (providerCredentials.WireGuard != nil) != testCase.expectedWireGuard {
t.Fatalf("expected wireguard presence %t, got %t", testCase.expectedWireGuard, providerCredentials.WireGuard != nil)
}
})
}
}
func Test_deleteCredential(t *testing.T) {
t.Parallel()
wireguardPrivateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
}
testCases := map[string]struct {
initialCredentials map[string]providerCredentials
provider string
vpnType string
expectedLength int
expectedOpenVPN bool
expectedWireGuard bool
}{
"deletes_openvpn_only": {
initialCredentials: map[string]providerCredentials{
"protonvpn": {
OpenVPN: &openvpnCredentials{Username: "user", Password: "pass"},
WireGuard: &wireguardCredentials{PrivateKey: wireguardPrivateKey.String()},
},
},
provider: "protonvpn",
vpnType: "openvpn",
expectedLength: 1,
expectedWireGuard: true,
},
"deletes_last_protocol_and_provider": {
initialCredentials: map[string]providerCredentials{
"protonvpn": {
OpenVPN: &openvpnCredentials{Username: "user", Password: "pass"},
},
},
provider: "protonvpn",
vpnType: "openvpn",
expectedLength: 0,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
credentials := cloneCredentials(testCase.initialCredentials)
err := deleteCredential(credentials, testCase.provider, testCase.vpnType)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(credentials) != testCase.expectedLength {
t.Fatalf("expected %d providers, got %d", testCase.expectedLength, len(credentials))
}
providerCredentials, exists := credentials[testCase.provider]
if !exists {
return
}
if (providerCredentials.OpenVPN != nil) != testCase.expectedOpenVPN {
t.Fatalf("expected openvpn presence %t, got %t", testCase.expectedOpenVPN, providerCredentials.OpenVPN != nil)
}
if (providerCredentials.WireGuard != nil) != testCase.expectedWireGuard {
t.Fatalf("expected wireguard presence %t, got %t", testCase.expectedWireGuard, providerCredentials.WireGuard != nil)
}
})
}
}
func Test_validateCredentials(t *testing.T) {
t.Parallel()
wireguardPrivateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
}
testCases := map[string]struct {
credentials map[string]providerCredentials
wantError bool
}{
"both_protocols_valid": {
credentials: map[string]providerCredentials{
"protonvpn": {
OpenVPN: &openvpnCredentials{Username: "user", Password: "pass"},
WireGuard: &wireguardCredentials{PrivateKey: wireguardPrivateKey.String()},
},
},
},
"invalid_wireguard_when_both_present": {
credentials: map[string]providerCredentials{
"protonvpn": {
OpenVPN: &openvpnCredentials{Username: "user", Password: "pass"},
WireGuard: &wireguardCredentials{PrivateKey: "invalid"},
},
},
wantError: true,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
err := validateCredentials(testCase.credentials)
if testCase.wantError && err == nil {
t.Fatal("expected an error but got nil")
}
if !testCase.wantError && err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
}
}
func Test_marshalLoadCredentials(t *testing.T) {
t.Parallel()
wireguardPrivateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
}
credentials := map[string]providerCredentials{
"mullvad": {
WireGuard: &wireguardCredentials{
PrivateKey: wireguardPrivateKey.String(),
Address: "10.0.0.2/32",
},
},
"protonvpn": {
OpenVPN: &openvpnCredentials{
Username: "user",
Password: "pass",
},
},
}
encoded, err := marshalCredentials(credentials)
if err != nil {
t.Fatalf("unexpected marshal error: %v", err)
}
decoded, err := loadCredentials(encoded)
if err != nil {
t.Fatalf("unexpected load error: %v", err)
}
if len(decoded) != len(credentials) {
t.Fatalf("expected %d providers, got %d", len(credentials), len(decoded))
}
if decoded["mullvad"].WireGuard == nil {
t.Fatal("expected mullvad wireguard credentials to be present")
}
if decoded["protonvpn"].OpenVPN == nil {
t.Fatal("expected protonvpn openvpn credentials to be present")
}
if decoded["protonvpn"].OpenVPN.Password != "pass" {
t.Fatalf("expected protonvpn password %q, got %q", "pass", decoded["protonvpn"].OpenVPN.Password)
}
}
func Test_formatCredentialForDump(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
provider string
vpnType string
providerCredentials providerCredentials
expectedOutput string
wantError bool
}{
"openvpn": {
provider: "protonvpn",
vpnType: vpnTypeOpenVPN,
providerCredentials: providerCredentials{
OpenVPN: &openvpnCredentials{
Username: "user",
Password: "pass",
Key: "key",
Cert: "cert",
},
},
expectedOutput: "provider: protonvpn\n" +
"vpn_type: openvpn\n" +
"username: user\n" +
"password: pass\n" +
"key: key\n" +
"cert: cert\n",
},
"wireguard": {
provider: "mullvad",
vpnType: vpnTypeWireGuard,
providerCredentials: providerCredentials{
WireGuard: &wireguardCredentials{
PrivateKey: "private",
Address: "10.0.0.2/32",
PresharedKey: "preshared",
},
},
expectedOutput: "provider: mullvad\n" +
"vpn_type: wireguard\n" +
"private_key: private\n" +
"address: 10.0.0.2/32\n" +
"preshared_key: preshared",
},
"missing_protocol": {
provider: "protonvpn",
vpnType: vpnTypeOpenVPN,
wantError: true,
},
"unknown_protocol": {
provider: "protonvpn",
vpnType: "other",
wantError: true,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
output, err := formatCredentialForDump(
testCase.provider,
testCase.vpnType,
testCase.providerCredentials,
)
if testCase.wantError {
if err == nil {
t.Fatal("expected an error but got nil")
}
return
}
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if output != testCase.expectedOutput {
t.Fatalf("expected output %q, got %q", testCase.expectedOutput, output)
}
})
}
}
func cloneCredentials(credentials map[string]providerCredentials) map[string]providerCredentials {
clone := make(map[string]providerCredentials, len(credentials))
for provider, providerCredentials := range credentials {
copied := providerCredentials
if providerCredentials.OpenVPN != nil {
openvpnCredentials := *providerCredentials.OpenVPN
copied.OpenVPN = &openvpnCredentials
}
if providerCredentials.WireGuard != nil {
wireguardCredentials := *providerCredentials.WireGuard
copied.WireGuard = &wireguardCredentials
}
clone[provider] = copied
}
return clone
}
-533
View File
@@ -1,533 +0,0 @@
package internal
import (
"bufio"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
"maps"
"os"
"slices"
"strings"
"sync"
"syscall"
"golang.org/x/crypto/scrypt"
"golang.org/x/term"
)
// Encryption format: [16-byte salt][12-byte nonce][AES-256-GCM ciphertext+tag]
// Key derivation: scrypt(password, salt, N=32768, r=8, p=1, keyLen=32)
const (
saltSize = 16
nonceSize = 12
keySize = 32
scryptN = 32768
scryptR = 8
scryptP = 1
)
// AddCredential prompts for credential values and stores them in the encrypted credentials file.
func AddCredential(ctx context.Context, provider, vpnType string) error {
credentials, password, err := loadCredentialsForMutation(ctx)
if err != nil {
return err
}
err = promptAndAddCredential(ctx, credentials, provider, vpnType)
if err != nil {
return err
}
err = validateCredentials(credentials)
if err != nil {
return fmt.Errorf("validating credentials: %w", err)
}
err = writeEncryptedCredentials(credentials, password)
if err != nil {
return err
}
fmt.Printf(
"Credentials for provider %q and vpn type %q saved to %s\n",
provider, vpnType, credentialsFilename,
)
return nil
}
// DeleteCredential removes credentials for a provider and VPN type
// from the encrypted credentials file.
func DeleteCredential(ctx context.Context, provider, vpnType string) error {
credentials, password, err := loadExistingCredentialsForMutation(ctx)
if err != nil {
return err
}
err = deleteCredential(credentials, provider, vpnType)
if err != nil {
return err
}
err = writeEncryptedCredentials(credentials, password)
if err != nil {
return err
}
fmt.Printf(
"Credentials for provider %q and vpn type %q removed from %s\n",
provider, vpnType, credentialsFilename,
)
return nil
}
// DumpCredential decrypts the credential store and prints one provider/vpn-type entry.
func DumpCredential(ctx context.Context, provider, vpnType string) error {
credentials, err := decryptCredentials(ctx)
if err != nil {
return err
}
providerCredentials, exists := credentials[provider]
if !exists {
existingProviders := slices.Collect(maps.Keys(credentials))
return fmt.Errorf("provider %q does not exist, available providers are: %s",
provider, strings.Join(existingProviders, ", "))
}
output, err := formatCredentialForDump(provider, vpnType, providerCredentials)
if err != nil {
return err
}
fmt.Println(output)
return nil
}
// decryptCredentials reads the encrypted credentials file,
// prompts for a password, and returns the decrypted credentials.
func decryptCredentials(ctx context.Context) (map[string]providerCredentials, error) {
password, err := readSecret(ctx, "Enter credentials password: ", false)
if err != nil {
return nil, fmt.Errorf("reading password: %w", err)
}
plaintext, err := decryptCredentialsFile(password)
if err != nil {
return nil, err
}
credentials, err := loadCredentials(plaintext)
if err != nil {
return nil, fmt.Errorf("loading credentials: %w", err)
}
return credentials, nil
}
func loadCredentialsForMutation(ctx context.Context) (
credentials map[string]providerCredentials,
password []byte,
err error,
) {
_, err = os.Stat(credentialsFilename)
if os.IsNotExist(err) {
password, err = readPasswordConfirmed(ctx,
"Enter new credentials password: ",
"Confirm new credentials password: ",
)
if err != nil {
return nil, nil, fmt.Errorf("reading password: %w", err)
}
return make(map[string]providerCredentials), password, nil
}
if err != nil {
return nil, nil, fmt.Errorf("stating %s: %w", credentialsFilename, err)
}
password, err = readSecret(ctx, "Enter credentials password: ", false)
if err != nil {
return nil, nil, fmt.Errorf("reading password: %w", err)
}
plaintext, err := decryptCredentialsFile(password)
if err != nil {
return nil, nil, err
}
credentials, err = loadCredentials(plaintext)
if err != nil {
return nil, nil, fmt.Errorf("loading credentials: %w", err)
}
return credentials, password, nil
}
func loadExistingCredentialsForMutation(ctx context.Context) (
credentials map[string]providerCredentials,
password []byte,
err error,
) {
_, err = os.Stat(credentialsFilename)
if os.IsNotExist(err) {
return nil, nil, fmt.Errorf("%s does not exist", credentialsFilename)
}
if err != nil {
return nil, nil, fmt.Errorf("stating %s: %w", credentialsFilename, err)
}
password, err = readSecret(ctx, "Enter credentials password: ", false)
if err != nil {
return nil, nil, fmt.Errorf("reading password: %w", err)
}
plaintext, err := decryptCredentialsFile(password)
if err != nil {
return nil, nil, err
}
credentials, err = loadCredentials(plaintext)
if err != nil {
return nil, nil, fmt.Errorf("loading credentials: %w", err)
}
return credentials, password, nil
}
func promptAndAddCredential(
ctx context.Context,
credentials map[string]providerCredentials,
provider, vpnType string,
) error {
switch vpnType {
case vpnTypeOpenVPN:
username, err := readLine(ctx, "OpenVPN username: ", true)
if err != nil {
return fmt.Errorf("reading username: %w", err)
}
password, err := readSecret(ctx, "OpenVPN password: ", username == "")
if err != nil {
return fmt.Errorf("reading password: %w", err)
}
key, err := readSecret(ctx, "OpenVPN key: ", true)
if err != nil {
return fmt.Errorf("reading key: %w", err)
}
cert, err := readSecret(ctx, "OpenVPN cert: ", true)
if err != nil {
return fmt.Errorf("reading cert: %w", err)
}
openvpnCredentials := &openvpnCredentials{
Username: username,
Password: string(password),
Key: string(key),
Cert: string(cert),
}
err = validateOpenvpnCredentials(provider, openvpnCredentials)
if err != nil {
return err
}
return addCredential(credentials, provider, vpnType, openvpnCredentials, nil)
case vpnTypeWireGuard:
privateKey, err := readSecret(ctx, "WireGuard private key: ", false)
if err != nil {
return fmt.Errorf("reading private key: %w", err)
}
address, err := readLine(ctx, "WireGuard address (optional): ", true)
if err != nil {
return fmt.Errorf("reading address: %w", err)
}
presharedKey, err := readSecret(
ctx,
"WireGuard preshared key (optional): ",
true,
)
if err != nil {
return fmt.Errorf("reading preshared key: %w", err)
}
wireguardCredentials := &wireguardCredentials{
PrivateKey: string(privateKey),
Address: address,
PresharedKey: string(presharedKey),
}
err = validateWireguardCredentials(provider, wireguardCredentials)
if err != nil {
return err
}
return addCredential(credentials, provider, vpnType, nil, wireguardCredentials)
default:
return fmt.Errorf("unknown vpn type %q, must be wireguard or openvpn", vpnType)
}
}
func writeEncryptedCredentials(
credentials map[string]providerCredentials,
password []byte,
) error {
plaintext, err := marshalCredentials(credentials)
if err != nil {
return fmt.Errorf("encoding credentials: %w", err)
}
encrypted, err := encryptData(plaintext, password)
if err != nil {
return fmt.Errorf("encrypting credentials: %w", err)
}
const filePerms = 0o600
err = os.WriteFile(credentialsFilename, encrypted, filePerms)
if err != nil {
return fmt.Errorf("writing %s: %w", credentialsFilename, err)
}
return nil
}
func decryptCredentialsFile(password []byte) ([]byte, error) {
encryptedData, err := os.ReadFile(credentialsFilename)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", credentialsFilename, err)
}
plaintext, err := decryptData(encryptedData, password)
if err != nil {
return nil, fmt.Errorf("decrypting credentials: %w", err)
}
return plaintext, nil
}
func readSecret(ctx context.Context, prompt string, allowEmpty bool) ([]byte, error) {
fmt.Print(prompt)
passwordFD, err := syscall.Dup(syscall.Stdin)
if err != nil {
fmt.Println()
return nil, fmt.Errorf("duplicating stdin file descriptor: %w", err)
}
var closeFDOnce sync.Once
closePasswordFD := func() {
closeFDOnce.Do(func() {
_ = syscall.Close(passwordFD)
})
}
passwordResult := make(chan struct {
password []byte
err error
})
go func() {
password, err := term.ReadPassword(passwordFD)
closePasswordFD()
result := struct {
password []byte
err error
}{
password: password,
err: err,
}
select {
case <-ctx.Done():
return
case passwordResult <- result:
}
}()
select {
case <-ctx.Done():
closePasswordFD()
fmt.Println()
return nil, ctx.Err()
case result := <-passwordResult:
closePasswordFD()
fmt.Println()
if result.err != nil {
return nil, fmt.Errorf("reading hidden input from terminal: %w", result.err)
}
if len(result.password) == 0 && !allowEmpty {
return nil, fmt.Errorf("value cannot be empty")
}
return result.password, nil
}
}
func readLine(ctx context.Context, prompt string, allowEmpty bool) (string, error) {
fmt.Print(prompt)
inputFD, err := syscall.Dup(syscall.Stdin)
if err != nil {
fmt.Println()
return "", fmt.Errorf("duplicating stdin file descriptor: %w", err)
}
var closeFDOnce sync.Once
closeInputFD := func() {
closeFDOnce.Do(func() {
_ = syscall.Close(inputFD)
})
}
inputResult := make(chan struct {
value string
err error
})
go func() {
inputFile := os.NewFile(uintptr(inputFD), "stdin")
reader := bufio.NewReader(inputFile)
value, err := reader.ReadString('\n')
closeInputFD()
value = strings.TrimRight(value, "\r\n")
if err == io.EOF {
err = nil
}
result := struct {
value string
err error
}{
value: value,
err: err,
}
select {
case <-ctx.Done():
return
case inputResult <- result:
}
}()
select {
case <-ctx.Done():
closeInputFD()
fmt.Println()
return "", ctx.Err()
case result := <-inputResult:
closeInputFD()
if result.err != nil {
return "", fmt.Errorf("reading line from terminal: %w", result.err)
}
if result.value == "" && !allowEmpty {
return "", fmt.Errorf("value cannot be empty")
}
return result.value, nil
}
}
func readPasswordConfirmed(
ctx context.Context,
prompt, confirmationPrompt string,
) ([]byte, error) {
password, err := readSecret(ctx, prompt, false)
if err != nil {
return nil, err
}
confirmation, err := readSecret(ctx, confirmationPrompt, false)
if err != nil {
return nil, err
}
if string(password) != string(confirmation) {
return nil, fmt.Errorf("passwords do not match")
}
return password, nil
}
func deriveKey(password, salt []byte) ([]byte, error) {
key, err := scrypt.Key(password, salt, scryptN, scryptR, scryptP, keySize)
if err != nil {
return nil, fmt.Errorf("deriving key with scrypt: %w", err)
}
return key, nil
}
func encryptData(plaintext, password []byte) ([]byte, error) {
salt := make([]byte, saltSize)
_, err := io.ReadFull(rand.Reader, salt)
if err != nil {
return nil, fmt.Errorf("generating salt: %w", err)
}
key, err := deriveKey(password, salt)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("creating AES cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("creating GCM: %w", err)
}
nonce := make([]byte, nonceSize)
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return nil, fmt.Errorf("generating nonce: %w", err)
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
result := make([]byte, 0, saltSize+nonceSize+len(ciphertext))
result = append(result, salt...)
result = append(result, nonce...)
result = append(result, ciphertext...)
return result, nil
}
func decryptData(data, password []byte) ([]byte, error) {
const minSize = saltSize + nonceSize + 16 // 16 is the GCM tag size
if len(data) < minSize {
return nil, fmt.Errorf("encrypted data too short: %d bytes", len(data))
}
salt := data[:saltSize]
nonce := data[saltSize : saltSize+nonceSize]
ciphertext := data[saltSize+nonceSize:]
key, err := deriveKey(password, salt)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("creating AES cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("creating GCM: %w", err)
}
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("decrypting data (wrong password?): %w", err)
}
return plaintext, nil
}
-351
View File
@@ -1,351 +0,0 @@
package internal
import (
"context"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/term"
)
type containerOptions struct {
env []string
binds []string
ports nat.PortMap
dns []string
devices []container.DeviceMapping
labels map[string]string
capAdd []string
}
// Run decrypts credentials, builds the container environment, and runs a Gluetun container.
// extraArgs is the list of additional flags (e.g. ["-e", "PORT_FORWARDING=on", "-v", "/host:/container"]).
func Run(ctx context.Context, provider, vpnType string, extraArgs []string,
forceKill <-chan struct{},
) error {
credentials, err := decryptCredentials(ctx)
if err != nil {
return fmt.Errorf("loading credentials: %w", err)
}
credentialEnvVars, err := lookupCredentials(credentials, provider, vpnType)
if err != nil {
return err
}
extraOpts, err := parseExtraArgs(extraArgs)
if err != nil {
return fmt.Errorf("parsing extra flags: %w", err)
}
opts := extraOpts
opts.env = append(opts.env,
"VPN_SERVICE_PROVIDER="+provider,
"VPN_TYPE="+vpnType,
"LOG_LEVEL=debug",
)
opts.env = append(opts.env, credentialEnvVars...)
opts.capAdd = append(opts.capAdd, "NET_ADMIN")
return runContainer(ctx, opts, forceKill)
}
// parseExtraArgs parses extra arguments and maps them to container options.
// Supported flags:
//
// -e, --env KEY=VALUE - environment variable
// -v, --volume SPEC - volume mount (e.g., "/host:/container" or "name:/container")
// -p, --publish PORT:PORT - port mapping
// --dns IP - DNS server
// --device SPEC - device access (e.g., "/dev/net/tun")
// --label KEY=VALUE - container label
// --cap-add CAPABILITY - add Linux capability (e.g., "SYS_PTRACE")
func parseExtraArgs(args []string) (opts containerOptions, err error) { //nolint:gocognit,gocyclo
opts = containerOptions{
ports: make(nat.PortMap),
labels: make(map[string]string),
}
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "-e" || arg == "--env":
if i+1 >= len(args) {
return opts, fmt.Errorf("flag %q requires an argument", arg)
}
i++
opts.env = append(opts.env, args[i])
case strings.HasPrefix(arg, "-e="):
opts.env = append(opts.env, strings.TrimPrefix(arg, "-e="))
case strings.HasPrefix(arg, "--env="):
opts.env = append(opts.env, strings.TrimPrefix(arg, "--env="))
case arg == "-v" || arg == "--volume":
if i+1 >= len(args) {
return opts, fmt.Errorf("flag %q requires an argument", arg)
}
i++
opts.binds = append(opts.binds, args[i])
case strings.HasPrefix(arg, "-v="):
opts.binds = append(opts.binds, strings.TrimPrefix(arg, "-v="))
case strings.HasPrefix(arg, "--volume="):
opts.binds = append(opts.binds, strings.TrimPrefix(arg, "--volume="))
case arg == "-p" || arg == "--publish":
if i+1 >= len(args) {
return opts, fmt.Errorf("flag %q requires an argument", arg)
}
i++
if err := parsePortMapping(opts.ports, args[i]); err != nil {
return opts, fmt.Errorf("parsing port mapping: %w", err)
}
case strings.HasPrefix(arg, "-p="):
if err := parsePortMapping(opts.ports, strings.TrimPrefix(arg, "-p=")); err != nil {
return opts, fmt.Errorf("parsing port mapping: %w", err)
}
case strings.HasPrefix(arg, "--publish="):
if err := parsePortMapping(opts.ports, strings.TrimPrefix(arg, "--publish=")); err != nil {
return opts, fmt.Errorf("parsing port mapping: %w", err)
}
case arg == "--dns":
if i+1 >= len(args) {
return opts, fmt.Errorf("flag %q requires an argument", arg)
}
i++
opts.dns = append(opts.dns, args[i])
case strings.HasPrefix(arg, "--dns="):
opts.dns = append(opts.dns, strings.TrimPrefix(arg, "--dns="))
case arg == "--device":
if i+1 >= len(args) {
return opts, fmt.Errorf("flag %q requires an argument", arg)
}
i++
parseDeviceMapping(&opts.devices, args[i])
case strings.HasPrefix(arg, "--device="):
parseDeviceMapping(&opts.devices, strings.TrimPrefix(arg, "--device="))
case arg == "--label":
if i+1 >= len(args) {
return opts, fmt.Errorf("flag %q requires an argument", arg)
}
i++
parseLabel(opts.labels, args[i])
case strings.HasPrefix(arg, "--label="):
parseLabel(opts.labels, strings.TrimPrefix(arg, "--label="))
case arg == "--cap-add":
if i+1 >= len(args) {
return opts, fmt.Errorf("flag %q requires an argument", arg)
}
i++
opts.capAdd = append(opts.capAdd, args[i])
case strings.HasPrefix(arg, "--cap-add="):
opts.capAdd = append(opts.capAdd, strings.TrimPrefix(arg, "--cap-add="))
default:
return opts, fmt.Errorf("unsupported flag %q", arg)
}
}
return opts, nil
}
func parsePortMapping(portMap nat.PortMap, spec string) error {
port, bindings, err := nat.ParsePortSpecs([]string{spec})
if err != nil {
return err
}
for p, binding := range bindings {
portMap[p] = binding
}
for p := range port {
if _, exists := portMap[p]; !exists {
portMap[p] = []nat.PortBinding{}
}
}
return nil
}
func parseDeviceMapping(devices *[]container.DeviceMapping, spec string) {
parts := strings.SplitN(spec, ":", 3) //nolint:mnd
pathOnHost := parts[0]
pathInContainer := pathOnHost
permissions := "rwm"
if len(parts) >= 2 { //nolint:mnd
pathInContainer = parts[1]
}
if len(parts) >= 3 { //nolint:mnd
permissions = parts[2]
}
*devices = append(*devices, container.DeviceMapping{
PathOnHost: pathOnHost,
PathInContainer: pathInContainer,
CgroupPermissions: permissions,
})
}
func parseLabel(labels map[string]string, kv string) {
parts := strings.SplitN(kv, "=", 2) //nolint:mnd
key := parts[0]
value := ""
if len(parts) > 1 {
value = parts[1]
}
labels[key] = value
}
func runContainer(ctx context.Context, opts containerOptions, forceKill <-chan struct{}) error {
dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("creating docker client: %w", err)
}
defer dockerClient.Close()
hasTTY := term.IsTerminal(int(os.Stdout.Fd()))
containerConfig := &container.Config{
Image: "qmcgaw/gluetun",
Env: opts.env,
Labels: opts.labels,
Tty: hasTTY,
}
mounts := make([]mount.Mount, 0, len(opts.binds))
for _, bind := range opts.binds {
m, err := parseBindMount(bind)
if err != nil {
return fmt.Errorf("parsing bind mount %q: %w", bind, err)
}
mounts = append(mounts, m)
}
hostConfig := &container.HostConfig{
AutoRemove: true,
CapAdd: opts.capAdd,
Binds: opts.binds,
Mounts: mounts,
PortBindings: opts.ports,
DNS: opts.dns,
}
hostConfig.Devices = opts.devices
networkConfig := &network.NetworkingConfig{}
platform := (*v1.Platform)(nil)
const containerName = "gluetun"
response, err := dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, platform, containerName)
if err != nil {
return fmt.Errorf("creating container: %w", err)
}
for _, warning := range response.Warnings {
fmt.Fprintln(os.Stderr, "container creation warning:", warning)
}
containerID := response.ID
err = dockerClient.ContainerStart(ctx, containerID, container.StartOptions{})
if err != nil {
return fmt.Errorf("starting container: %w", err)
}
fmt.Printf("Container started (id: %.12s)\n", containerID)
streamLogsErr := make(chan error, 1)
go func() {
streamLogsErr <- streamLogs(context.Background(), dockerClient, containerID, hasTTY)
}()
contextDone := ctx.Done()
forceKillSignal := forceKill
for {
select {
case err := <-streamLogsErr:
if err != nil {
return err
}
return nil
case <-contextDone:
fmt.Fprintln(os.Stderr, "\nReceived interrupt, stopping container (5s timeout)...")
err = stopContainer(dockerClient, containerID)
if err != nil {
fmt.Fprintln(os.Stderr, "stopping container:", err)
}
contextDone = nil
case <-forceKillSignal:
fmt.Fprintln(os.Stderr, "\nReceived second interrupt, killing container...")
err = killContainer(dockerClient, containerID)
if err != nil {
fmt.Fprintln(os.Stderr, "killing container:", err)
}
forceKillSignal = nil
}
}
}
func parseBindMount(bind string) (mount.Mount, error) {
parts := strings.SplitN(bind, ":", 3) //nolint:mnd
if len(parts) < 2 { //nolint:mnd
return mount.Mount{}, fmt.Errorf("invalid bind mount format: %q (expected source:target[:mode])", bind)
}
source := parts[0]
target := parts[1]
readOnly := len(parts) > 2 && strings.Contains(parts[2], "ro") //nolint:mnd
return mount.Mount{
Type: mount.TypeBind,
Source: source,
Target: target,
ReadOnly: readOnly,
}, nil
}
func stopContainer(dockerClient *client.Client, containerID string) error {
const stopTimeout = 5 * time.Second
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
defer stopCancel()
timeoutSeconds := int(stopTimeout.Seconds())
return dockerClient.ContainerStop(stopCtx, containerID, container.StopOptions{Timeout: &timeoutSeconds})
}
func killContainer(dockerClient *client.Client, containerID string) error {
return dockerClient.ContainerKill(context.Background(), containerID, "KILL")
}
func streamLogs(ctx context.Context, dockerClient *client.Client, containerID string, hasTTY bool) error {
logOptions := container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Timestamps: false,
}
reader, err := dockerClient.ContainerLogs(ctx, containerID, logOptions)
if err != nil {
return fmt.Errorf("getting container logs: %w", err)
}
defer reader.Close()
if hasTTY {
_, err = io.Copy(os.Stdout, reader)
} else {
_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, reader)
}
if err != nil && err != io.EOF {
return fmt.Errorf("streaming container logs: %w", err)
}
return nil
}
+26 -33
View File
@@ -1,66 +1,59 @@
module github.com/qdm12/gluetun
go 1.25.0
go 1.23
require (
github.com/ProtonMail/go-srp v0.0.7
github.com/amnezia-vpn/amneziawg-go v0.2.18
github.com/breml/rootcerts v0.3.4
github.com/breml/rootcerts v0.2.18
github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0
github.com/jsimonetti/rtnetlink v1.4.2
github.com/klauspost/compress v1.18.4
github.com/klauspost/compress v1.17.11
github.com/klauspost/pgzip v1.2.6
github.com/mdlayher/genetlink v1.4.0
github.com/mdlayher/netlink v1.9.0
github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
github.com/qdm12/gluetun-servers v0.1.0
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978
github.com/qdm12/gosettings v0.4.4
github.com/pelletier/go-toml/v2 v2.2.3
github.com/qdm12/dns/v2 v2.0.0-rc8
github.com/qdm12/gosettings v0.4.3
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/log v0.1.0
github.com/qdm12/ss-server v0.6.0
github.com/stretchr/testify v1.11.1
github.com/ti-mo/netfilter v0.5.3
github.com/ulikunitz/xz v0.5.15
github.com/stretchr/testify v1.9.0
github.com/ulikunitz/xz v0.5.11
github.com/vishvananda/netlink v1.2.1
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.org/x/net v0.55.0
golang.org/x/sys v0.45.0
golang.org/x/text v0.38.0
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
golang.org/x/net v0.30.0
golang.org/x/sys v0.27.0
golang.org/x/text v0.19.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
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 (
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cronokirby/saferith v0.33.0 // 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-isatty v0.0.20 // indirect
github.com/mdlayher/socket v0.6.0 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/qdm12/goservices v0.1.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/tools v0.45.0 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+56 -109
View File
@@ -1,42 +1,23 @@
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
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/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/amnezia-vpn/amneziawg-go v0.2.18 h1:pUn7/P8qdGmHd6JmE3bCQXPblZs3vruWR98nLODQLJg=
github.com/amnezia-vpn/amneziawg-go v0.2.18/go.mod h1:aMgOk9MuX0xI7b5TKAYp8pLM54RlXcOPzDvYw3YEO5A=
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/breml/rootcerts v0.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys=
github.com/breml/rootcerts v0.3.4/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
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/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.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
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/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/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/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -48,22 +29,20 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o=
github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA=
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
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/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
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/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/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
@@ -74,18 +53,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/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
github.com/qdm12/gluetun-servers v0.1.0 h1:w9JLghKZwI0Gzpp9p5rNANgEYUUZ1dxdxsG6NKIojaY=
github.com/qdm12/gluetun-servers v0.1.0/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
github.com/qdm12/gosettings v0.4.3 h1:oGAjiKVtml9oHVlPQo6H3yk6TmtWpVYicNeGFcM7AP8=
github.com/qdm12/gosettings v0.4.3/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
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.1-0.20260305164749-b713de4fee6c/go.mod h1:vgRg8Skq9+RNp1THecwMI7SGsnIwO/NPMfYenNTgpAc=
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
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/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
@@ -96,88 +73,59 @@ 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/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/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/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/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/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/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
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=
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-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-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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
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/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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
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-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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.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.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/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-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.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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -192,13 +140,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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
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/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
+5 -2
View File
@@ -1,6 +1,7 @@
package alpine
import (
"errors"
"fmt"
"io/fs"
"os"
@@ -8,6 +9,8 @@ import (
"strconv"
)
var ErrUserAlreadyExists = errors.New("user already exists")
// CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
UIDStr := strconv.Itoa(uid)
@@ -31,8 +34,8 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
}
if u != nil {
return "", fmt.Errorf("user already exists: with name %s for ID %s instead of %d",
username, u.Uid, uid)
return "", fmt.Errorf("%w: with name %s for ID %s instead of %d",
ErrUserAlreadyExists, username, u.Uid, uid)
}
const permission = fs.FileMode(0o644)
-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
}
-87
View File
@@ -1,87 +0,0 @@
package amneziawg
import (
"errors"
"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: errors.New("private key is missing"),
},
"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)
}
-127
View File
@@ -1,127 +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"
)
// 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("TUN device name is mismatching: expected %q and got %q", 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 = errors.New("device waited for")
}
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 {
logger.Infof("gluetun.com is DOWN most likely thanks to you! so not doing anything anymore")
}
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)
}
+6 -2
View File
@@ -1,7 +1,11 @@
package cli
type CLI struct{}
type CLI struct {
repoServersPath string
}
func New() *CLI {
return &CLI{}
return &CLI{
repoServersPath: "./internal/storage/servers.json",
}
}
+13 -4
View File
@@ -9,11 +9,18 @@ import (
"path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/storage"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
)
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
provider string, titleCaser cases.Caser,
) {
@@ -58,10 +65,11 @@ func (c *CLI) FormatServers(args []string) error {
}
switch len(providers) {
case 0:
return errors.New("VPN provider to format was not specified")
return fmt.Errorf("%w", ErrProviderUnspecified)
case 1:
default:
return fmt.Errorf("more than one VPN provider to format were specified: %d specified: %s", len(providers),
return fmt.Errorf("%w: %d specified: %s",
ErrMultipleProvidersToFormat, len(providers),
strings.Join(providers, ", "))
}
@@ -72,9 +80,10 @@ func (c *CLI) FormatServers(args []string) error {
}
}
storage, err := setupStorage(newNoopLogger())
logger := newNoopLogger()
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("setting up storage: %w", err)
return fmt.Errorf("creating servers storage: %w", err)
}
formatted, err := storage.Format(providerToFormat, format)
-39
View File
@@ -1,39 +0,0 @@
package cli
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/reader/sources/env"
)
type storageSetupLogger interface {
storage.Logger
files.Warner
}
func setupStorage(logger storageSetupLogger) (s *storage.Storage, err error) {
settingsReader := reader.New(reader.Settings{
Sources: []reader.Source{
secrets.New(logger),
files.New(logger),
env.New(env.Settings{}),
},
})
var settings settings.Storage
err = settings.Read(settingsReader)
if err != nil {
return nil, fmt.Errorf("reading storage settings: %w", err)
}
settings.SetDefaults()
storage, err := storage.New(logger, *settings.ServersEnabled, settings.ServersPath,
settings.LegacyServersFilepath)
if err != nil {
return nil, fmt.Errorf("creating storage: %w", err)
}
return storage, nil
}
-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
}
+1 -4
View File
@@ -6,7 +6,4 @@ func newNoopLogger() *noopLogger {
return new(noopLogger)
}
func (l *noopLogger) Info(string) {}
func (l *noopLogger) Infof(string, ...any) {}
func (l *noopLogger) Warn(string) {}
func (l *noopLogger) Warnf(string, ...any) {}
func (l *noopLogger) Info(string) {}
+10 -14
View File
@@ -9,10 +9,11 @@ import (
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gosettings/reader"
)
@@ -39,17 +40,15 @@ type IPFetcher interface {
}
type IPv6Checker interface {
FindIPv6SupportLevel(ctx context.Context,
checkAddresses []netip.AddrPort, firewall netlink.Firewall,
) (level netlink.IPv6SupportLevel, err error)
IsIPv6Supported() (supported bool, err error)
}
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
ipv6Checker IPv6Checker,
) error {
storage, err := setupStorage(newNoopLogger())
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("setting up storage: %w", err)
return err
}
var allSettings settings.Settings
@@ -57,16 +56,13 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
if err != nil {
return err
}
allSettings.SetDefaults()
ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel(context.Background(),
allSettings.IPv6.CheckAddresses, &noopFirewall{})
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err)
}
err = allSettings.Validate(storage, ipv6SupportLevel.IsSupported(), logger)
if err != nil {
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
return fmt.Errorf("validating settings: %w", err)
}
@@ -79,16 +75,16 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, warner, client,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6SupportLevel == netlink.IPv6Internet)
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
if err != nil {
return err
}
lines := providerConf.OpenVPNConfig(connection,
allSettings.VPN.OpenVPN, ipv6SupportLevel.IsSupported())
allSettings.VPN.OpenVPN, ipv6Supported)
fmt.Println(strings.Join(lines, "\n"))
return nil
+27 -54
View File
@@ -5,84 +5,65 @@ import (
"errors"
"flag"
"fmt"
"net"
"net/http"
"slices"
"strings"
"time"
"github.com/qdm12/dns/v2/pkg/doh"
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
)
var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrNoProviderSpecified = errors.New("no provider was specified")
)
type UpdaterLogger interface {
Info(s string)
Infof(format string, args ...any)
Warn(s string)
Warnf(format string, args ...any)
Error(s string)
}
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{}
// TODO v4: remove flags below already present in standard settings
var endUserMode, maintainerMode bool
var updateAll bool
var dnsServer, csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
var endUserMode, maintainerMode, updateAll bool
var csvProviders, ipToken string
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.StringVar(&dnsServer, "dns", "", "no longer used, your DNS will use DoH with Cloudflare and Google")
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false,
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
const defaultMinRatio = 0.8
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
"Minimum ratio of servers to find for the update to succeed")
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
flagSet.StringVar(&protonUsername, "proton-username", "",
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
flagSet.BoolVar(&endUserMode, "enduser", false, "deprecated")
flagSet.BoolVar(&maintainerMode, "maintainer", false, "deprecated")
if err := flagSet.Parse(args); err != nil {
return err
}
switch {
case dnsServer != "":
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
case endUserMode:
logger.Warn("The -enduser flag is now unused")
case maintainerMode:
logger.Warn("The -maintainer flag is now unused")
if !endUserMode && !maintainerMode {
return fmt.Errorf("%w", ErrModeUnspecified)
}
if updateAll {
options.Providers = providers.All()
} else {
if csvProviders == "" {
return errors.New("no provider was specified")
return fmt.Errorf("%w", ErrNoProviderSpecified)
}
options.Providers = strings.Split(csvProviders, ",")
}
if slices.Contains(options.Providers, providers.Protonvpn) {
if protonEmail == "" && protonUsername != "" {
protonEmail = protonUsername + "@protonmail.com"
logger.Warn("use -proton-email instead of -proton-username in the future. " +
"This assumes the email is " + protonEmail + " and may not work.")
}
options.ProtonEmail = &protonEmail
options.ProtonPassword = &protonPassword
}
options.SetDefaults(options.Providers[0])
err := options.Validate()
@@ -90,30 +71,15 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return fmt.Errorf("options validation failed: %w", err)
}
storage, err := setupStorage(logger)
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
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)
}
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: dnsDialer.Dial,
}
const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(dnsDialer)
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
nameTokenPairs := []api.NameToken{
{Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)},
@@ -128,13 +94,20 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
updater := updater.New(httpClient, storage, providers, logger, *options.PreferDirectDownload)
updater := updater.New(httpClient, storage, providers, logger)
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
if err != nil {
return fmt.Errorf("updating server information: %w", err)
}
if maintainerMode {
err := storage.FlushToFile(c.repoServersPath)
if err != nil {
return fmt.Errorf("writing servers data to embedded JSON file: %w", err)
}
}
return nil
}
-143
View File
@@ -1,143 +0,0 @@
package command
import (
"bytes"
"errors"
"fmt"
"strings"
"unicode/utf8"
)
// split splits a command string into a slice of arguments.
// This is especially important for commands such as:
// /bin/sh -c "echo hello"
// which should be split into: ["/bin/sh", "-c", "echo hello"]
// It supports backslash-escapes, single-quotes and double-quotes.
// It does not support:
// - the $" quoting style.
// - expansion (brace, shell or pathname).
func split(command string) (words []string, err error) {
if command == "" {
return nil, errors.New("command is empty")
}
const bufferSize = 1024
buffer := bytes.NewBuffer(make([]byte, bufferSize))
startIndex := 0
for startIndex < len(command) {
// skip any split characters at the start
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
switch {
case strings.ContainsRune(" \n\t", character):
startIndex += runeSize
case character == '\\':
// Look ahead to eventually skip an escaped newline
if command[startIndex+runeSize:] == "" {
return nil, fmt.Errorf("unterminated backslash-escape: %q", command)
}
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
if character == '\n' {
startIndex += runeSize + runeSize // backslash and newline
}
default:
var word string
buffer.Reset()
word, startIndex, err = splitWord(command, startIndex, buffer)
if err != nil {
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
}
words = append(words, word)
}
}
return words, nil
}
// WARNING: buffer must be cleared before calling this function.
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
cursor := startIndex
for cursor < len(input) {
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
cursor += runeLength
if character == '"' ||
character == '\'' ||
character == '\\' ||
character == ' ' ||
character == '\n' ||
character == '\t' {
buffer.WriteString(input[startIndex : cursor-runeLength])
}
switch {
case strings.ContainsRune(" \n\t", character): // spacing character
return buffer.String(), cursor, nil
case character == '"':
return handleDoubleQuoted(input, cursor, buffer)
case character == '\'':
return handleSingleQuoted(input, cursor, buffer)
case character == '\\':
return handleEscaped(input, cursor, buffer)
}
}
buffer.WriteString(input[startIndex:])
return buffer.String(), len(input), nil
}
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
cursor := startIndex
for cursor < len(input) {
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
cursor += nextRuneLength
switch nextCharacter {
case '"': // end of the double quoted string
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
return splitWord(input, cursor, buffer)
case '\\': // escaped character
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
cursor += escapedRuneLength
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
break
}
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
if escapedCharacter != '\n' {
// skip backslash entirely for the newline character
buffer.WriteRune(escapedCharacter)
}
startIndex = cursor
}
}
return "", 0, errors.New("unterminated double-quoted string")
}
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
if closingQuoteIndex == -1 {
return "", 0, errors.New("unterminated single-quoted string")
}
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
const singleQuoteRuneLength = 1
startIndex += closingQuoteIndex + singleQuoteRuneLength
return splitWord(input, startIndex, buffer)
}
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
if input[startIndex:] == "" {
return "", 0, errors.New("unterminated backslash-escape")
}
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
if character != '\n' { // backslash-escaped newline is ignored
buffer.WriteString(input[startIndex : startIndex+runeLength])
}
startIndex += runeLength
return splitWord(input, startIndex, buffer)
}
-105
View File
@@ -1,105 +0,0 @@
package command
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_split(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
command string
words []string
errMessage string
}{
"empty": {
command: "",
errMessage: "command is empty",
},
"concrete_sh_command": {
command: `/bin/sh -c "echo 123"`,
words: []string{"/bin/sh", "-c", "echo 123"},
},
"single_word": {
command: "word1",
words: []string{"word1"},
},
"two_words_single_space": {
command: "word1 word2",
words: []string{"word1", "word2"},
},
"two_words_multiple_space": {
command: "word1 word2",
words: []string{"word1", "word2"},
},
"two_words_no_expansion": {
command: "word1* word2?",
words: []string{"word1*", "word2?"},
},
"escaped_single quote": {
command: "ain\\'t good",
words: []string{"ain't", "good"},
},
"escaped_single_quote_all_single_quoted": {
command: "'ain'\\''t good'",
words: []string{"ain't good"},
},
"empty_single_quoted": {
command: "word1 '' word2",
words: []string{"word1", "", "word2"},
},
"escaped_newline": {
command: "word1\\\nword2",
words: []string{"word1word2"},
},
"quoted_newline": {
command: "text \"with\na\" quoted newline",
words: []string{"text", "with\na", "quoted", "newline"},
},
"quoted_escaped_newline": {
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
words: []string{"word1\\d\\\" word2word3 word4"},
},
"escaped_separated_newline": {
command: "word1 \\\n word2",
words: []string{"word1", "word2"},
},
"double_quotes_no_spacing": {
command: "word1\"word2\"word3",
words: []string{"word1word2word3"},
},
"unterminated_single_quote": {
command: "'abc'\\''def",
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
},
"unterminated_double_quote": {
command: "\"abc'def",
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
},
"unterminated_escape": {
command: "abc\\",
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
},
"unterminated_escape_only": {
command: " \\",
errMessage: `unterminated backslash-escape: " \\"`,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
words, err := split(testCase.command)
assert.Equal(t, testCase.words, words)
if testCase.errMessage != "" {
assert.ErrorContains(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
}
+21 -18
View File
@@ -9,9 +9,8 @@ import (
)
// Start launches a command and streams stdout and stderr to channels.
// stdoutLines and stderrLines channels will be closed when there is no more
// output to read, in order for the caller to catch all lines even after the
// command has finished. The waitError channel returned will never be closed.
// All the channels returned are ready only and won't be closed
// if the command fails later.
func (c *Cmder) Start(cmd *exec.Cmd) (
stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error,
@@ -22,6 +21,7 @@ func (c *Cmder) Start(cmd *exec.Cmd) (
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error,
) {
stop := make(chan struct{})
stdoutReady := make(chan struct{})
stdoutLinesCh := make(chan string)
stdoutDone := make(chan struct{})
@@ -33,47 +33,43 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
if err != nil {
return nil, nil, nil, err
}
go streamToChannel(stdoutReady, stdoutDone, stdout, stdoutLinesCh)
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
stderr, err := cmd.StderrPipe()
if err != nil {
_ = stdout.Close()
close(stop)
<-stdoutDone
close(stdoutLinesCh)
return nil, nil, nil, err
}
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
err = cmd.Start()
if err != nil {
_ = stdout.Close()
<-stdoutDone
close(stdoutLinesCh)
_ = stderr.Close()
close(stop)
<-stdoutDone
<-stderrDone
close(stderrLinesCh)
return nil, nil, nil, err
}
waitErrorCh := make(chan error)
go func() {
err := cmd.Wait()
<-stdoutDone
close(stdoutLinesCh)
_ = stdout.Close()
<-stderrDone
close(stderrLinesCh)
_ = stderr.Close()
close(stop)
<-stdoutDone
<-stderrDone
waitErrorCh <- err
}()
<-stdoutReady
<-stderrReady
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
}
func streamToChannel(ready chan<- struct{}, done chan<- struct{},
func streamToChannel(ready chan<- struct{},
stop <-chan struct{}, done chan<- struct{},
stream io.Reader, lines chan<- string,
) {
defer close(done)
@@ -93,5 +89,12 @@ func streamToChannel(ready chan<- struct{}, done chan<- struct{},
if err == nil || errors.Is(err, os.ErrClosed) {
return
}
lines <- "stream error: " + err.Error()
// ignore the error if it is stopped.
select {
case <-stop:
return
default:
lines <- "stream error: " + err.Error()
}
}
+24 -42
View File
@@ -89,48 +89,30 @@ func Test_start(t *testing.T) {
require.NoError(t, err)
collectAndCheckChannels(t, stdoutLines, stderrLines, waitError,
testCase.stdout, testCase.stderr, testCase.waitErr)
var stdoutIndex, stderrIndex int
done := false
for !done {
select {
case line := <-stdoutLines:
assert.Equal(t, testCase.stdout[stdoutIndex], line)
stdoutIndex++
case line := <-stderrLines:
assert.Equal(t, testCase.stderr[stderrIndex], line)
stderrIndex++
case err := <-waitError:
if testCase.waitErr != nil {
require.Error(t, err)
assert.Equal(t, testCase.waitErr.Error(), err.Error())
} else {
assert.NoError(t, err)
}
done = true
}
}
assert.Equal(t, len(testCase.stdout), stdoutIndex)
assert.Equal(t, len(testCase.stderr), stderrIndex)
})
}
}
func collectAndCheckChannels(t *testing.T, stdoutLines, stderrLines <-chan string,
waitError <-chan error, expectedStdout, expectedStderr []string, expectedWaitErr error,
) {
t.Helper()
stdoutIndex := 0
stderrIndex := 0
done := false
for !done {
select {
case line, ok := <-stdoutLines:
if !ok {
stdoutLines = nil
continue
}
assert.Equal(t, expectedStdout[stdoutIndex], line)
stdoutIndex++
case line, ok := <-stderrLines:
if !ok {
stderrLines = nil
continue
}
assert.Equal(t, expectedStderr[stderrIndex], line)
stderrIndex++
case err := <-waitError:
if expectedWaitErr != nil {
require.Error(t, err)
assert.Equal(t, expectedWaitErr.Error(), err.Error())
} else {
assert.NoError(t, err)
}
done = true
}
}
assert.Equal(t, len(expectedStdout), stdoutIndex)
assert.Equal(t, len(expectedStderr), stderrIndex)
}
-56
View File
@@ -1,56 +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
}
streamDone := make(chan struct{})
go streamLines(streamDone, logger, stdout, stderr)
err = <-waitError
<-streamDone
return err
}
func streamLines(done chan<- struct{}, logger Logger,
stdout, stderr <-chan string,
) {
defer close(done)
for {
select {
case line, ok := <-stdout:
if ok {
logger.Info(line)
break
}
if stderr == nil {
return
}
stdout = nil
case line, ok := <-stderr:
if ok {
logger.Error(line)
break
}
if stdout == nil {
return
}
stderr = nil
}
}
}
@@ -1,234 +0,0 @@
package settings
import (
"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
}
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("junk packet count must be set when junk packet min or max is set: "+
"jc=%d and jmin=%d and jmax=%d", a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
}
} else {
if *a.JunkPacketMin == 0 || *a.JunkPacketMax == 0 {
return fmt.Errorf("junk packet min and max must be set when junk packet count is set: "+
"jc=%d and jmin=%d and jmax=%d", a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
} else if *a.JunkPacketMin > *a.JunkPacketMax {
return fmt.Errorf("junk packet minimum must be lower than or equal to maximum: "+
"jmin=%d and jmax=%d", *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("header range is malformed: "+
"%s value %s is not a number", name, headerRange)
}
case 2: //nolint:mnd
for _, field := range fields {
_, err := strconv.Atoi(field)
if err != nil {
return fmt.Errorf("header range is malformed: "+
"%s value %s is not a valid range", name, headerRange)
}
}
default:
return fmt.Errorf("header range is malformed: "+
"%s value %s must be in the form n or n-m", 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
}
@@ -1,23 +1,19 @@
package settings
import (
"maps"
"slices"
"github.com/qdm12/gosettings/reader"
"golang.org/x/exp/maps"
)
func readObsolete(r *reader.Reader) (warnings []string) {
keyToMessage := map[string]string{
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
"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_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because you should use the built-in server which now " +
"forwards local names to private DNS resolvers found in /etc/resolv.conf at container start",
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
}
sortedKeys := slices.Collect(maps.Keys(keyToMessage))
sortedKeys := maps.Keys(keyToMessage)
slices.Sort(sortedKeys)
warnings = make([]string, 0, len(keyToMessage))
for _, key := range sortedKeys {
+40 -225
View File
@@ -3,130 +3,50 @@ package settings
import (
"fmt"
"net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
const (
DNSUpstreamTypeDot = "dot"
DNSUpstreamTypeDoh = "doh"
DNSUpstreamTypePlain = "plain"
)
// DNS contains settings to configure DNS.
type DNS struct {
// ServerEnabled indicates if the DNS server should be enabled.
// It defaults to true and cannot be nil in the internal state.
ServerEnabled *bool `json:"enabled"`
// UpstreamType can be [DNSUpstreamTypeDot], [DNSUpstreamTypeDoh]
// or [DNSUpstreamTypePlain]. It defaults to [DNSUpstreamTypeDot].
UpstreamType string `json:"upstream_type"`
// UpdatePeriod is the period to update DNS block lists.
// It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in
// the internal state.
UpdatePeriod *time.Duration
// 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"`
// Caching is true if the server should cache
// DNS responses.
Caching *bool `json:"caching"`
// IPv6 is true if the server should connect over IPv6.
IPv6 *bool `json:"ipv6"`
// Blacklist contains settings to configure the filter
// block lists.
Blacklist DNSBlacklist
// UpstreamPlainAddresses are the upstream plaintext DNS resolver
// addresses to use by the built-in DNS server forwarder.
// Note, if the upstream type is [dnsUpstreamTypePlain] and this field is set,
// the Providers field is ignored.
UpstreamPlainAddresses []netip.AddrPort
// ServerAddress is the DNS server to use inside
// the Go program and for the system.
// It defaults to '127.0.0.1' to be used with the
// DoT server. It cannot be the zero value in the internal
// 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 over TLS server and the
// `ServerAddress` field will be ignored.
// It defaults to false and cannot be nil in the
// internal state.
KeepNameserver *bool
// DOT contains settings to configure the DoT
// server.
DoT DoT
}
func (d DNS) validate() (err error) {
if !helpers.IsOneOf(d.UpstreamType, DNSUpstreamTypeDot, DNSUpstreamTypeDoh, DNSUpstreamTypePlain) {
return fmt.Errorf("DNS upstream type is not valid: %s", d.UpstreamType)
}
if !*d.ServerEnabled {
err = d.validateForServerOff()
if err != nil {
return err
}
}
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("update period is too short: %s must be bigger than %s",
*d.UpdatePeriod, minUpdatePeriod)
}
if d.UpstreamType == DNSUpstreamTypePlain {
selectedHasPlainIPv4, selectedHasPlainIPv6 := false, false
for _, addrPort := range d.UpstreamPlainAddresses {
if !selectedHasPlainIPv4 && addrPort.Addr().Is4() {
selectedHasPlainIPv4 = true
}
if !selectedHasPlainIPv6 && addrPort.Addr().Is6() {
selectedHasPlainIPv6 = true
}
if selectedHasPlainIPv4 && selectedHasPlainIPv6 {
break
}
}
switch {
case *d.IPv6 && !selectedHasPlainIPv6:
return fmt.Errorf("upstream plain addresses do not contain any IPv6 address: "+
"in %d addresses", len(d.UpstreamPlainAddresses))
case !*d.IPv6 && !selectedHasPlainIPv4:
return fmt.Errorf("upstream plain addresses do not contain any IPv4 address: "+
"in %d addresses", len(d.UpstreamPlainAddresses))
}
}
// Note: all DNS built in providers have both IPv4 and IPv6 addresses for all modes
err = d.Blacklist.validate()
err = d.DoT.validate()
if err != nil {
return err
return fmt.Errorf("validating DoT settings: %w", err)
}
return nil
}
func (d DNS) validateForServerOff() (err error) {
switch {
case d.UpstreamType != DNSUpstreamTypePlain:
return fmt.Errorf("upstream type %s must be %s if the built-in DNS server is disabled",
d.UpstreamType, DNSUpstreamTypePlain)
case len(d.UpstreamPlainAddresses) == 0:
return fmt.Errorf("if DNS is disabled, at least one upstream plain address must be set")
}
for _, addrPort := range d.UpstreamPlainAddresses {
const defaultDNSPort = 53
if addrPort.Port() != defaultDNSPort {
return fmt.Errorf("invalid DNS port in %s: must be %d", addrPort, defaultDNSPort)
}
}
return nil
}
func (d *DNS) Copy() (copied DNS) {
return DNS{
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
UpstreamType: d.UpstreamType,
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
UpstreamPlainAddresses: gosettings.CopySlice(d.UpstreamPlainAddresses),
ServerAddress: d.ServerAddress,
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
DoT: d.DoT.copy(),
}
}
@@ -134,36 +54,16 @@ func (d *DNS) Copy() (copied DNS) {
// settings object with any field set in the other
// settings.
func (d *DNS) overrideWith(other DNS) {
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
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)
d.DoT.overrideWith(other.DoT)
}
func (d *DNS) setDefaults() {
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
defaultUpstreamType := DNSUpstreamTypeDot
if !*d.ServerEnabled {
defaultUpstreamType = DNSUpstreamTypePlain
}
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, defaultUpstreamType)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.UpstreamPlainAddresses = gosettings.DefaultSlice(d.UpstreamPlainAddresses, []netip.AddrPort{})
d.Providers = gosettings.DefaultSlice(d.Providers, defaultDNSProviders())
d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults()
}
func defaultDNSProviders() []string {
return []string{
provider.Cloudflare().Name,
}
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
d.DoT.setDefaults()
}
func (d DNS) String() string {
@@ -172,115 +72,30 @@ func (d DNS) String() string {
func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:")
if !*d.ServerEnabled {
plainServers := node.Append("Plain DNS servers to use directly:")
for _, addr := range d.UpstreamPlainAddresses {
plainServers.Append(addr.String())
}
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
if *d.KeepNameserver {
return node
}
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
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 {
upstreamResolvers.Append(provider)
}
}
} else {
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
update := "disabled"
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
node.AppendNode(d.Blacklist.toLinesNode())
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.AppendNode(d.DoT.toLinesNode())
return node
}
func (d *DNS) read(r *reader.Reader) (err error) {
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
if err != nil {
return err
}
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
if err != nil {
return err
}
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
err = d.DoT.read(r)
if err != nil {
return err
}
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
if err != nil {
return err
}
err = d.Blacklist.read(r)
if err != nil {
return err
}
err = d.readUpstreamPlainAddresses(r)
if err != nil {
return err
return fmt.Errorf("DNS over TLS settings: %w", err)
}
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_RESOLVER_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")
}
+24 -65
View File
@@ -1,6 +1,7 @@
package settings
import (
"errors"
"fmt"
"net/http"
"net/netip"
@@ -21,11 +22,6 @@ type DNSBlacklist struct {
AddBlockedHosts []string
AddBlockedIPs []netip.Addr
AddBlockedIPPrefixes []netip.Prefix
// RebindingProtectionExemptHostnames is a list of hostnames
// exempt from DNS rebinding protection. It can contain parent
// domains which are of the form "*.example.com". Note the wildcard
// can only be used at the start of the hostname.
RebindingProtectionExemptHostnames []string
}
func (b *DNSBlacklist) setDefaults() {
@@ -36,25 +32,21 @@ func (b *DNSBlacklist) setDefaults() {
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
var (
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
)
func (b DNSBlacklist) validate() (err error) {
for _, host := range b.AllowedHosts {
if !hostRegex.MatchString(host) {
return fmt.Errorf("allowed host is not valid: %s", host)
return fmt.Errorf("%w: %s", ErrAllowedHostNotValid, host)
}
}
for _, host := range b.AddBlockedHosts {
if !hostRegex.MatchString(host) {
return fmt.Errorf("blocked host is not valid: %s", host)
}
}
for _, host := range b.RebindingProtectionExemptHostnames {
if len(host) > 2 && host[:2] == "*." {
host = host[2:]
}
if !hostRegex.MatchString(host) {
return fmt.Errorf("rebinding protection exempt host is not valid: %s", host)
return fmt.Errorf("%w: %s", ErrBlockedHostNotValid, host)
}
}
@@ -63,14 +55,13 @@ func (b DNSBlacklist) validate() (err error) {
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
return DNSBlacklist{
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
BlockAds: gosettings.CopyPointer(b.BlockAds),
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
BlockAds: gosettings.CopyPointer(b.BlockAds),
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
}
}
@@ -82,8 +73,6 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
other.RebindingProtectionExemptHostnames)
}
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
@@ -140,13 +129,6 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
}
}
if len(b.RebindingProtectionExemptHostnames) > 0 {
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
for _, host := range b.RebindingProtectionExemptHostnames {
exemptHostsNode.Append(host)
}
}
return node
}
@@ -167,45 +149,23 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
return err
}
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
if err != nil {
return err
}
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
return nil
}
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
if err != nil {
return nil, nil, err
}
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
if err != nil {
return nil, nil, err
}
// TODO v4 remove this block below
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
if err != nil {
return nil, nil, err
}
ips = append(ips, privateIPs...)
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
return ips, ipPrefixes, nil
}
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
if len(privateAddresses) == 0 {
return nil, nil, nil
}
@@ -227,9 +187,8 @@ func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
}
return nil, nil, fmt.Errorf(
"environment variable DOT_PRIVATE_ADDRESS: "+
"private address is not a valid IP or CIDR range: %s",
privateAddress)
"environment variable DOT_PRIVATE_ADDRESS: %w: %s",
ErrPrivateAddressNotValid, privateAddress)
}
return ips, ipPrefixes, nil
+170
View File
@@ -0,0 +1,170 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// DoT contains settings to configure the DoT server.
type DoT struct {
// Enabled is true if the DoT server should be running
// and used. It defaults to true, and cannot be nil
// in the internal state.
Enabled *bool
// UpdatePeriod is the period to update DNS block lists.
// It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in
// the internal state.
UpdatePeriod *time.Duration
// Providers is a list of DNS over TLS providers
Providers []string `json:"providers"`
// Caching is true if the DoT server should cache
// DNS responses.
Caching *bool `json:"caching"`
// IPv6 is true if the DoT server should connect over IPv6.
IPv6 *bool `json:"ipv6"`
// Blacklist contains settings to configure the filter
// block lists.
Blacklist DNSBlacklist
}
var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
func (d DoT) validate() (err error) {
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("%w: %s must be bigger than %s",
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
}
providers := provider.NewProviders()
for _, providerName := range d.Providers {
_, err := providers.Get(providerName)
if err != nil {
return err
}
}
err = d.Blacklist.validate()
if err != nil {
return err
}
return nil
}
func (d *DoT) copy() (copied DoT) {
return DoT{
Enabled: gosettings.CopyPointer(d.Enabled),
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
}
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DoT) overrideWith(other DoT) {
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist)
}
func (d *DoT) setDefaults() {
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
provider.Cloudflare().Name,
})
d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults()
}
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
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.DoT.IPv4[0].Addr()
}
func (d DoT) String() string {
return d.toLinesNode().String()
}
func (d DoT) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS over TLS settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
if !*d.Enabled {
return node
}
update := "disabled" //nolint:goconst
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
upstreamResolvers := node.Append("Upstream resolvers:")
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
node.AppendNode(d.Blacklist.toLinesNode())
return node
}
func (d *DoT) read(reader *reader.Reader) (err error) {
d.Enabled, err = reader.BoolPtr("DOT")
if err != nil {
return err
}
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return err
}
d.Providers = reader.CSV("DOT_PROVIDERS")
d.Caching, err = reader.BoolPtr("DOT_CACHING")
if err != nil {
return err
}
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
if err != nil {
return err
}
err = d.Blacklist.read(reader)
if err != nil {
return err
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package settings
import "errors"
var (
ErrValueUnknown = errors.New("value is unknown")
ErrCityNotValid = errors.New("the city specified is not valid")
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
ErrCategoryNotValid = errors.New("the category specified is not valid")
ErrCountryNotValid = errors.New("the country specified is not valid")
ErrFilepathMissing = errors.New("filepath is missing")
ErrFirewallZeroPort = errors.New("cannot have a zero port")
ErrFirewallPublicOutboundSubnet = errors.New("outbound subnet has an unspecified address")
ErrHostnameNotValid = errors.New("the hostname specified is not valid")
ErrISPNotValid = errors.New("the ISP specified is not valid")
ErrMinRatioNotValid = errors.New("minimum ratio is not valid")
ErrMissingValue = errors.New("missing value")
ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
ErrOpenVPNKeyPassphraseIsEmpty = errors.New("key passphrase is empty")
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
ErrOpenVPNUserIsEmpty = errors.New("user is empty")
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
ErrRegionNotValid = errors.New("the region specified is not valid")
ErrServerAddressNotValid = errors.New("server listening address is not valid")
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
ErrWireguardEndpointPortSet = errors.New("endpoint port is set")
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
ErrWireguardInterfaceAddressIPv6 = errors.New("interface address is IPv6 but IPv6 is not supported")
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
ErrWireguardKeepAliveNegative = errors.New("persistent keep alive interval is negative")
ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
)
+13 -17
View File
@@ -1,7 +1,6 @@
package settings
import (
"errors"
"fmt"
"net/netip"
@@ -16,29 +15,24 @@ type Firewall struct {
InputPorts []uint16
OutboundSubnets []netip.Prefix
Enabled *bool
Iptables Iptables
Debug *bool
}
func (f Firewall) validate() (err error) {
if hasZeroPort(f.VPNInputPorts) {
return errors.New("VPN input ports: cannot have a zero port")
return fmt.Errorf("VPN input ports: %w", ErrFirewallZeroPort)
}
if hasZeroPort(f.InputPorts) {
return errors.New("input ports: cannot have a zero port")
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
}
for _, subnet := range f.OutboundSubnets {
if subnet.Addr().IsUnspecified() {
return fmt.Errorf("outbound subnet has an unspecified address: %s", subnet)
return fmt.Errorf("%w: %s", ErrFirewallPublicOutboundSubnet, subnet)
}
}
err = f.Iptables.validate()
if err != nil {
return fmt.Errorf("iptables settings: %w", err)
}
return nil
}
@@ -57,7 +51,7 @@ func (f *Firewall) copy() (copied Firewall) {
InputPorts: gosettings.CopySlice(f.InputPorts),
OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets),
Enabled: gosettings.CopyPointer(f.Enabled),
Iptables: f.Iptables.copy(),
Debug: gosettings.CopyPointer(f.Debug),
}
}
@@ -69,12 +63,12 @@ func (f *Firewall) overrideWith(other Firewall) {
f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets)
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.Iptables.setDefaults(globalLogLevel)
f.Debug = gosettings.DefaultPointer(f.Debug, false)
}
func (f Firewall) String() string {
@@ -89,7 +83,9 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
return node
}
node.AppendNode(f.Iptables.toLinesNode())
if *f.Debug {
node.Appendf("Debug mode: on")
}
if len(f.VPNInputPorts) > 0 {
vpnInputPortsNode := node.Appendf("VPN input ports:")
@@ -137,9 +133,9 @@ func (f *Firewall) read(r *reader.Reader) (err error) {
return err
}
err = f.Iptables.read(r)
f.Debug, err = r.BoolPtr("FIREWALL_DEBUG")
if err != nil {
return fmt.Errorf("reading iptables settings: %w", err)
return err
}
return nil
@@ -4,7 +4,6 @@ import (
"net/netip"
"testing"
"github.com/qdm12/log"
"github.com/stretchr/testify/assert"
)
@@ -13,21 +12,22 @@ func Test_Firewall_validate(t *testing.T) {
testCases := map[string]struct {
firewall Firewall
errWrapped error
errMessage string
}{
"empty": {
errMessage: "iptables settings: log level: level is not recognized: ",
},
"empty": {},
"zero_vpn_input_port": {
firewall: Firewall{
VPNInputPorts: []uint16{0},
},
errWrapped: ErrFirewallZeroPort,
errMessage: "VPN input ports: cannot have a zero port",
},
"zero_input_port": {
firewall: Firewall{
InputPorts: []uint16{0},
},
errWrapped: ErrFirewallZeroPort,
errMessage: "input ports: cannot have a zero port",
},
"unspecified_outbound_subnet": {
@@ -36,11 +36,11 @@ func Test_Firewall_validate(t *testing.T) {
netip.MustParsePrefix("0.0.0.0/0"),
},
},
errWrapped: ErrFirewallPublicOutboundSubnet,
errMessage: "outbound subnet has an unspecified address: 0.0.0.0/0",
},
"public_outbound_subnet": {
firewall: Firewall{
Iptables: Iptables{LogLevel: log.LevelInfo.String()},
OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("1.2.3.4/32"),
},
@@ -48,7 +48,6 @@ func Test_Firewall_validate(t *testing.T) {
},
"valid_settings": {
firewall: Firewall{
Iptables: Iptables{LogLevel: log.LevelInfo.String()},
VPNInputPorts: []uint16{100, 101},
InputPorts: []uint16{200, 201},
OutboundSubnets: []netip.Prefix{
@@ -65,10 +64,9 @@ func Test_Firewall_validate(t *testing.T) {
err := testCase.firewall.validate()
if testCase.errMessage != "" {
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
+52 -73
View File
@@ -1,10 +1,9 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"os"
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
@@ -18,24 +17,23 @@ type Health struct {
// for the health check server.
// It cannot be the empty string in the internal state.
ServerAddress string
// TargetAddresses are the addresses (host or host:port)
// to TCP TLS dial to periodically for the health check.
// Addresses after the first one are used as fallbacks for retries.
// It cannot be empty in the internal state.
TargetAddresses []string
// ICMPTargetIPs are the IP addresses to use for ICMP echo requests
// in the health checker. The slice can be set to a single
// unspecified address (0.0.0.0) such that the VPN server IP is used,
// although this can be less reliable. It defaults to [1.1.1.1,8.8.8.8],
// and cannot be left empty in the internal state.
ICMPTargetIPs []netip.Addr
// SmallCheckType is the type of small health check to perform.
// It can be "icmp" or "dns", and defaults to "icmp".
// Note it changes automatically to dns if icmp is not supported.
SmallCheckType string
// RestartVPN indicates whether to restart the VPN connection
// when the healthcheck fails.
RestartVPN *bool
// ReadHeaderTimeout is the HTTP server header read timeout
// duration of the HTTP server. It defaults to 100 milliseconds.
ReadHeaderTimeout time.Duration
// ReadTimeout is the HTTP read timeout duration of the
// HTTP server. It defaults to 500 milliseconds.
ReadTimeout time.Duration
// TargetAddress is the address (host or host:port)
// to TCP dial to periodically for the health check.
// It cannot be the empty string in the internal state.
TargetAddress string
// SuccessWait is the duration to wait to re-run the
// healthcheck after a successful healthcheck.
// It defaults to 5 seconds and cannot be zero in
// the internal state.
SuccessWait time.Duration
// VPN has health settings specific to the VPN loop.
VPN HealthyWait
}
func (h Health) Validate() (err error) {
@@ -44,19 +42,9 @@ func (h Health) Validate() (err error) {
return fmt.Errorf("server listening address is not valid: %w", err)
}
for _, ip := range h.ICMPTargetIPs {
switch {
case !ip.IsValid():
return fmt.Errorf("ICMP target IP address is not valid: %s", ip)
case ip.IsUnspecified() && len(h.ICMPTargetIPs) > 1:
return errors.New("ICMP target IP addresses are not compatible: " +
"only a single IP address must be set if it is to be unspecified")
}
}
err = validate.IsOneOf(h.SmallCheckType, "icmp", "dns")
err = h.VPN.validate()
if err != nil {
return fmt.Errorf("small check type is not valid: %w", err)
return fmt.Errorf("health VPN settings: %w", err)
}
return nil
@@ -64,11 +52,12 @@ func (h Health) Validate() (err error) {
func (h *Health) copy() (copied Health) {
return Health{
ServerAddress: h.ServerAddress,
TargetAddresses: h.TargetAddresses,
ICMPTargetIPs: gosettings.CopySlice(h.ICMPTargetIPs),
SmallCheckType: h.SmallCheckType,
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
ServerAddress: h.ServerAddress,
ReadHeaderTimeout: h.ReadHeaderTimeout,
ReadTimeout: h.ReadTimeout,
TargetAddress: h.TargetAddress,
SuccessWait: h.SuccessWait,
VPN: h.VPN.copy(),
}
}
@@ -77,21 +66,23 @@ func (h *Health) copy() (copied Health) {
// settings.
func (h *Health) OverrideWith(other Health) {
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
h.TargetAddresses = gosettings.OverrideWithSlice(h.TargetAddresses, other.TargetAddresses)
h.ICMPTargetIPs = gosettings.OverrideWithSlice(h.ICMPTargetIPs, other.ICMPTargetIPs)
h.SmallCheckType = gosettings.OverrideWithComparable(h.SmallCheckType, other.SmallCheckType)
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
h.VPN.overrideWith(other.VPN)
}
func (h *Health) SetDefaults() {
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
h.TargetAddresses = gosettings.DefaultSlice(h.TargetAddresses, []string{"cloudflare.com:443", "github.com:443"})
h.ICMPTargetIPs = gosettings.DefaultSlice(h.ICMPTargetIPs, []netip.Addr{
netip.AddrFrom4([4]byte{1, 1, 1, 1}),
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
})
h.SmallCheckType = gosettings.DefaultComparable(h.SmallCheckType, "icmp")
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
const defaultReadHeaderTimeout = 100 * time.Millisecond
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 500 * time.Millisecond
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
const defaultSuccessWait = 5 * time.Second
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
h.VPN.setDefaults()
}
func (h Health) String() string {
@@ -101,40 +92,28 @@ func (h Health) String() string {
func (h Health) toLinesNode() (node *gotree.Node) {
node = gotree.New("Health settings:")
node.Appendf("Server listening address: %s", h.ServerAddress)
targetAddrs := node.Appendf("Target addresses:")
for _, targetAddr := range h.TargetAddresses {
targetAddrs.Append(targetAddr)
}
switch h.SmallCheckType {
case "icmp":
icmpNode := node.Appendf("Small health check type: ICMP echo request")
if len(h.ICMPTargetIPs) == 1 && h.ICMPTargetIPs[0].IsUnspecified() {
icmpNode.Appendf("ICMP target IP: VPN server IP address")
} else {
icmpIPs := icmpNode.Appendf("ICMP target IPs:")
for _, ip := range h.ICMPTargetIPs {
icmpIPs.Append(ip.String())
}
}
case "dns":
node.Appendf("Small health check type: Plain DNS lookup over UDP")
}
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
node.Appendf("Target address: %s", h.TargetAddress)
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
node.Appendf("Read timeout: %s", h.ReadTimeout)
node.AppendNode(h.VPN.toLinesNode("VPN"))
return node
}
func (h *Health) Read(r *reader.Reader) (err error) {
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
h.TargetAddresses = r.CSV("HEALTH_TARGET_ADDRESSES",
reader.RetroKeys("HEALTH_ADDRESS_TO_PING", "HEALTH_TARGET_ADDRESS"))
h.ICMPTargetIPs, err = r.CSVNetipAddresses("HEALTH_ICMP_TARGET_IPS", reader.RetroKeys("HEALTH_ICMP_TARGET_IP"))
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
if err != nil {
return err
}
h.SmallCheckType = r.String("HEALTH_SMALL_CHECK_TYPE")
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
err = h.VPN.read(r)
if err != nil {
return err
return fmt.Errorf("VPN health settings: %w", err)
}
return nil
}
@@ -0,0 +1,76 @@
package settings
import (
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type HealthyWait struct {
// Initial is the initial duration to wait for the program
// to be healthy before taking action.
// It cannot be nil in the internal state.
Initial *time.Duration
// Addition is the duration to add to the Initial duration
// after Initial has expired to wait longer for the program
// to be healthy.
// It cannot be nil in the internal state.
Addition *time.Duration
}
func (h HealthyWait) validate() (err error) {
return nil
}
func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{
Initial: gosettings.CopyPointer(h.Initial),
Addition: gosettings.CopyPointer(h.Addition),
}
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *HealthyWait) overrideWith(other HealthyWait) {
h.Initial = gosettings.OverrideWithPointer(h.Initial, other.Initial)
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition)
}
func (h *HealthyWait) setDefaults() {
const initialDurationDefault = 6 * time.Second
const additionDurationDefault = 5 * time.Second
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault)
h.Addition = gosettings.DefaultPointer(h.Addition, additionDurationDefault)
}
func (h HealthyWait) String() string {
return h.toLinesNode("Health").String()
}
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
node = gotree.New(kind + " wait durations:")
node.Appendf("Initial duration: %s", *h.Initial)
node.Appendf("Additional duration: %s", *h.Addition)
return node
}
func (h *HealthyWait) read(r *reader.Reader) (err error) {
h.Initial, err = r.DurationPtr(
"HEALTH_VPN_DURATION_INITIAL",
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
if err != nil {
return err
}
h.Addition, err = r.DurationPtr(
"HEALTH_VPN_DURATION_ADDITION",
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
if err != nil {
return err
}
return nil
}
+3 -2
View File
@@ -48,7 +48,7 @@ func (h HTTPProxy) validate() (err error) {
// Do not validate user and password
err = validate.ListeningAddress(h.ListeningAddress, os.Getuid())
if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err)
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
}
return nil
@@ -176,6 +176,7 @@ func readHTTProxyLog(r *reader.Reader) (enabled *bool, err error) {
case "disabled", "no", "off":
return ptrTo(false), nil
default:
return nil, fmt.Errorf("HTTP retro-compatible proxy log setting: value is unknown: %s", value)
return nil, fmt.Errorf("HTTP retro-compatible proxy log setting: %w: %s",
ErrValueUnknown, value)
}
}
@@ -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
}
+16 -13
View File
@@ -2,7 +2,6 @@ package settings
import (
"encoding/base64"
"errors"
"fmt"
"regexp"
"strings"
@@ -93,7 +92,7 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version
validVersions := []string{openvpn.Openvpn25, openvpn.Openvpn26}
if err = validate.IsOneOf(o.Version, validVersions...); err != nil {
return fmt.Errorf("version is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrOpenVPNVersionIsNotValid, err)
}
isCustom := vpnProvider == providers.Custom
@@ -102,14 +101,14 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
vpnProvider != providers.VPNSecure
if isUserRequired && *o.User == "" {
return errors.New("user is empty")
return fmt.Errorf("%w", ErrOpenVPNUserIsEmpty)
}
passwordRequired := isUserRequired &&
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(*o.User))
if passwordRequired && *o.Password == "" {
return errors.New("password is empty")
return fmt.Errorf("%w", ErrOpenVPNPasswordIsEmpty)
}
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
@@ -133,20 +132,23 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
}
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
return errors.New("key passphrase is empty")
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
}
const maxMSSFix = 10000
if *o.MSSFix > maxMSSFix {
return fmt.Errorf("mssfix option value is too high: %d is over the maximum value of %d", *o.MSSFix, maxMSSFix)
return fmt.Errorf("%w: %d is over the maximum value of %d",
ErrOpenVPNMSSFixIsTooHigh, *o.MSSFix, maxMSSFix)
}
if !regexpInterfaceName.MatchString(o.Interface) {
return fmt.Errorf("interface name is not valid: '%s' does not match regex '%s'", o.Interface, regexpInterfaceName)
return fmt.Errorf("%w: '%s' does not match regex '%s'",
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
}
if *o.Verbosity < 0 || *o.Verbosity > 6 {
return fmt.Errorf("verbosity value is out of bounds: %d can only be between 0 and 5", o.Verbosity)
return fmt.Errorf("%w: %d can only be between 0 and 5",
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
}
return nil
@@ -160,7 +162,7 @@ func validateOpenVPNConfigFilepath(isCustom bool,
}
if confFile == "" {
return errors.New("filepath is missing")
return fmt.Errorf("%w", ErrFilepathMissing)
}
err = validate.FileExists(confFile)
@@ -187,7 +189,7 @@ func validateOpenVPNClientCertificate(vpnProvider,
providers.VPNSecure,
providers.VPNUnlimited:
if clientCert == "" {
return errors.New("missing value")
return fmt.Errorf("%w", ErrMissingValue)
}
}
@@ -207,9 +209,10 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
case
providers.Airvpn,
providers.Cyberghost,
providers.VPNUnlimited:
providers.VPNUnlimited,
providers.Wevpn:
if clientKey == "" {
return errors.New("missing value")
return fmt.Errorf("%w", ErrMissingValue)
}
}
@@ -228,7 +231,7 @@ func validateOpenVPNEncryptedKey(vpnProvider,
encryptedPrivateKey string,
) (err error) {
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
return errors.New("missing value")
return fmt.Errorf("%w", ErrMissingValue)
}
if encryptedPrivateKey == "" {
@@ -2,7 +2,6 @@ package settings
import (
"fmt"
"net/netip"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
@@ -25,12 +24,6 @@ type OpenVPNSelection struct {
// and can be udp or tcp. It cannot be the empty string
// in the internal state.
Protocol string `json:"protocol"`
// EndpointIP is the server endpoint IP address.
// If set, it overrides any IP address from the picked
// built-in server connection. To indicate it should
// not be used, it should be set to [netip.IPv4Unspecified].
// It can never be the zero value in the internal state.
EndpointIP netip.Addr `json:"endpoint_ip"`
// CustomPort is the OpenVPN server endpoint port.
// It can be set to 0 to indicate no custom port should
// be used. It cannot be nil in the internal state.
@@ -60,9 +53,11 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
providers.Giganews,
providers.Ipvanish,
providers.Perfectprivacy,
providers.Privado,
providers.Vyprvpn,
) {
return fmt.Errorf("TCP protocol is not supported: for VPN service provider %s", vpnProvider)
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNTCPNotSupported, vpnProvider)
}
// Validate CustomPort
@@ -73,11 +68,12 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
providers.Privatevpn, providers.Torguard:
// no custom port allowed
case providers.Expressvpn, providers.Fastestvpn,
providers.Giganews, providers.Ipvanish,
providers.Nordvpn, providers.Purevpn,
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
providers.Privado, providers.Purevpn,
providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("custom endpoint port is not allowed: for VPN service provider %s", vpnProvider)
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default:
var allowedTCP, allowedUDP []uint16
switch vpnProvider {
@@ -96,18 +92,18 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
case providers.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433}
case providers.Privado:
allowedTCP = []uint16{443, 1194, 8080, 8443}
allowedUDP = []uint16{443, 1194, 8080, 8443}
case providers.PrivateInternetAccess:
allowedTCP = []uint16{80, 110, 443, 501, 502, 8443}
allowedTCP = []uint16{80, 110, 443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060, 51820}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
case providers.SlickVPN:
allowedTCP = []uint16{443, 8080, 8888}
allowedUDP = []uint16{443, 8080, 8888}
case providers.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198}
case providers.Windscribe:
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
@@ -121,7 +117,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
}
err = validate.IsOneOf(*o.CustomPort, allowedPorts...)
if err != nil {
return fmt.Errorf("custom endpoint port is not allowed: for VPN service provider %s: %w", vpnProvider, err)
return fmt.Errorf("%w: for VPN service provider %s: %w",
ErrOpenVPNCustomPortNotAllowed, vpnProvider, err)
}
}
}
@@ -129,11 +126,12 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{
presets.None,
presets.Normal,
presets.Strong,
}
if err = validate.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...); err != nil {
return fmt.Errorf("PIA encryption preset is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrOpenVPNEncryptionPresetNotValid, err)
}
}
@@ -144,7 +142,6 @@ func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
return OpenVPNSelection{
ConfFile: gosettings.CopyPointer(o.ConfFile),
Protocol: o.Protocol,
EndpointIP: o.EndpointIP,
CustomPort: gosettings.CopyPointer(o.CustomPort),
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
}
@@ -154,14 +151,12 @@ func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
o.EndpointIP = gosettings.OverrideWithValidator(o.EndpointIP, other.EndpointIP)
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
o.EndpointIP = gosettings.DefaultValidator(o.EndpointIP, netip.IPv4Unspecified())
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
var defaultEncPreset string
@@ -179,10 +174,6 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN server selection settings:")
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
if !o.EndpointIP.IsUnspecified() {
node.Appendf("Endpoint IP address: %s", o.EndpointIP)
}
if *o.CustomPort != 0 {
node.Appendf("Custom port: %d", *o.CustomPort)
}
@@ -203,12 +194,6 @@ func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
o.EndpointIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
if err != nil {
return err
}
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
if err != nil {
-105
View File
@@ -1,105 +0,0 @@
package settings
import (
"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"`
}
// Validate validates PMTUD settings.
func (p PMTUD) validate() (err error) {
for i, addr := range p.ICMPAddresses {
if !addr.IsValid() {
return fmt.Errorf("PMTUD ICMP address is not valid: at index %d", i)
}
}
for i, addr := range p.TCPAddresses {
if !addr.IsValid() {
return fmt.Errorf("PMTUD TCP address is not valid: at index %d", 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
}
+21 -90
View File
@@ -1,10 +1,8 @@
package settings
import (
"errors"
"fmt"
"path/filepath"
"slices"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings"
@@ -31,24 +29,10 @@ type PortForwarding struct {
// to write to a file. It cannot be nil for the
// internal state
Filepath *string `json:"status_file_path"`
// UpCommand is the command to use when the port forwarding 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 port forwarding 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"`
// ListeningPorts are the ports traffic would be redirected to from the
// forwarded ports. The redirection is disabled if it is the slice [0],
// which is its default as well. If set and not [0], its length must match
// the PortsCount value, such that each forwarded port is redirected to
// the corresponding listening port.
ListeningPorts []uint16 `json:"listening_port"`
// PortsCount is the number of ports to forward. It is optional for ProtonVPN
// and be between 1 and 5. For other providers, it must be set to 1 if port
// forwarding is enabled.
PortsCount uint16 `json:"ports_count"`
// ListeningPort is the port traffic would be redirected to from the
// forwarded port. The redirection is disabled if it is set to 0, which
// is its default as well.
ListeningPort *uint16 `json:"listening_port"`
// Username is only used for Private Internet Access port forwarding.
Username string `json:"username"`
// Password is only used for Private Internet Access port forwarding.
@@ -72,7 +56,7 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
providers.Protonvpn,
}
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
return fmt.Errorf("port forwarding cannot be enabled: %w", err)
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
}
// Validate Filepath
@@ -83,36 +67,12 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
}
}
switch providerSelected {
case providers.PrivateInternetAccess:
const maxPortsCount = 1
if providerSelected == providers.PrivateInternetAccess {
switch {
case p.PortsCount > maxPortsCount:
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
case p.Username == "":
return errors.New("port forwarding username is empty")
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
case p.Password == "":
return errors.New("port forwarding password is empty")
}
case providers.Protonvpn:
const maxPortsCount = 5
if p.PortsCount > maxPortsCount {
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
}
default:
const maxPortsCount = 1
if p.PortsCount > maxPortsCount {
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
}
}
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
switch {
case len(p.ListeningPorts) != int(p.PortsCount):
return fmt.Errorf("listening ports length must be equal to ports count: "+
"%d != %d", len(p.ListeningPorts), p.PortsCount)
case slices.Contains(p.ListeningPorts, 0):
return fmt.Errorf("listening port cannot be 0: in %v", p.ListeningPorts)
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
}
}
@@ -121,14 +81,12 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
func (p *PortForwarding) Copy() (copied PortForwarding) {
return PortForwarding{
Enabled: gosettings.CopyPointer(p.Enabled),
Provider: gosettings.CopyPointer(p.Provider),
Filepath: gosettings.CopyPointer(p.Filepath),
UpCommand: gosettings.CopyPointer(p.UpCommand),
DownCommand: gosettings.CopyPointer(p.DownCommand),
ListeningPorts: gosettings.CopySlice(p.ListeningPorts),
Username: p.Username,
Password: p.Password,
Enabled: gosettings.CopyPointer(p.Enabled),
Provider: gosettings.CopyPointer(p.Provider),
Filepath: gosettings.CopyPointer(p.Filepath),
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
Username: p.Username,
Password: p.Password,
}
}
@@ -136,9 +94,7 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
p.ListeningPorts = gosettings.OverrideWithSlice(p.ListeningPorts, other.ListeningPorts)
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
}
@@ -147,10 +103,7 @@ func (p *PortForwarding) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
p.Provider = gosettings.DefaultPointer(p.Provider, "")
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
p.ListeningPorts = gosettings.DefaultSlice(p.ListeningPorts, []uint16{0}) // disabled
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, 1)
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
}
func (p PortForwarding) String() string {
@@ -164,14 +117,11 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
node = gotree.New("Automatic port forwarding settings:")
node.Appendf("Number of ports to be forwarded: %d", p.PortsCount)
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
redirNode := node.Appendf("Redirection for listening ports:")
for i, port := range p.ListeningPorts {
redirNode.Appendf("Port #%d -> %d", i+1, port)
}
listeningPort := "disabled"
if *p.ListeningPort != 0 {
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
}
node.Appendf("Redirection listening port: %s", listeningPort)
if *p.Provider == "" {
node.Appendf("Use port forwarding code for current provider")
@@ -185,13 +135,6 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
}
node.Appendf("Forwarded port file path: %s", filepath)
if *p.UpCommand != "" {
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
}
if *p.DownCommand != "" {
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
}
if p.Username != "" {
credentialsNode := node.Appendf("Credentials:")
credentialsNode.Appendf("Username: %s", p.Username)
@@ -220,19 +163,7 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
))
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
reader.ForceLowercase(false))
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
reader.ForceLowercase(false))
p.ListeningPorts, err = r.CSVUint16("VPN_PORT_FORWARDING_LISTENING_PORTS",
reader.RetroKeys("VPN_PORT_FORWARDING_LISTENING_PORT"))
if err != nil {
return err
}
p.PortsCount, err = r.Uint16("VPN_PORT_FORWARDING_PORTS_COUNT")
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
if err != nil {
return err
}
+3 -13
View File
@@ -2,8 +2,6 @@ package settings
import (
"fmt"
"slices"
"sort"
"strings"
"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) {
// Validate Name
var validNames []string
switch vpnType {
case vpn.AmneziaWg:
validNames = []string{providers.Custom}
case vpn.OpenVPN:
if vpnType == vpn.OpenVPN {
validNames = providers.AllWithCustom()
validNames = append(validNames, "pia") // Retro-compatibility
// Remove Mullvad since it no longer supports OpenVPN as of January 15th, 2026
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:
} else { // Wireguard
validNames = []string{
providers.Airvpn,
providers.Custom,
@@ -55,7 +45,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
}
}
if err = validate.IsOneOf(p.Name, validNames...); err != nil {
return fmt.Errorf("VPN provider name is not valid for %s: %w", vpnType, err)
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
}
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
@@ -15,6 +15,7 @@ func Test_PublicIP_read(t *testing.T) {
makeReader func(ctrl *gomock.Controller) *reader.Reader
makeWarner func(ctrl *gomock.Controller) Warner
settings PublicIP
errWrapped error
errMessage string
}{
"nothing_read": {
@@ -151,10 +152,9 @@ func Test_PublicIP_read(t *testing.T) {
err := settings.read(reader, warner)
assert.Equal(t, testCase.settings, settings)
if testCase.errMessage != "" {
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
+5 -41
View File
@@ -1,14 +1,11 @@
package settings
import (
"bytes"
"encoding/json"
"fmt"
"net"
"os"
"strconv"
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
@@ -27,9 +24,6 @@ type ControlServer struct {
// It cannot be empty in the internal state and defaults to
// /gluetun/auth/config.toml.
AuthFilePath string
// AuthDefaultRole is a JSON encoded object defining the default role
// that applies to all routes without a previously user-defined role assigned to.
AuthDefaultRole string
}
func (c ControlServer) validate() (err error) {
@@ -46,22 +40,8 @@ func (c ControlServer) validate() (err error) {
uid := os.Getuid()
const maxPrivilegedPort = 1023
if uid != 0 && port != 0 && port <= maxPrivilegedPort {
return fmt.Errorf("cannot use privileged port without running as root: %d when running with user ID %d", port, uid)
}
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
jsonDecoder.DisallowUnknownFields()
var role auth.Role
err = jsonDecoder.Decode(&role)
if err != nil {
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
}
if role.Auth != "" {
err = role.Validate()
if err != nil {
return fmt.Errorf("default authentication role is not valid: %w", err)
}
return fmt.Errorf("%w: %d when running with user ID %d",
ErrControlServerPrivilegedPort, port, uid)
}
return nil
@@ -69,10 +49,9 @@ func (c ControlServer) validate() (err error) {
func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{
Address: gosettings.CopyPointer(c.Address),
Log: gosettings.CopyPointer(c.Log),
AuthFilePath: c.AuthFilePath,
AuthDefaultRole: c.AuthDefaultRole,
Address: gosettings.CopyPointer(c.Address),
Log: gosettings.CopyPointer(c.Log),
AuthFilePath: c.AuthFilePath,
}
}
@@ -83,21 +62,12 @@ func (c *ControlServer) overrideWith(other ControlServer) {
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
}
func (c *ControlServer) setDefaults() {
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
c.Log = gosettings.DefaultPointer(c.Log, true)
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
c.AuthDefaultRole = gosettings.DefaultComparable(c.AuthDefaultRole, "{}")
if c.AuthDefaultRole != "{}" {
var role auth.Role
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
role.Name = "default"
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
c.AuthDefaultRole = string(roleBytes)
}
}
func (c ControlServer) String() string {
@@ -109,11 +79,6 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
node.Appendf("Listening address: %s", *c.Address)
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
node.Appendf("Authentication file path: %s", c.AuthFilePath)
if c.AuthDefaultRole != "{}" {
var role auth.Role
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
node.AppendNode(role.ToLinesNode())
}
return node
}
@@ -126,7 +91,6 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
c.AuthDefaultRole = r.String("HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE", reader.ForceLowercase(false))
return nil
}
@@ -3,6 +3,7 @@ package settings
import (
"errors"
"fmt"
"net/netip"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
@@ -16,11 +17,17 @@ import (
"github.com/qdm12/gotree"
)
type ServerSelection struct {
type ServerSelection struct { //nolint:maligned
// VPN is the VPN type which can be 'openvpn'
// or 'wireguard'. It cannot be the empty string
// in the internal state.
VPN string `json:"vpn"`
// TargetIP is the server endpoint IP address to use.
// It will override any IP address from the picked
// built-in server. It cannot be the empty value in the internal
// state, and can be set to the unspecified address to indicate
// there is not target IP address to use.
TargetIP netip.Addr `json:"target_ip"`
// Countries is the list of countries to filter VPN servers with.
Countries []string `json:"countries"`
// Categories is the list of categories to filter VPN servers with.
@@ -71,13 +78,25 @@ type ServerSelection struct {
Wireguard WireguardSelection `json:"wireguard"`
}
var (
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported")
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
ErrPortForwardOnlyNotSupported = errors.New("port forwarding only filter is not supported")
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set")
ErrSecureCoreOnlyNotSupported = errors.New("secure core only filter is not supported")
ErrTorOnlyNotSupported = errors.New("tor only filter is not supported")
)
func (ss *ServerSelection) validate(vpnServiceProvider string,
filterChoicesGetter FilterChoicesGetter, warner Warner,
) (err error) {
switch ss.VPN {
case vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard:
case vpn.OpenVPN, vpn.Wireguard:
default:
return fmt.Errorf("VPN type is not valid: %s", ss.VPN)
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
}
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
@@ -138,7 +157,7 @@ func getLocationFilterChoices(vpnServiceProvider string,
// Only return error comparing with newer regions, we don't want to confuse the user
// with the retro regions in the error message.
err = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner)
return models.FilterChoices{}, fmt.Errorf("the region specified is not valid: %w", err)
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
}
}
@@ -152,27 +171,27 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
) (err error) {
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
if err != nil {
return fmt.Errorf("the country specified is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
if err != nil {
return fmt.Errorf("the region specified is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
if err != nil {
return fmt.Errorf("the city specified is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
if err != nil {
return fmt.Errorf("the ISP specified is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
if err != nil {
return fmt.Errorf("the hostname specified is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
}
if vpnServiceProvider == providers.Custom {
@@ -184,19 +203,19 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
// which requires a server name for TLS verification.
filterChoices.Names = settings.Names
default:
return fmt.Errorf("name is not valid: "+
"%d names specified instead of 0 or 1 for the custom provider",
len(settings.Names))
return fmt.Errorf("%w: %d names specified instead of "+
"0 or 1 for the custom provider",
ErrNameNotValid, len(settings.Names))
}
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner)
if err != nil {
return fmt.Errorf("the server name specified is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
if err != nil {
return fmt.Errorf("the category specified is not valid: %w", err)
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
}
return nil
@@ -243,12 +262,12 @@ func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvide
switch {
case *settings.FreeOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
return errors.New("free only filter is not supported")
return fmt.Errorf("%w", ErrFreeOnlyNotSupported)
case *settings.PremiumOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.VPNSecure):
return errors.New("premium only filter is not supported")
return fmt.Errorf("%w", ErrPremiumOnlyNotSupported)
case *settings.FreeOnly && *settings.PremiumOnly:
return errors.New("free only and premium only filters are both set")
return fmt.Errorf("%w", ErrFreePremiumBothSet)
default:
return nil
}
@@ -257,21 +276,21 @@ func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvide
func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) error {
switch {
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
return errors.New("owned only filter is not supported")
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
return errors.New("port forwarding only filter is not supported: together with free only filter")
return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
case *settings.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
return errors.New("stream only filter is not supported")
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
case *settings.MultiHopOnly && vpnServiceProvider != providers.Surfshark:
return errors.New("multi hop only filter is not supported")
return fmt.Errorf("%w", ErrMultiHopOnlyNotSupported)
case *settings.PortForwardOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.PrivateInternetAccess, providers.Protonvpn):
return errors.New("port forwarding only filter is not supported")
return fmt.Errorf("%w", ErrPortForwardOnlyNotSupported)
case *settings.SecureCoreOnly && vpnServiceProvider != providers.Protonvpn:
return errors.New("secure core only filter is not supported")
return fmt.Errorf("%w", ErrSecureCoreOnlyNotSupported)
case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn:
return errors.New("tor only filter is not supported")
return fmt.Errorf("%w", ErrTorOnlyNotSupported)
default:
return nil
}
@@ -280,6 +299,7 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
func (ss *ServerSelection) copy() (copied ServerSelection) {
return ServerSelection{
VPN: ss.VPN,
TargetIP: ss.TargetIP,
Countries: gosettings.CopySlice(ss.Countries),
Categories: gosettings.CopySlice(ss.Categories),
Regions: gosettings.CopySlice(ss.Regions),
@@ -303,6 +323,7 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
@@ -325,6 +346,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
@@ -332,8 +354,11 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
defaultPortForwardOnly := portForwardingEnabled &&
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
defaultPortForwardOnly := false
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
providers.PrivateInternetAccess, providers.Protonvpn) {
defaultPortForwardOnly = true
}
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults()
@@ -346,6 +371,9 @@ func (ss ServerSelection) String() string {
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server selection settings:")
node.Appendf("VPN type: %s", ss.VPN)
if !ss.TargetIP.IsUnspecified() {
node.Appendf("Target IP address: %s", ss.TargetIP)
}
if len(ss.Countries) > 0 {
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
@@ -436,6 +464,12 @@ func (ss *ServerSelection) read(r *reader.Reader,
) (err error) {
ss.VPN = vpnType
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
if err != nil {
return err
}
countriesRetroKeys := []string{"COUNTRY"}
if vpnProvider == providers.Cyberghost {
countriesRetroKeys = append(countriesRetroKeys, "REGION")
@@ -506,8 +540,7 @@ func (ss *ServerSelection) read(r *reader.Reader,
return err
}
amneziawg := ss.VPN == vpn.AmneziaWg
err = ss.Wireguard.read(r, amneziawg)
err = ss.Wireguard.read(r)
if err != nil {
return err
}

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