Compare commits

..

67 Commits

Author SHA1 Message Date
dependabot[bot]
f4dafaecac Chore(deps): Bump peter-evans/dockerhub-description from 4 to 5
Bumps [peter-evans/dockerhub-description](https://github.com/peter-evans/dockerhub-description) from 4 to 5.
- [Release notes](https://github.com/peter-evans/dockerhub-description/releases)
- [Commits](https://github.com/peter-evans/dockerhub-description/compare/v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/dockerhub-description
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 20:04:12 +00:00
Quentin McGaw
b0b769d2c1 ci(markdown): fix config file path 2025-10-31 20:02:55 +00:00
Quentin McGaw
d3c7d3c7bc docs(readme): update Alpine version and image size 2025-10-30 16:15:44 +00:00
Quentin McGaw
65f49ea012 fix(wireguard): specify IP family for new route (#2629) 2025-10-30 17:14:45 +01:00
Quentin McGaw
5687555921 chore(container): bump Alpine from 3.20 to 3.22 2025-10-30 16:08:40 +00:00
Quentin McGaw
0fb75036a0 chore(build): bump Go from 1.24 to 1.25 2025-10-30 16:04:10 +00:00
dependabot[bot]
2b513dd43d Chore(deps): Bump github.com/vishvananda/netlink from 1.2.1 to 1.3.1 (#2932) 2025-10-30 17:02:32 +01:00
Quentin McGaw
687d9b4736 hotfix(tests): fix unit test for healthcheck 2025-10-30 16:01:25 +00:00
dependabot[bot]
c70c2ef932 Chore(deps): Bump golang.org/x/net from 0.34.0 to 0.46.0 (#2937) 2025-10-30 17:00:30 +01:00
dependabot[bot]
af3ada109b Chore(deps): Bump actions/setup-go from 5 to 6 (#2929) 2025-10-30 17:00:15 +01:00
Quentin McGaw
9d40564734 chore(deps): bump breml/rootcerts from v0.2.20 to v0.3.2 2025-10-30 15:59:20 +00:00
Quentin McGaw
3734815ada hotfix(health): debug log failed attempts and warn log all attempt errors if all failed
- Reduce "worrying" noise of icmp attempt failing
- Only log when an action (restart the VPN) is taken
2025-10-30 15:57:40 +00:00
Quentin McGaw
b9cc5c1fdc fix(port-forward): clear port file instead of removing it
- Prevent port forwarding loop crash when trying to delete a directly bind mounted file
- See https://github.com/qdm12/gluetun/issues/2942#issuecomment-3468510402
2025-10-30 15:45:01 +00:00
dependabot[bot]
c646ca5766 Chore(deps): Bump peter-evans/create-or-update-comment from 4 to 5 (#2931) 2025-10-30 03:45:31 +01:00
dependabot[bot]
1394be5143 Chore(deps): Bump golang.org/x/sys from 0.29.0 to 0.37.0 (#2939) 2025-10-30 03:45:16 +01:00
Quentin McGaw
93442526f8 chore(ci): run container and wait for it to connect (#2956)
- Added safety to prevent panics/errors when skipping CI checks (shame on me, sometimes)
- Opens new possibilities for end to end integration tests. PRs accepted!
2025-10-30 03:44:31 +01:00
dependabot[bot]
d85402050b Chore(deps): Bump github.com/ulikunitz/xz from 0.5.11 to 0.5.15 (#2955) 2025-10-30 01:57:18 +01:00
dependabot[bot]
b1c62cb525 Chore(deps): Bump golang.org/x/text from 0.21.0 to 0.30.0 (#2938) 2025-10-30 01:56:53 +01:00
dependabot[bot]
fae64a297a Chore(deps): Bump github/codeql-action from 3 to 4 (#2935) 2025-10-30 01:56:41 +01:00
Quentin McGaw
6e2682a9ce docs(readme): remove no longer valid LoC badge 2025-10-30 00:55:39 +00:00
Quentin McGaw
555049f09c feat(privado): update servers data 2025-10-29 12:30:48 +00:00
Quentin McGaw
712f7c3d35 chore(build): bump Go from 1.23 to 1.24 2025-10-29 02:34:22 +00:00
Quentin McGaw
7a51c211cd fix(publicip): respect PUBLICIP_ENABLED 2025-10-23 19:49:21 +00:00
Quentin McGaw
c48189c1c4 feat(health/icmp): log out return address on errors 2025-10-23 19:22:31 +00:00
Quentin McGaw
9803fa1cfd hotfix(health): info log on healthcheck passing after failure 2025-10-23 18:58:19 +00:00
Quentin McGaw
cf756f561a feat(health): info log when healthcheck passes after failure for the case of HEALTH_VPN_RESTART=off 2025-10-21 18:42:33 +00:00
Quentin McGaw
a4021fedc3 feat(health): HEALTH_RESTART_VPN option
- You should really leave it to `on` ⚠️
- Turn it to `off` if you have trust issues with the healthcheck. Don't then report issues if the connection is dead though.
2025-10-21 15:36:15 +00:00
Quentin McGaw
31a36a9250 hotfix(health): increase timeout values and periods
- run small check every 60s, from 15s
- small check (icmp/dns) initial timeout from 3s to 10s
- small check (icmp/dns) timeout increase from 1s to 10s
- full check initial timeout increased from 10s to 20s
- full check extra timeout increase from 3s to 10s
2025-10-19 23:27:02 +00:00
Quentin McGaw
36fe349b70 chore(ci): ignore .github/pull_request_template.md with markdown linter 2025-10-19 23:23:41 +00:00
shwoop
3ef1cfd97c docs(github): add pull request template (#2918) 2025-10-17 20:34:05 +02:00
Quentin McGaw
669feb45f1 hotfix(healthcheck): correct error string for DNS plain lookup fallback 2025-10-17 18:08:24 +00:00
Quentin McGaw
85890520ab feat(healthcheck): combination of ICMP and TCP+TLS checks (#2923)
- New option: `HEALTH_ICMP_TARGET_IP` defaults to `0.0.0.0` meaning use the VPN server public IP address.
- Options removed: `HEALTH_VPN_INITIAL_DURATION` and `HEALTH_VPN_ADDITIONAL_DURATION` - times and retries are handpicked and hardcoded.
- Less aggressive checks and less false positive detection
2025-10-17 01:45:50 +02:00
dependabot[bot]
340016521e Chore(deps): Bump github.com/breml/rootcerts from 0.2.19 to 0.2.20 (#2683) 2025-10-06 13:36:00 +02:00
Matthew Bennett
ef523df42c feat(expressvpn): update hardcoded servers data (#2888) 2025-10-06 13:33:36 +02:00
Quentin McGaw
5306e3bab1 feat(mullvad): update servers data 2025-10-03 15:25:12 +00:00
Vahin M
72a49afd2b docs(healthcheck): fix grammar issue in log (#2773) 2025-09-26 18:58:08 +02:00
Quentin McGaw
9b8edbb81e hotfix(vpnunlimited): fix formatting of certificates 2025-09-24 12:55:45 +00:00
Quentin McGaw
a1554feb3f chore(dev): add vscode git remote add task 2025-09-24 12:54:16 +00:00
Quentin McGaw
490410bf09 chore(dev): convert .vscode/launch.json to tasks.json 2025-09-24 12:54:16 +00:00
mutschler
8c113f5268 fix(vpnunlimited): update certificate values (#2835) 2025-09-11 21:15:20 +02:00
shwoop
075cbd5a0f chore(ci): bump github actions and use go.mod Go version (#2880)
- actions/checkout from v4 to v5
- actions/setup-go uses go-version from go.mod file
- DavidAnson/markdownlint-cli2-action from v19 to v20
2025-09-11 21:14:19 +02:00
Quentin McGaw
d82df2b431 hotfix(build): bump xcputranslate so it's available on ghcr.io
- v0.7.0 is a broken build
- v0.9.0 is the version available on ghcr.io
2025-08-16 20:34:07 +00:00
Quentin McGaw
a09f8214d9 hotfix(build): bump xcputranslate so it's available on ghcr.io 2025-08-16 20:29:40 +00:00
Quentin McGaw
396e9c003e chore(ci): pull container images at build time from ghcr.io when possible
- Reduce silly image pull rate limiting from docker hub registry
- still rely on docker hub registry to pull golang and alpine images since these are not on ghcr.io
2025-08-16 20:12:21 +00:00
Quentin McGaw
b0c4a28be6 chore(lint): upgrade linter to v2.4.0
- migrate configuration file
- fix existing code issues
- add exclusion rules
- update linter names
2025-08-16 20:10:19 +00:00
Quentin McGaw
85325e4a31 chore(dev): upgrade dev container to v0.21
See [release notes](https://github.com/qdm12/godevcontainer/releases/tag/v0.21.0)

Notably:
- Go upgraded from 1.23 to 1.25
- golangci-lint upgraded to v2.4.0
- Alpine upgraded from 3.20 to 3.22
- Disable package comment requirement by gopls' staticcheck
- Pull container image from ghcr.io
2025-08-16 20:10:14 +00:00
dependabot[bot]
9933dd3ec5 Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 18 to 19 (#2632) 2025-01-22 09:27:10 +01:00
dependabot[bot]
13532c8b4b Chore(deps): Bump golang.org/x/net from 0.31.0 to 0.34.0 (#2648) 2025-01-22 09:26:57 +01:00
Leroy
3926797295 docs(readme): remove docker-compose example version field (#2663) 2025-01-22 09:26:39 +01:00
K1
febd3f784f docs(readme): "swiss-knife-like" -> "swiss-army-knife-like" (#2652) 2025-01-22 09:25:46 +01:00
dependabot[bot]
61b053f0e1 Chore(deps): Bump golang.org/x/crypto from 0.29.0 to 0.31.0 (#2619) 2024-12-27 21:15:31 +01:00
Quentin McGaw
8dae352ccc fix(cli): fix openvpnconfig command panic due to missing SetDefaults call 2024-12-27 09:31:04 +00:00
Quentin McGaw
e890c50da6 feat(firewall): support icmp rules 2024-12-25 20:05:55 +00:00
Quentin McGaw
ddd9f4d021 chore(natpmp): fix determinism for test Test_Client_ExternalAddress 2024-12-14 21:04:07 +00:00
dependabot[bot]
7e58b4baee Chore(deps): Bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#2600) 2024-12-14 21:19:30 +01:00
dependabot[bot]
a21fbb9a4f Chore(deps): Bump github.com/breml/rootcerts from 0.2.18 to 0.2.19 (#2601) 2024-12-14 21:19:11 +01:00
Quentin McGaw
3b7d27c919 hotfix(ci): use --device /dev/net/tun for test container 2024-12-14 20:15:42 +00:00
dependabot[bot]
68ddbfc0fe Chore(deps): Bump golang.org/x/net from 0.30.0 to 0.31.0 (#2578) 2024-11-18 10:46:04 +01:00
dependabot[bot]
a2047cb800 Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 16 to 18 (#2588) 2024-11-18 10:45:49 +01:00
Quentin McGaw
fdd499146c fix(wireguard): point to Kubernetes wiki page when encountering IP rule add file exists error (#2526) 2024-11-15 18:47:06 +01:00
Quentin McGaw
37900341cf hotfix(firewall): fix unit test for previous PR 2024-11-15 17:46:10 +00:00
Jean-François Roy
36bb368cad fix(firewall): iptables list uses -n flag for testing iptables path (#2574)
Signed-off-by: Jean-Francois Roy <jf@devklog.net>
2024-11-15 16:47:08 +01:00
Quentin McGaw
f9bdb219d0 chore(deps): update gosettings to v0.4.4
- Better support for quote expressions especially for commands such as VPN_PORT_FORWARDING_UP_COMMAND
2024-11-12 09:11:48 +00:00
Quentin McGaw
0374c14e42 feat(portforwarding): VPN_PORT_FORWARDING_DOWN_COMMAND option 2024-11-10 10:18:29 +00:00
Alex Lavallee
a035a151bd feat(portforwarding): allow running script upon port forwarding success (#2399) 2024-11-10 09:49:02 +01:00
Quentin McGaw
e69966381d feat(fastestvpn): add aes-256-gcm to ciphers list 2024-11-09 15:44:05 +00:00
Quentin McGaw
94dfb2b1f2 fix(ipvanish): fix openvpn configuration
- update CA value
- add `comp-lzo` option
2024-11-09 15:43:51 +00:00
97 changed files with 16058 additions and 15231 deletions

View File

@@ -1,2 +1,2 @@
FROM qmcgaw/godevcontainer:v0.20-alpine
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
RUN apk add wireguard-tools htop openssl

View File

@@ -19,16 +19,16 @@ It works on Linux, Windows (WSL2) and OSX.
mkdir -p ~/.ssh
```
1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker.
1. **For OSX hosts**: 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:

View File

@@ -82,6 +82,9 @@
"gopls": {
"usePlaceholders": false,
"staticcheck": true,
"ui.diagnostic.analyses": {
"ST1000": false
},
"formatting.gofumpt": true,
},
"go.lintTool": "golangci-lint",

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
# 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 we do not accept manual changes to the servers.json file <!-- If this is your goal, please consult https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-using-the-command-line -->
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)

View File

@@ -37,7 +37,7 @@ jobs:
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: reviewdog/action-misspell@v1
with:
@@ -59,13 +59,40 @@ jobs:
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
docker run --rm --device /dev/net/tun \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Build final image
run: docker build -t final-image .
verify-private:
if: |
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]
runs-on: ubuntu-latest
environment: secrets
steps:
- uses: actions/checkout@v5
- 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
codeql:
runs-on: ubuntu-latest
permissions:
@@ -73,15 +100,15 @@ jobs:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "^1.23"
- uses: github/codeql-action/init@v3
go-version-file: go.mod
- uses: github/codeql-action/init@v4
with:
languages: go
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
- uses: github/codeql-action/autobuild@v4
- uses: github/codeql-action/analyze@v4
publish:
if: |
@@ -98,7 +125,7 @@ jobs:
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action

View File

@@ -9,7 +9,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v4
- uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}

View File

@@ -11,7 +11,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: crazy-max/ghaction-github-labeler@v5
with:
yaml-file: .github/labels.yml

View File

@@ -18,12 +18,12 @@ jobs:
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: DavidAnson/markdownlint-cli2-action@v16
- uses: DavidAnson/markdownlint-cli2-action@v20
with:
globs: "**.md"
config: .markdownlint.json
config: .markdownlint-cli2.jsonc
- uses: reviewdog/action-misspell@v1
with:
@@ -37,7 +37,7 @@ jobs:
use-quiet-mode: yes
config-file: .github/workflows/configs/mlc-config.json
- uses: peter-evans/dockerhub-description@v4
- uses: peter-evans/dockerhub-description@v5
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
with:
username: qmcgaw

View File

@@ -9,7 +9,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v4
- uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}

View File

@@ -1,27 +1,70 @@
linters-settings:
version: "2"
formatters:
enable:
- gci
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
linters:
settings:
misspell:
locale: US
goconst:
ignore-string-values:
# commonly used settings strings
- "^disabled$"
# Firewall and routing strings
- "^(ACCEPT|DROP)$"
- "^--delete$"
- "^all$"
- "^(tcp|udp)$"
# Server route strings
- "^/status$"
issues:
exclude-rules:
- path: _test\.go
linters:
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- containedctx
- dupl
- err113
- containedctx
- maintidx
- path: "internal\\/server\\/.+\\.go"
linters:
path: _test\.go
- linters:
- dupl
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters:
path: internal\/server\/.+\.go
- linters:
- ireturn
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
linters:
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:
- revive
path: internal\/provider\/(common|utils)\/.+\.go
text: "var-naming: avoid (bad|meaningless) package names"
- linters:
- err113
- mnd
path: ci\/.+\.go
linters:
paths:
- third_party$
- builtin$
- examples$
enable:
# - cyclop
# - errorlint
@@ -42,7 +85,6 @@ linters:
- exhaustive
- fatcontext
- forcetypeassert
- gci
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
@@ -51,9 +93,7 @@ linters:
- gocritic
- gocyclo
- godot
- gofumpt
- goheader
- goimports
- gomoddirectives
- goprintffuncname
- gosec
@@ -86,7 +126,6 @@ linters:
- rowserrcheck
- sqlclosecheck
- tagalign
- tenv
- thelper
- tparallel
- unconvert

9
.markdownlint-cli2.jsonc Normal file
View File

@@ -0,0 +1,9 @@
{
"config": {
"default": true,
"MD013": false,
},
"ignores": [
".github/pull_request_template.md"
]
}

View File

@@ -1,3 +0,0 @@
{
"MD013": false
}

35
.vscode/launch.json vendored
View File

@@ -1,35 +0,0 @@
{
"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"
},
]
}

51
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"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"
],
}
],
"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",
},
]
}

View File

@@ -1,14 +1,14 @@
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 ALPINE_VERSION=3.22
ARG GO_ALPINE_VERSION=3.22
ARG GO_VERSION=1.25
ARG XCPUTRANSLATE_VERSION=v0.9.0
ARG GOLANGCI_LINT_VERSION=v2.4.0
ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64
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} 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} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
@@ -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 --timeout=10m
RUN golangci-lint run
FROM --platform=${BUILDPLATFORM} base AS mocks
RUN git init && \
@@ -125,6 +125,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
VPN_PORT_FORWARDING_USERNAME= \
VPN_PORT_FORWARDING_PASSWORD= \
VPN_PORT_FORWARDING_UP_COMMAND= \
VPN_PORT_FORWARDING_DOWN_COMMAND= \
# # Cyberghost only:
OPENVPN_CERT= \
OPENVPN_KEY= \
@@ -162,9 +164,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
# Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
HEALTH_SUCCESS_WAIT_DURATION=5s \
HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \
HEALTH_ICMP_TARGET_IP=0.0.0.0 \
HEALTH_RESTART_VPN=on \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \

View File

@@ -1,6 +1,6 @@
# Gluetun VPN client
Lightweight swiss-knife-like VPN client to multiple VPN service providers
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
@@ -26,7 +26,6 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
[![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)
[![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)
@@ -56,7 +55,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
## Features
- Based on Alpine 3.20 for a small Docker image of 35.6MB
- Based on Alpine 3.22 for a small Docker image of 41.1MB
- 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
@@ -88,7 +87,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

33
ci/cmd/main.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/qdm12/gluetun/ci/internal"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("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)
default:
err = fmt.Errorf("unknown command: %s", os.Args[1])
}
stop()
if err != nil {
fmt.Println("❌", err)
os.Exit(1)
}
fmt.Println("✅ Test completed successfully.")
}

36
ci/go.mod Normal file
View File

@@ -0,0 +1,36 @@
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/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.0.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.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
)

97
ci/go.sum Normal file
View File

@@ -0,0 +1,97 @@
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/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/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/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.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=

193
ci/internal/mullvad.go Normal file
View File

@@ -0,0 +1,193 @@
package internal
import (
"bufio"
"context"
"fmt"
"io"
"os"
"regexp"
"strings"
"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 MullvadTest(ctx context.Context) error {
secrets, err := readSecrets(ctx)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
const timeout = 15 * time.Second
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: []string{
"VPN_SERVICE_PROVIDER=mullvad",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=USA",
"WIREGUARD_PRIVATE_KEY=" + secrets.mullvadWireguardPrivateKey,
"WIREGUARD_ADDRESSES=" + secrets.mullvadWireguardAddress,
},
}
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 waitForLogLine(ctx, client, containerID, beforeStartTime)
}
func ptrTo[T any](v T) *T { return &v }
type secrets struct {
mullvadWireguardPrivateKey string
mullvadWireguardAddress string
}
func readSecrets(ctx context.Context) (secrets, error) {
expectedSecrets := [...]string{
"Mullvad Wireguard private key",
"Mullvad Wireguard address",
}
scanner := bufio.NewScanner(os.Stdin)
lines := make([]string, 0, len(expectedSecrets))
for i := range expectedSecrets {
fmt.Println("🤫 reading", expectedSecrets[i], "from Stdin...")
if !scanner.Scan() {
break
}
lines = append(lines, strings.TrimSpace(scanner.Text()))
if ctx.Err() != nil {
return secrets{}, ctx.Err()
}
}
if err := scanner.Err(); err != nil {
return secrets{}, fmt.Errorf("reading secrets from stdin: %w", err)
}
if len(lines) < len(expectedSecrets) {
return secrets{}, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
len(expectedSecrets), len(lines))
}
for i, line := range lines {
if line == "" {
return secrets{}, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
}
}
return secrets{
mullvadWireguardPrivateKey: lines[0],
mullvadWireguardAddress: lines[1],
}, nil
}
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)
}
}
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
beforeStartTime time.Time,
) 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()
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)
if successRegexp.MatchString(line) {
fmt.Println("✅ Success line logged")
return nil
}
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
fmt.Println("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)
}
}

View File

@@ -380,7 +380,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, puid, pgid)
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
portForwardRunError, err := portForwardLooper.Start(ctx)
if err != nil {
return fmt.Errorf("starting port forwarding loop: %w", err)
@@ -414,6 +414,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return fmt.Errorf("starting public ip 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)
updaterLogger := logger.New(log.SetComponent("updater"))
unzipper := unzip.New(httpClient)
@@ -424,8 +431,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second))
@@ -476,12 +483,6 @@ 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)
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),

28
go.mod
View File

@@ -1,29 +1,29 @@
module github.com/qdm12/gluetun
go 1.23
go 1.25.0
require (
github.com/breml/rootcerts v0.2.18
github.com/breml/rootcerts v0.3.2
github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.17.11
github.com/klauspost/pgzip v1.2.6
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/gosettings v0.4.4
github.com/qdm12/goshutdown v0.3.0
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.9.0
github.com/ulikunitz/xz v0.5.11
github.com/vishvananda/netlink v1.2.1
github.com/stretchr/testify v1.10.0
github.com/ulikunitz/xz v0.5.15
github.com/vishvananda/netlink v1.3.1
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
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.org/x/net v0.46.0
golang.org/x/sys v0.37.0
golang.org/x/text v0.30.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.0
@@ -49,11 +49,11 @@ require (
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
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
github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/tools v0.37.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

52
go.sum
View File

@@ -1,7 +1,7 @@
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.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/breml/rootcerts v0.3.2 h1:gm11iClhK8wFn/GDdINoDaqPkiaGXyVvSwoXINZN+z4=
github.com/breml/rootcerts v0.3.2/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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -57,8 +57,8 @@ github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ1
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/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
@@ -73,36 +73,36 @@ 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/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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
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/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -112,20 +112,20 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
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.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/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
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=

View File

@@ -56,6 +56,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
if err != nil {
return err
}
allSettings.SetDefaults()
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
if err != nil {

150
internal/command/split.go Normal file
View File

@@ -0,0 +1,150 @@
package command
import (
"bytes"
"errors"
"fmt"
"strings"
"unicode/utf8"
)
var (
ErrCommandEmpty = errors.New("command is empty")
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
)
// 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, fmt.Errorf("%w", ErrCommandEmpty)
}
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("%w: %q", ErrEscapeUnterminated, 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, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
}
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, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
}
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, fmt.Errorf("%w", ErrEscapeUnterminated)
}
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)
}

View File

@@ -0,0 +1,110 @@
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
errWrapped error
errMessage string
}{
"empty": {
command: "",
errWrapped: ErrCommandEmpty,
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",
errWrapped: ErrSingleQuoteUnterminated,
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
},
"unterminated_double_quote": {
command: "\"abc'def",
errWrapped: ErrDoubleQuoteUnterminated,
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
},
"unterminated_escape": {
command: "abc\\",
errWrapped: ErrEscapeUnterminated,
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
},
"unterminated_escape_only": {
command: " \\",
errWrapped: ErrEscapeUnterminated,
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)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}

View File

@@ -12,6 +12,8 @@ func readObsolete(r *reader.Reader) (warnings []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",
}
sortedKeys := maps.Keys(keyToMessage)
slices.Sort(sortedKeys)

View File

@@ -119,7 +119,7 @@ func (d DoT) toLinesNode() (node *gotree.Node) {
return node
}
update := "disabled" //nolint:goconst
update := "disabled"
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}

View File

@@ -2,6 +2,7 @@ package settings
import (
"fmt"
"net/netip"
"os"
"time"
@@ -24,16 +25,16 @@ type Health struct {
// 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.
// to TCP TLS 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
// ICMPTargetIP is the IP address to use for ICMP echo requests
// in the health checker. It can be set to an unspecified address
// such that the VPN server IP is used, which is also the default behavior.
ICMPTargetIP netip.Addr
// RestartVPN indicates whether to restart the VPN connection
// when the healthcheck fails.
RestartVPN *bool
}
func (h Health) Validate() (err error) {
@@ -42,11 +43,6 @@ func (h Health) Validate() (err error) {
return fmt.Errorf("server listening address is not valid: %w", err)
}
err = h.VPN.validate()
if err != nil {
return fmt.Errorf("health VPN settings: %w", err)
}
return nil
}
@@ -56,8 +52,8 @@ func (h *Health) copy() (copied Health) {
ReadHeaderTimeout: h.ReadHeaderTimeout,
ReadTimeout: h.ReadTimeout,
TargetAddress: h.TargetAddress,
SuccessWait: h.SuccessWait,
VPN: h.VPN.copy(),
ICMPTargetIP: h.ICMPTargetIP,
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
}
}
@@ -69,8 +65,8 @@ func (h *Health) OverrideWith(other Health) {
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)
h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
}
func (h *Health) SetDefaults() {
@@ -80,9 +76,8 @@ func (h *Health) SetDefaults() {
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()
h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
}
func (h Health) String() string {
@@ -93,10 +88,12 @@ func (h Health) toLinesNode() (node *gotree.Node) {
node = gotree.New("Health settings:")
node.Appendf("Server listening address: %s", h.ServerAddress)
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"))
icmpTarget := "VPN server IP"
if !h.ICMPTargetIP.IsUnspecified() {
icmpTarget = h.ICMPTargetIP.String()
}
node.Appendf("ICMP target IP: %s", icmpTarget)
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
return node
}
@@ -104,16 +101,13 @@ func (h *Health) Read(r *reader.Reader) (err error) {
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
h.ICMPTargetIP, err = r.NetipAddr("HEALTH_ICMP_TARGET_IP")
if err != nil {
return err
}
err = h.VPN.read(r)
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
if err != nil {
return fmt.Errorf("VPN health settings: %w", err)
return err
}
return nil
}

View File

@@ -1,76 +0,0 @@
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
}

View File

@@ -29,6 +29,14 @@ 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"`
// 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.
@@ -84,6 +92,8 @@ func (p *PortForwarding) Copy() (copied 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),
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
Username: p.Username,
Password: p.Password,
@@ -94,6 +104,8 @@ 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.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
@@ -103,6 +115,8 @@ 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.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
}
@@ -135,6 +149,13 @@ 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)
@@ -163,6 +184,12 @@ 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.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
if err != nil {
return err

View File

@@ -17,7 +17,7 @@ import (
"github.com/qdm12/gotree"
)
type ServerSelection struct { //nolint:maligned
type ServerSelection struct {
// VPN is the VPN type which can be 'openvpn'
// or 'wireguard'. It cannot be the empty string
// in the internal state.
@@ -354,11 +354,8 @@ 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 := false
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
providers.PrivateInternetAccess, providers.Protonvpn) {
defaultPortForwardOnly = true
}
defaultPortForwardOnly := portForwardingEnabled &&
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults()

View File

@@ -58,12 +58,8 @@ func Test_Settings_String(t *testing.T) {
├── Health settings:
| ├── Server listening address: 127.0.0.1:9999
| ├── Target address: cloudflare.com:443
| ├── Duration to wait after success: 5s
| ── Read header timeout: 100ms
| ├── Read timeout: 500ms
| └── VPN wait durations:
| ├── Initial duration: 6s
| └── Additional duration: 5s
| ├── ICMP target IP: VPN server IP
| ── Restart VPN on healthcheck failure: yes
├── Shadowsocks server settings:
| └── Enabled: no
├── HTTP proxy settings:

View File

@@ -15,7 +15,7 @@ type Shadowsocks struct {
// It defaults to false, and cannot be nil in the internal state.
Enabled *bool
// Settings are settings for the TCP+UDP server.
tcpudp.Settings
Settings tcpudp.Settings
}
func (s Shadowsocks) validate() (err error) {

View File

@@ -16,7 +16,7 @@ func isDeleteMatchInstruction(instruction string) bool {
fields := strings.Fields(instruction)
for i, field := range fields {
switch {
case field != "-D" && field != "--delete": //nolint:goconst
case field != "-D" && field != "--delete":
continue
case i == len(fields)-1: // malformed: missing chain name
return false

View File

@@ -9,7 +9,7 @@ import (
"github.com/qdm12/gluetun/internal/routing"
)
type Config struct { //nolint:maligned
type Config struct {
runner CmdRunner
logger Logger
iptablesMutex sync.Mutex

View File

@@ -58,7 +58,7 @@ var ErrPolicyNotValid = errors.New("policy is not valid")
func (c *Config) setIPv6AllPolicies(ctx context.Context, policy string) error {
switch policy {
case "ACCEPT", "DROP": //nolint:goconst
case "ACCEPT", "DROP":
default:
return fmt.Errorf("%w: %s", ErrPolicyNotValid, policy)
}

View File

@@ -149,7 +149,7 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
) error {
protocol := connection.Protocol
if protocol == "tcp-client" {
protocol = "tcp" //nolint:goconst
protocol = "tcp"
}
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
appendOrDelete(remove), connection.IP, defaultInterface, protocol,

View File

@@ -22,7 +22,7 @@ type chainRule struct {
packets uint64
bytes uint64
target string // "ACCEPT", "DROP", "REJECT" or "REDIRECT"
protocol string // "tcp", "udp" or "" for all protocols.
protocol string // "icmp", "tcp", "udp" or "" for all protocols.
inputInterface string // input interface, for example "tun0" or "*""
outputInterface string // output interface, for example "eth0" or "*""
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
@@ -324,6 +324,8 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
func parseProtocol(s string) (protocol string, err error) {
switch s {
case "0":
case "1":
protocol = "icmp"
case "6":
protocol = "tcp"
case "17":

View File

@@ -56,7 +56,8 @@ num pkts bytes target prot opt in out source destinati
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
3 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
3 0 0 ACCEPT 1 -- tun0 * 0.0.0.0/0 0.0.0.0/0
4 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
`,
table: chain{
name: "INPUT",
@@ -92,6 +93,17 @@ num pkts bytes target prot opt in out source destinati
lineNumber: 3,
packets: 0,
bytes: 0,
target: "ACCEPT",
protocol: "icmp",
inputInterface: "tun0",
outputInterface: "*",
source: netip.MustParsePrefix("0.0.0.0/0"),
destination: netip.MustParsePrefix("0.0.0.0/0"),
},
{
lineNumber: 4,
packets: 0,
bytes: 0,
target: "DROP",
protocol: "",
inputInterface: "tun0",

View File

@@ -92,7 +92,7 @@ func testIptablesPath(ctx context.Context, path string,
// Set policy as the existing policy so no mutation is done.
// This is an extra check for some buggy kernels where setting the policy
// does not work.
cmd = exec.CommandContext(ctx, path, "-L", "INPUT")
cmd = exec.CommandContext(ctx, path, "-nL", "INPUT")
output, err = runner.Run(cmd)
if err != nil {
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)

View File

@@ -24,7 +24,7 @@ func newDeleteTestRuleMatcher(path string) *cmdMatcher {
func newListInputRulesMatcher(path string) *cmdMatcher {
return newCmdMatcher(path,
"^-L$", "^INPUT$")
"^-nL$", "^INPUT$")
}
func newSetPolicyMatcher(path, inputPolicy string) *cmdMatcher { //nolint:unparam

View File

@@ -0,0 +1,250 @@
package healthcheck
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/netip"
"strings"
"sync"
"time"
"github.com/qdm12/gluetun/internal/healthcheck/dns"
"github.com/qdm12/gluetun/internal/healthcheck/icmp"
)
type Checker struct {
tlsDialAddr string
dialer *net.Dialer
echoer *icmp.Echoer
dnsClient *dns.Client
logger Logger
icmpTarget netip.Addr
configMutex sync.Mutex
icmpNotPermitted bool
smallCheckName string
// Internal periodic service signals
stop context.CancelFunc
done <-chan struct{}
}
func NewChecker(logger Logger) *Checker {
return &Checker{
dialer: &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
},
},
echoer: icmp.NewEchoer(logger),
dnsClient: dns.New(),
logger: logger,
}
}
// SetConfig sets the TCP+TLS dial address and the ICMP echo IP address
// to target by the [Checker].
// This function MUST be called before calling [Checker.Start].
func (c *Checker) SetConfig(tlsDialAddr string, icmpTarget netip.Addr) {
c.configMutex.Lock()
defer c.configMutex.Unlock()
c.tlsDialAddr = tlsDialAddr
c.icmpTarget = icmpTarget
}
// Start starts the checker by first running a blocking 2s-timed TCP+TLS check,
// and, on success, starts the periodic checks in a separate goroutine:
// - a "small" ICMP echo check every 15 seconds
// - a "full" TCP+TLS check every 5 minutes
// It returns a channel `runError` that receives an error (nil or not) when a periodic check is performed.
// It returns an error if the initial TCP+TLS check fails.
// The Checker has to be ultimately stopped by calling [Checker.Stop].
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
if c.tlsDialAddr == "" || c.icmpTarget.IsUnspecified() {
panic("call Checker.SetConfig with non empty values before Checker.Start")
}
// connection isn't under load yet when the checker starts, so a short
// 6 seconds timeout suffices and provides quick enough feedback that
// the new connection is not working.
const timeout = 6 * time.Second
tcpTLSCheckCtx, tcpTLSCheckCancel := context.WithTimeout(ctx, timeout)
err = tcpTLSCheck(tcpTLSCheckCtx, c.dialer, c.tlsDialAddr)
tcpTLSCheckCancel()
if err != nil {
return nil, fmt.Errorf("startup check: %w", err)
}
ready := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
c.stop = cancel
done := make(chan struct{})
c.done = done
c.smallCheckName = "ICMP echo"
const smallCheckPeriod = time.Minute
smallCheckTimer := time.NewTimer(smallCheckPeriod)
const fullCheckPeriod = 5 * time.Minute
fullCheckTimer := time.NewTimer(fullCheckPeriod)
runErrorCh := make(chan error)
runError = runErrorCh
go func() {
defer close(done)
close(ready)
for {
select {
case <-ctx.Done():
fullCheckTimer.Stop()
smallCheckTimer.Stop()
return
case <-smallCheckTimer.C:
err := c.smallPeriodicCheck(ctx)
if err != nil {
err = fmt.Errorf("small periodic check: %w", err)
}
runErrorCh <- err
smallCheckTimer.Reset(smallCheckPeriod)
case <-fullCheckTimer.C:
err := c.fullPeriodicCheck(ctx)
if err != nil {
err = fmt.Errorf("full periodic check: %w", err)
}
runErrorCh <- err
fullCheckTimer.Reset(fullCheckPeriod)
}
}
}()
<-ready
return runError, nil
}
func (c *Checker) Stop() error {
c.stop()
<-c.done
c.icmpTarget = netip.Addr{}
return nil
}
func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
c.configMutex.Lock()
ip := c.icmpTarget
c.configMutex.Unlock()
const maxTries = 3
const timeout = 10 * time.Second
const extraTryTime = 10 * time.Second // 10s added for each subsequent retry
check := func(ctx context.Context) error {
if c.icmpNotPermitted {
return c.dnsClient.Check(ctx)
}
err := c.echoer.Echo(ctx, ip)
if errors.Is(err, icmp.ErrNotPermitted) {
c.icmpNotPermitted = true
c.smallCheckName = "plain DNS over UDP"
c.logger.Infof("%s; permanently falling back to %s checks.", c.smallCheckName, err)
return c.dnsClient.Check(ctx)
}
return err
}
return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, c.smallCheckName, check)
}
func (c *Checker) fullPeriodicCheck(ctx context.Context) error {
const maxTries = 2
// 20s timeout in case the connection is under stress
// See https://github.com/qdm12/gluetun/issues/2270
const timeout = 20 * time.Second
const extraTryTime = 10 * time.Second // 10s added for each subsequent retry
check := func(ctx context.Context) error {
return tcpTLSCheck(ctx, c.dialer, c.tlsDialAddr)
}
return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, "TCP+TLS dial", check)
}
func tcpTLSCheck(ctx context.Context, dialer *net.Dialer, targetAddress string) error {
// TODO use mullvad API if current provider is Mullvad
address, err := makeAddressToDial(targetAddress)
if err != nil {
return err
}
const dialNetwork = "tcp4"
connection, err := dialer.DialContext(ctx, dialNetwork, address)
if err != nil {
return fmt.Errorf("dialing: %w", err)
}
if strings.HasSuffix(address, ":443") {
host, _, err := net.SplitHostPort(address)
if err != nil {
return fmt.Errorf("splitting host and port: %w", err)
}
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
}
tlsConnection := tls.Client(connection, tlsConfig)
err = tlsConnection.HandshakeContext(ctx)
if err != nil {
return fmt.Errorf("running TLS handshake: %w", err)
}
}
err = connection.Close()
if err != nil {
return fmt.Errorf("closing connection: %w", err)
}
return nil
}
func makeAddressToDial(address string) (addressToDial string, err error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
addrErr := new(net.AddrError)
ok := errors.As(err, &addrErr)
if !ok || addrErr.Err != "missing port in address" {
return "", fmt.Errorf("splitting host and port from address: %w", err)
}
host = address
const defaultPort = "443"
port = defaultPort
}
address = net.JoinHostPort(host, port)
return address, nil
}
var ErrAllCheckTriesFailed = errors.New("all check tries failed")
func withRetries(ctx context.Context, maxTries uint, tryTimeout, extraTryTime time.Duration,
logger Logger, checkName string, check func(ctx context.Context) error,
) error {
try := uint(0)
var errs []error
for {
timeout := tryTimeout + time.Duration(try)*extraTryTime //nolint:gosec
checkCtx, cancel := context.WithTimeout(ctx, timeout)
err := check(checkCtx)
cancel()
switch {
case err == nil:
return nil
case ctx.Err() != nil:
return fmt.Errorf("%s: %w", checkName, ctx.Err())
}
logger.Debugf("%s attempt %d/%d failed: %s", checkName, try+1, maxTries, err)
errs = append(errs, err)
try++
if try < maxTries {
continue
}
errStrings := make([]string, len(errs))
for i, err := range errs {
errStrings[i] = fmt.Sprintf("attempt %d: %s", i+1, err.Error())
}
return fmt.Errorf("%w: after %d %s attempts (%s)",
ErrAllCheckTriesFailed, maxTries, checkName, strings.Join(errStrings, "; "))
}
}

View File

@@ -7,12 +7,11 @@ import (
"testing"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Server_healthCheck(t *testing.T) {
func Test_Checker_fullcheck(t *testing.T) {
t.Parallel()
t.Run("canceled real dialer", func(t *testing.T) {
@@ -21,26 +20,28 @@ func Test_Server_healthCheck(t *testing.T) {
dialer := &net.Dialer{}
const address = "cloudflare.com:443"
server := &Server{
checker := &Checker{
dialer: dialer,
config: settings.Health{
TargetAddress: address,
},
tlsDialAddr: address,
}
canceledCtx, cancel := context.WithCancel(context.Background())
cancel()
err := server.healthCheck(canceledCtx)
err := checker.fullPeriodicCheck(canceledCtx)
require.Error(t, err)
assert.Contains(t, err.Error(), "operation was canceled")
assert.EqualError(t, err, "TCP+TLS dial: context canceled")
})
t.Run("dial localhost:0", func(t *testing.T) {
t.Parallel()
listener, err := net.Listen("tcp4", "localhost:0")
const timeout = 100 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
listenConfig := &net.ListenConfig{}
listener, err := listenConfig.Listen(ctx, "tcp4", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() {
err = listener.Close()
@@ -50,18 +51,12 @@ func Test_Server_healthCheck(t *testing.T) {
listeningAddress := listener.Addr()
dialer := &net.Dialer{}
server := &Server{
checker := &Checker{
dialer: dialer,
config: settings.Health{
TargetAddress: listeningAddress.String(),
},
tlsDialAddr: listeningAddress.String(),
}
const timeout = 100 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err = server.healthCheck(ctx)
err = checker.fullPeriodicCheck(ctx)
assert.NoError(t, err)
})

View File

@@ -0,0 +1,39 @@
package dns
import (
"context"
"errors"
"fmt"
"net"
)
// Client is a simple plaintext UDP DNS client, to be used for healthchecks.
// Note the client connects to a DNS server only over UDP on port 53,
// because we don't want to use DoT or DoH and impact the TCP connections
// when running a healthcheck.
type Client struct{}
func New() *Client {
return &Client{}
}
var ErrLookupNoIPs = errors.New("no IPs found from DNS lookup")
func (c *Client) Check(ctx context.Context) error {
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "udp", "1.1.1.1:53")
},
}
ips, err := resolver.LookupIP(ctx, "ip", "github.com")
switch {
case err != nil:
return err
case len(ips) == 0:
return fmt.Errorf("%w", ErrLookupNoIPs)
default:
return nil
}
}

View File

@@ -9,13 +9,15 @@ import (
type handler struct {
healthErr error
healthErrMu sync.RWMutex
logger Logger
}
var errHealthcheckNotRunYet = errors.New("healthcheck did not run yet")
func newHandler() *handler {
func newHandler(logger Logger) *handler {
return &handler{
healthErr: errHealthcheckNotRunYet,
logger: logger,
}
}

View File

@@ -1,122 +0,0 @@
package healthcheck
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"time"
)
func (s *Server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) {
defer close(done)
timeoutIndex := 0
healthcheckTimeouts := []time.Duration{
2 * time.Second,
4 * time.Second,
6 * time.Second,
8 * time.Second,
// This can be useful when the connection is under stress
// See https://github.com/qdm12/gluetun/issues/2270
10 * time.Second,
}
s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait)
for {
previousErr := s.handler.getErr()
timeout := healthcheckTimeouts[timeoutIndex]
healthcheckCtx, healthcheckCancel := context.WithTimeout(
ctx, timeout)
err := s.healthCheck(healthcheckCtx)
healthcheckCancel()
s.handler.setErr(err)
switch {
case previousErr != nil && err == nil: // First success
s.logger.Info("healthy!")
timeoutIndex = 0
s.vpn.healthyTimer.Stop()
s.vpn.healthyWait = *s.config.VPN.Initial
case previousErr == nil && err != nil: // First failure
s.logger.Debug("unhealthy: " + err.Error())
s.vpn.healthyTimer.Stop()
s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait)
case previousErr != nil && err != nil: // Nth failure
if timeoutIndex < len(healthcheckTimeouts)-1 {
timeoutIndex++
}
select {
case <-s.vpn.healthyTimer.C:
timeoutIndex = 0 // retry next with the smallest timeout
s.onUnhealthyVPN(ctx, err.Error())
default:
}
case previousErr == nil && err == nil: // Nth success
timer := time.NewTimer(s.config.SuccessWait)
select {
case <-ctx.Done():
return
case <-timer.C:
}
}
}
}
func (s *Server) healthCheck(ctx context.Context) (err error) {
// TODO use mullvad API if current provider is Mullvad
address, err := makeAddressToDial(s.config.TargetAddress)
if err != nil {
return err
}
const dialNetwork = "tcp4"
connection, err := s.dialer.DialContext(ctx, dialNetwork, address)
if err != nil {
return fmt.Errorf("dialing: %w", err)
}
if strings.HasSuffix(address, ":443") {
host, _, err := net.SplitHostPort(address)
if err != nil {
return fmt.Errorf("splitting host and port: %w", err)
}
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
}
tlsConnection := tls.Client(connection, tlsConfig)
err = tlsConnection.HandshakeContext(ctx)
if err != nil {
return fmt.Errorf("running TLS handshake: %w", err)
}
}
err = connection.Close()
if err != nil {
return fmt.Errorf("closing connection: %w", err)
}
return nil
}
func makeAddressToDial(address string) (addressToDial string, err error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
addrErr := new(net.AddrError)
ok := errors.As(err, &addrErr)
if !ok || addrErr.Err != "missing port in address" {
return "", fmt.Errorf("splitting host and port from address: %w", err)
}
host = address
const defaultPort = "443"
port = defaultPort
}
address = net.JoinHostPort(host, port)
return address, nil
}

View File

@@ -0,0 +1,49 @@
package icmp
import (
"net"
"time"
"golang.org/x/net/ipv4"
)
var _ net.PacketConn = &ipv4Wrapper{}
// ipv4Wrapper is a wrapper around ipv4.PacketConn to implement
// the net.PacketConn interface. It's only used for Darwin or iOS.
type ipv4Wrapper struct {
ipv4Conn *ipv4.PacketConn
}
func ipv4ToNetPacketConn(ipv4 *ipv4.PacketConn) *ipv4Wrapper {
return &ipv4Wrapper{ipv4Conn: ipv4}
}
func (i *ipv4Wrapper) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, _, addr, err = i.ipv4Conn.ReadFrom(p)
return n, addr, err
}
func (i *ipv4Wrapper) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return i.ipv4Conn.WriteTo(p, nil, addr)
}
func (i *ipv4Wrapper) Close() error {
return i.ipv4Conn.Close()
}
func (i *ipv4Wrapper) LocalAddr() net.Addr {
return i.ipv4Conn.LocalAddr()
}
func (i *ipv4Wrapper) SetDeadline(t time.Time) error {
return i.ipv4Conn.SetDeadline(t)
}
func (i *ipv4Wrapper) SetReadDeadline(t time.Time) error {
return i.ipv4Conn.SetReadDeadline(t)
}
func (i *ipv4Wrapper) SetWriteDeadline(t time.Time) error {
return i.ipv4Conn.SetWriteDeadline(t)
}

View File

@@ -0,0 +1,193 @@
package icmp
import (
"bytes"
"context"
cryptorand "crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"math/rand/v2"
"net"
"net/netip"
"strings"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
var (
ErrICMPBodyUnsupported = errors.New("ICMP body type is not supported")
ErrICMPEchoDataMismatch = errors.New("ICMP data mismatch")
)
type Echoer struct {
buffer []byte
randomSource io.Reader
logger Logger
}
func NewEchoer(logger Logger) *Echoer {
const maxICMPEchoSize = 1500
buffer := make([]byte, maxICMPEchoSize)
var seed [32]byte
_, _ = cryptorand.Read(seed[:])
randomSource := rand.NewChaCha8(seed)
return &Echoer{
buffer: buffer,
randomSource: randomSource,
logger: logger,
}
}
var (
ErrTimedOut = errors.New("timed out waiting for ICMP echo reply")
ErrNotPermitted = errors.New("not permitted")
)
func (i *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) {
var ipVersion string
var conn net.PacketConn
if ip.Is4() {
ipVersion = "v4"
conn, err = listenICMPv4(ctx)
} else {
ipVersion = "v6"
conn, err = listenICMPv6(ctx)
}
if err != nil {
if strings.HasSuffix(err.Error(), "socket: operation not permitted") {
err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrNotPermitted)
}
return fmt.Errorf("listening for ICMP packets: %w", err)
}
go func() {
<-ctx.Done()
conn.Close()
}()
const echoDataSize = 32
id, message := buildMessageToSend(ipVersion, echoDataSize, i.randomSource)
encodedMessage, err := message.Marshal(nil)
if err != nil {
return fmt.Errorf("encoding ICMP message: %w", err)
}
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice()})
if err != nil {
if strings.HasSuffix(err.Error(), "sendto: operation not permitted") {
err = fmt.Errorf("%w", ErrNotPermitted)
}
return fmt.Errorf("writing ICMP message: %w", err)
}
receivedData, err := receiveEchoReply(conn, id, i.buffer, ipVersion, i.logger)
if err != nil {
if errors.Is(err, net.ErrClosed) && ctx.Err() != nil {
return fmt.Errorf("%w", ErrTimedOut)
}
return fmt.Errorf("receiving ICMP echo reply: %w", err)
}
sentData := message.Body.(*icmp.Echo).Data //nolint:forcetypeassert
if !bytes.Equal(receivedData, sentData) {
return fmt.Errorf("%w: sent %x and received %x", ErrICMPEchoDataMismatch, sentData, receivedData)
}
return nil
}
func buildMessageToSend(ipVersion string, size uint, randomSource io.Reader) (id int, message *icmp.Message) {
const uint16Bytes = 2
idBytes := make([]byte, uint16Bytes)
_, _ = randomSource.Read(idBytes)
id = int(binary.BigEndian.Uint16(idBytes))
var icmpType icmp.Type
switch ipVersion {
case "v4":
icmpType = ipv4.ICMPTypeEcho
case "v6":
icmpType = ipv6.ICMPTypeEchoRequest
default:
panic(fmt.Sprintf("IP version %q not supported", ipVersion))
}
messageBodyData := make([]byte, size)
_, _ = randomSource.Read(messageBodyData)
// See https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types
message = &icmp.Message{
Type: icmpType, // echo request
Code: 0, // no code
Checksum: 0, // calculated at encoding (ipv4) or sending (ipv6)
Body: &icmp.Echo{
ID: id,
Seq: 0, // only one packet
Data: messageBodyData,
},
}
return id, message
}
func receiveEchoReply(conn net.PacketConn, id int, buffer []byte, ipVersion string, logger Logger,
) (data []byte, err error) {
var icmpProtocol int
const (
icmpv4Protocol = 1
icmpv6Protocol = 58
)
switch ipVersion {
case "v4":
icmpProtocol = icmpv4Protocol
case "v6":
icmpProtocol = icmpv6Protocol
default:
panic(fmt.Sprintf("unknown IP version: %s", ipVersion))
}
for {
// Note we need to read the whole packet in one call to ReadFrom, so the buffer
// must be large enough to read the entire reply packet. See:
// https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J
bytesRead, returnAddr, err := conn.ReadFrom(buffer)
if err != nil {
return nil, fmt.Errorf("reading from ICMP connection: %w", err)
}
packetBytes := buffer[:bytesRead]
// Parse the ICMP message
message, err := icmp.ParseMessage(icmpProtocol, packetBytes)
if err != nil {
return nil, fmt.Errorf("parsing message: %w", err)
}
switch body := message.Body.(type) {
case *icmp.Echo:
if id != body.ID {
logger.Warnf("ignoring ICMP echo reply mismatching expected id %d "+
"(id: %d, type: %d, code: %d, length: %d, return address %s)",
id, body.ID, message.Type, message.Code, len(packetBytes), returnAddr)
continue // not the ID we are looking for
}
return body.Data, nil
case *icmp.DstUnreach:
logger.Debugf("ignoring ICMP destination unreachable message (type: 3, code: %d, return address %s, expected-id %d)",
message.Code, returnAddr, id)
// See https://github.com/qdm12/gluetun/pull/2923#issuecomment-3377532249
// on why we ignore this message. If it is actually unreachable, the timeout on waiting for
// the echo reply will do instead of returning an error error.
continue
case *icmp.TimeExceeded:
logger.Debugf("ignoring ICMP time exceeded message (type: 11, code: %d, return address %s, expected-id %d)",
message.Code, returnAddr, id)
continue
default:
return nil, fmt.Errorf("%w: %T (type %d, code %d, return address %s, expected-id %d)",
ErrICMPBodyUnsupported, body, message.Type, message.Code, returnAddr, id)
}
}
}

View File

@@ -0,0 +1,6 @@
package icmp
type Logger interface {
Debugf(format string, args ...any)
Warnf(format string, args ...any)
}

View File

@@ -0,0 +1,35 @@
package icmp
import (
"context"
"fmt"
"net"
"runtime"
"golang.org/x/net/ipv4"
)
func listenICMPv4(ctx context.Context) (conn net.PacketConn, err error) {
var listenConfig net.ListenConfig
const listenAddress = ""
packetConn, err := listenConfig.ListenPacket(ctx, "ip4:icmp", listenAddress)
if err != nil {
return nil, fmt.Errorf("listening for ICMP packets: %w", err)
}
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
packetConn = ipv4ToNetPacketConn(ipv4.NewPacketConn(packetConn))
}
return packetConn, nil
}
func listenICMPv6(ctx context.Context) (conn net.PacketConn, err error) {
var listenConfig net.ListenConfig
const listenAddress = ""
packetConn, err := listenConfig.ListenPacket(ctx, "ip6:ipv6-icmp", listenAddress)
if err != nil {
return nil, fmt.Errorf("listening for ICMPv6 packets: %w", err)
}
return packetConn, nil
}

View File

@@ -0,0 +1,9 @@
package healthcheck
type Logger interface {
Debugf(format string, args ...any)
Info(s string)
Infof(format string, args ...any)
Warnf(format string, args ...any)
Error(s string)
}

View File

@@ -1,7 +0,0 @@
package healthcheck
type Logger interface {
Debug(s string)
Info(s string)
Error(s string)
}

View File

@@ -1,25 +0,0 @@
package healthcheck
import (
"context"
"time"
"github.com/qdm12/gluetun/internal/constants"
)
type vpnHealth struct {
loop StatusApplier
healthyWait time.Duration
healthyTimer *time.Timer
}
func (s *Server) onUnhealthyVPN(ctx context.Context, lastErrMessage string) {
s.logger.Info("program has been unhealthy for " +
s.vpn.healthyWait.String() + ": restarting VPN (healthcheck error: " + lastErrMessage + ")")
s.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md")
s.logger.Info("DO NOT OPEN AN ISSUE UNLESS YOU READ AND TRIED EACH POSSIBLE SOLUTION")
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Stopped)
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Running)
s.vpn.healthyWait += *s.config.VPN.Addition
s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait)
}

View File

@@ -10,9 +10,6 @@ import (
func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
defer close(done)
loopDone := make(chan struct{})
go s.runHealthcheckLoop(ctx, loopDone)
server := http.Server{
Addr: s.config.ServerAddress,
Handler: s.handler,
@@ -37,6 +34,5 @@ func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
s.logger.Error(err.Error())
}
<-loopDone
<-serverDone
}

View File

@@ -2,7 +2,6 @@ package healthcheck
import (
"context"
"net"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models"
@@ -11,30 +10,21 @@ import (
type Server struct {
logger Logger
handler *handler
dialer *net.Dialer
config settings.Health
vpn vpnHealth
}
func NewServer(config settings.Health,
logger Logger, vpnLoop StatusApplier,
) *Server {
func NewServer(config settings.Health, logger Logger) *Server {
return &Server{
logger: logger,
handler: newHandler(),
dialer: &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
},
},
handler: newHandler(logger),
config: config,
vpn: vpnHealth{
loop: vpnLoop,
healthyWait: *config.VPN.Initial,
},
}
}
func (s *Server) SetError(err error) {
s.handler.setErr(err)
}
type StatusApplier interface {
ApplyStatus(ctx context.Context, status models.LoopStatus) (
outcome string, err error)

View File

@@ -20,8 +20,10 @@ func (s *Server) Run(ctx context.Context, ready chan<- struct{}, done chan<- str
crashed := make(chan struct{})
shutdownDone := make(chan struct{})
listenCtx, listenCancel := context.WithCancel(ctx)
go func() {
defer close(shutdownDone)
defer listenCancel()
select {
case <-ctx.Done():
case <-crashed:
@@ -37,7 +39,8 @@ func (s *Server) Run(ctx context.Context, ready chan<- struct{}, done chan<- str
}
}()
listener, err := net.Listen("tcp", s.address)
listenConfig := &net.ListenConfig{}
listener, err := listenConfig.Listen(listenCtx, "tcp", s.address)
if err != nil {
close(s.addressSet)
close(crashed) // stop shutdown goroutine

View File

@@ -76,9 +76,9 @@ func initModule(path string) (err error) {
const flags = 0
err = unix.FinitModule(int(file.Fd()), moduleParams, flags)
switch {
case err == nil, err == unix.EEXIST: //nolint:goerr113
case err == nil, err == unix.EEXIST: //nolint:err113
return nil
case err != unix.ENOSYS: //nolint:goerr113
case err != unix.ENOSYS: //nolint:err113
if strings.HasSuffix(err.Error(), "operation not permitted") {
err = fmt.Errorf("%w; did you set the SYS_MODULE capability to your container?", err)
}

View File

@@ -2,6 +2,7 @@ package natpmp
import (
"context"
"net"
"net/netip"
"testing"
"time"
@@ -23,14 +24,15 @@ func Test_Client_ExternalAddress(t *testing.T) {
durationSinceStartOfEpoch time.Duration
externalIPv4Address netip.Addr
err error
errMessage string
errMessageRegex string
}{
"failure": {
ctx: canceledCtx,
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
initialConnDuration: initialConnectionDuration,
err: context.Canceled,
errMessage: "executing remote procedure call: reading from udp connection: context canceled",
err: net.ErrClosed,
errMessageRegex: "executing remote procedure call: setting connection deadline: " +
"set udp 127.0.0.1:[1-9][0-9]{1,4}: use of closed network connection",
},
"success": {
ctx: context.Background(),
@@ -60,7 +62,7 @@ func Test_Client_ExternalAddress(t *testing.T) {
durationSinceStartOfEpoch, externalIPv4Address, err := client.ExternalAddress(testCase.ctx, testCase.gateway)
assert.ErrorIs(t, err, testCase.err)
if testCase.err != nil {
assert.EqualError(t, err, testCase.errMessage)
assert.Regexp(t, testCase.errMessageRegex, err.Error())
}
assert.Equal(t, testCase.durationSinceStartOfEpoch, durationSinceStartOfEpoch)
assert.Equal(t, testCase.externalIPv4Address, externalIPv4Address)

View File

@@ -45,8 +45,10 @@ func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
cancel()
<-endGoroutineDone
}()
ctxListeningReady := make(chan struct{})
go func() {
defer close(endGoroutineDone)
close(ctxListeningReady)
// Context is canceled either by the parent context or
// when this function returns.
<-ctx.Done()
@@ -60,6 +62,7 @@ func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
}
err = fmt.Errorf("%w; closing connection: %w", err, closeErr)
}()
<-ctxListeningReady // really to make unit testing reliable
const maxResponseSize = 16
response = make([]byte, maxResponseSize)

View File

@@ -13,7 +13,7 @@ const (
func FamilyToString(family int) string {
switch family {
case FamilyAll:
return "all" //nolint:goconst
return "all"
case FamilyV4:
return "v4"
case FamilyV6:

View File

@@ -3,6 +3,7 @@ package portforward
import (
"context"
"net/netip"
"os/exec"
)
type Service interface {
@@ -29,3 +30,8 @@ type Logger interface {
Warn(s string)
Error(s string)
}
type Cmder interface {
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error)
}

View File

@@ -20,6 +20,7 @@ type Loop struct {
client *http.Client
portAllower PortAllower
logger Logger
cmder Cmder
// Fixed parameters
uid, gid int
// Internal channels and locks
@@ -34,7 +35,7 @@ type Loop struct {
func NewLoop(settings settings.PortForwarding, routing Routing,
client *http.Client, portAllower PortAllower,
logger Logger, uid, gid int,
logger Logger, cmder Cmder, uid, gid int,
) *Loop {
return &Loop{
settings: Settings{
@@ -42,6 +43,8 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
Service: service.Settings{
Enabled: settings.Enabled,
Filepath: *settings.Filepath,
UpCommand: *settings.UpCommand,
DownCommand: *settings.DownCommand,
ListeningPort: *settings.ListeningPort,
},
},
@@ -49,6 +52,7 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
client: client,
portAllower: portAllower,
logger: logger,
cmder: cmder,
uid: uid,
gid: gid,
}
@@ -115,7 +119,7 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
*serviceSettings.Enabled = *serviceSettings.Enabled && *l.settings.VPNIsUp
l.service = service.New(serviceSettings, l.routing, l.client,
l.portAllower, l.logger, l.uid, l.gid)
l.portAllower, l.logger, l.cmder, l.uid, l.gid)
var err error
serviceRunError, err = l.service.Start(runCtx)

View File

@@ -0,0 +1,59 @@
package service
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/qdm12/gluetun/internal/command"
)
func runCommand(ctx context.Context, cmder Cmder, logger Logger,
commandTemplate string, ports []uint16,
) (err error) {
portStrings := make([]string, len(ports))
for i, port := range ports {
portStrings[i] = fmt.Sprint(int(port))
}
portsString := strings.Join(portStrings, ",")
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
args, err := command.Split(commandString)
if err != nil {
return fmt.Errorf("parsing command: %w", err)
}
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec G204
stdout, stderr, waitError, err := cmder.Start(cmd)
if err != nil {
return err
}
streamCtx, streamCancel := context.WithCancel(context.Background())
streamDone := make(chan struct{})
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
err = <-waitError
streamCancel()
<-streamDone
return err
}
func streamLines(ctx context.Context, done chan<- struct{},
logger Logger, stdout, stderr <-chan string,
) {
defer close(done)
var line string
for {
select {
case <-ctx.Done():
return
case line = <-stdout:
logger.Info(line)
case line = <-stderr:
logger.Error(line)
}
}
}

View File

@@ -0,0 +1,28 @@
//go:build linux
package service
import (
"context"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/command"
"github.com/stretchr/testify/require"
)
func Test_Service_runCommand(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := context.Background()
cmder := command.New()
const commandTemplate = `/bin/sh -c "echo {{PORTS}}"`
ports := []uint16{1234, 5678}
logger := NewMockLogger(ctrl)
logger.EXPECT().Info("1234,5678")
err := runCommand(ctx, cmder, logger, commandTemplate, ports)
require.NoError(t, err)
}

View File

@@ -3,6 +3,7 @@ package service
import (
"context"
"net/netip"
"os/exec"
"github.com/qdm12/gluetun/internal/provider/utils"
)
@@ -32,3 +33,8 @@ type PortForwarder interface {
ports []uint16, err error)
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
}
type Cmder interface {
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error)
}

View File

@@ -0,0 +1,3 @@
package service
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Logger

View File

@@ -0,0 +1,82 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/portforward/service (interfaces: Logger)
// Package service is a generated GoMock package.
package service
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)
}
// Info mocks base method.
func (m *MockLogger) Info(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Info", arg0)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
}
// Warn mocks base method.
func (m *MockLogger) Warn(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Warn", arg0)
}
// Warn indicates an expected call of Warn.
func (mr *MockLoggerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), arg0)
}

View File

@@ -19,6 +19,7 @@ type Service struct {
client *http.Client
portAllower PortAllower
logger Logger
cmder Cmder
// Internal channels and locks
startStopMutex sync.Mutex
keepPortCancel context.CancelFunc
@@ -26,7 +27,7 @@ type Service struct {
}
func New(settings Settings, routing Routing, client *http.Client,
portAllower PortAllower, logger Logger, puid, pgid int,
portAllower PortAllower, logger Logger, cmder Cmder, puid, pgid int,
) *Service {
return &Service{
// Fixed parameters
@@ -38,6 +39,7 @@ func New(settings Settings, routing Routing, client *http.Client,
client: client,
portAllower: portAllower,
logger: logger,
cmder: cmder,
}
}

View File

@@ -12,6 +12,8 @@ type Settings struct {
Enabled *bool
PortForwarder PortForwarder
Filepath string
UpCommand string
DownCommand string
Interface string // needed for PIA, PrivateVPN and ProtonVPN, tun0 for example
ServerName string // needed for PIA
CanPortForward bool // needed for PIA
@@ -24,6 +26,8 @@ func (s Settings) Copy() (copied Settings) {
copied.Enabled = gosettings.CopyPointer(s.Enabled)
copied.PortForwarder = s.PortForwarder
copied.Filepath = s.Filepath
copied.UpCommand = s.UpCommand
copied.DownCommand = s.DownCommand
copied.Interface = s.Interface
copied.ServerName = s.ServerName
copied.CanPortForward = s.CanPortForward
@@ -37,6 +41,8 @@ func (s *Settings) OverrideWith(update Settings) {
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, update.Enabled)
s.PortForwarder = gosettings.OverrideWithComparable(s.PortForwarder, update.PortForwarder)
s.Filepath = gosettings.OverrideWithComparable(s.Filepath, update.Filepath)
s.UpCommand = gosettings.OverrideWithComparable(s.UpCommand, update.UpCommand)
s.DownCommand = gosettings.OverrideWithComparable(s.DownCommand, update.DownCommand)
s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface)
s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName)
s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward)

View File

@@ -73,6 +73,14 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
s.ports = ports
s.portMutex.Unlock()
if s.settings.UpCommand != "" {
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports)
if err != nil {
err = fmt.Errorf("running up command: %w", err)
s.logger.Error(err.Error())
}
}
keepPortCtx, keepPortCancel := context.WithCancel(context.Background())
s.keepPortCancel = keepPortCancel
runErrorCh := make(chan error)

View File

@@ -3,7 +3,7 @@ package service
import (
"context"
"fmt"
"os"
"time"
)
func (s *Service) Stop() (err error) {
@@ -30,6 +30,17 @@ func (s *Service) cleanup() (err error) {
s.portMutex.Lock()
defer s.portMutex.Unlock()
if s.settings.DownCommand != "" {
const downTimeout = 60 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), downTimeout)
defer cancel()
err = runCommand(ctx, s.cmder, s.logger, s.settings.DownCommand, s.ports)
if err != nil {
err = fmt.Errorf("running down command: %w", err)
s.logger.Error(err.Error())
}
}
for _, port := range s.ports {
err = s.portAllower.RemoveAllowedPort(context.Background(), port)
if err != nil {
@@ -49,10 +60,10 @@ func (s *Service) cleanup() (err error) {
s.ports = nil
filepath := s.settings.Filepath
s.logger.Info("removing port file " + filepath)
err = os.Remove(filepath)
s.logger.Info("clearing port file " + filepath)
err = s.writePortForwardedFile(nil)
if err != nil {
return fmt.Errorf("removing port file: %w", err)
return fmt.Errorf("clearing port file: %w", err)
}
return nil

View File

@@ -4,7 +4,6 @@ import (
"github.com/qdm12/gluetun/internal/models"
)
//nolint:lll
func hardcodedServers() (servers []models.Server) {
return []models.Server{
{Country: "Albania", Hostname: "albania-ca-version-2.expressnetw.com"},
@@ -12,69 +11,83 @@ func hardcodedServers() (servers []models.Server) {
{Country: "Andorra", Hostname: "andorra-ca-version-2.expressnetw.com"},
{Country: "Argentina", Hostname: "argentina-ca-version-2.expressnetw.com"},
{Country: "Armenia", Hostname: "armenia-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Adelaide", Hostname: "australia-adelaide--ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Brisbane", Hostname: "australia-brisbane-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Melbourne", Hostname: "australia-melbourne-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Perth", Hostname: "australia-perth-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Sydney", Hostname: "australia-sydney-2-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Sydney", Hostname: "australia-sydney-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Woolloomooloo", Hostname: "australia-woolloomooloo-2-ca-version-2.expressnetw.com"},
{Country: "Austria", Hostname: "austria-ca-version-2.expressnetw.com"},
{Country: "Azerbaijan", Hostname: "azerbaijan-ca-version-2.expressnetw.com"},
{Country: "Bahamas", Hostname: "bahamas-ca-version-2.expressnetw.com"},
{Country: "Bangladesh", Hostname: "bangladesh-ca-version-2.expressnetw.com"},
{Country: "Belarus", Hostname: "belarus-ca-version-2.expressnetw.com"},
{Country: "Belgium", Hostname: "belgium-ca-version-2.expressnetw.com"},
{Country: "Bermuda", Hostname: "bermuda-ca-version-2.expressnetw.com"},
{Country: "Bhutan", Hostname: "bhutan-ca-version-2.expressnetw.com"},
{Country: "Bosnia And Herzegovina", City: "Bosnia And Herzegovina", Hostname: "bosniaandherzegovina-ca-version-2.expressnetw.com"},
{Country: "Bolivia", Hostname: "bolivia-ca-version-2.expressnetw.com"},
{Country: "Bosnia and Herzegovina", Hostname: "bosniaandherzegovina-ca-version-2.expressnetw.com"},
{Country: "Brazil", Hostname: "brazil-2-ca-version-2.expressnetw.com"},
{Country: "Brazil", Hostname: "brazil-ca-version-2.expressnetw.com"},
{Country: "Brunei", Hostname: "brunei-ca-version-2.expressnetw.com"},
{Country: "Bulgaria", Hostname: "bulgaria-ca-version-2.expressnetw.com"},
{Country: "Cambodia", Hostname: "cambodia-ca-version-2.expressnetw.com"},
{Country: "Canada", City: "Montreal", Hostname: "canada-montreal-ca-version-2.expressnetw.com"},
{Country: "Canada", City: "Montreal", Hostname: "canada-montreal-ca-version-2.expressnetw.com"},
{Country: "Canada", City: "Toronto", Hostname: "canada-toronto-2-ca-version-2.expressnetw.com"},
{Country: "Canada", City: "Toronto", Hostname: "canada-toronto-ca-version-2.expressnetw.com"},
{Country: "Canada", City: "Vancouver", Hostname: "canada-vancouver-ca-version-2.expressnetw.com"},
{Country: "Cayman Islands", Hostname: "caymanislands-ca-version-2.expressnetw.com"},
{Country: "Chile", Hostname: "chile-ca-version-2.expressnetw.com"},
{Country: "Colombia", Hostname: "colombia-ca-version-2.expressnetw.com"},
{Country: "Costa Rica", City: "Costa Rica", Hostname: "costarica-ca-version-2.expressnetw.com"},
{Country: "Costa Rica", Hostname: "costarica-ca-version-2.expressnetw.com"},
{Country: "Croatia", Hostname: "croatia-ca-version-2.expressnetw.com"},
{Country: "Cuba", Hostname: "cuba-ca-version-2.expressnetw.com"},
{Country: "Cyprus", Hostname: "cyprus-ca-version-2.expressnetw.com"},
{Country: "Czech Republic", City: "Czech Republic", Hostname: "czechrepublic-ca-version-2.expressnetw.com"},
{Country: "Czech Republic", Hostname: "czechrepublic-ca-version-2.expressnetw.com"},
{Country: "Denmark", Hostname: "denmark-ca-version-2.expressnetw.com"},
{Country: "Dominican Republic", Hostname: "dominicanrepublic-ca-version-2.expressnetw.com"},
{Country: "Ecuador", Hostname: "ecuador-ca-version-2.expressnetw.com"},
{Country: "Egypt", Hostname: "egypt-ca-version-2.expressnetw.com"},
{Country: "Estonia", Hostname: "estonia-ca-version-2.expressnetw.com"},
{Country: "Finland", Hostname: "finland-ca-version-2.expressnetw.com"},
{Country: "France", City: "Alsace", Hostname: "france-alsace-ca-version-2.expressnetw.com"},
{Country: "France", City: "Marseille", Hostname: "france-marseille-ca-version-2.expressnetw.com"},
{Country: "France", City: "Paris", Hostname: "france-paris-1-ca-version-2.expressnetw.com"},
{Country: "France", City: "Paris", Hostname: "france-paris-2-ca-version-2.expressnetw.com"},
{Country: "France", City: "Strasbourg", Hostname: "france-strasbourg-ca-version-2.expressnetw.com"},
{Country: "Georgia", Hostname: "georgia-ca-version-2.expressnetw.com"},
{Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-1-ca-version-2.expressnetw.com"},
{Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-2-ca-version-2.expressnetw.com"},
{Country: "Germany", City: "Frankfurt", Hostname: "germany-darmstadt-ca-version-2.expressnetw.com"},
{Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-1-ca-version-2.expressnetw.com"},
{Country: "Germany", City: "Nuremberg", Hostname: "germany-nuremberg-ca-version-2.expressnetw.com"},
{Country: "Ghana", Hostname: "ghana-ca-version-2.expressnetw.com"},
{Country: "Greece", Hostname: "greece-ca-version-2.expressnetw.com"},
{Country: "Guam", Hostname: "guam-ca-version-2.expressnetw.com"},
{Country: "Guatemala", Hostname: "guatemala-ca-version-2.expressnetw.com"},
{Country: "Hong Kong", City: "Hong Kong", Hostname: "hongkong-2-ca-version-2.expressnetw.com"},
{Country: "Hong Kong", City: "Hong Kong", Hostname: "hongkong4-ca-version-2.expressnetw.com"},
{Country: "Honduras", Hostname: "honduras-ca-version-2.expressnetw.com"},
{Country: "Hong Kong", Hostname: "hongkong-1-ca-version-2.expressnetw.com"},
{Country: "Hong Kong", Hostname: "hongkong-2-ca-version-2.expressnetw.com"},
{Country: "Hungary", Hostname: "hungary-ca-version-2.expressnetw.com"},
{Country: "Iceland", Hostname: "iceland-ca-version-2.expressnetw.com"},
{Country: "India", City: "Chennai", Hostname: "india-chennai-ca-version-2.expressnetw.com"},
{Country: "India", City: "Mumbai", Hostname: "india-mumbai-1-ca-version-2.expressnetw.com"},
{Country: "India (via Singapore)", Hostname: "india-sg-ca-version-2.expressnetw.com"},
{Country: "India (via UK)", Hostname: "india-uk-ca-version-2.expressnetw.com"},
{Country: "Indonesia", Hostname: "indonesia-ca-version-2.expressnetw.com"},
{Country: "Ireland", Hostname: "ireland-ca-version-2.expressnetw.com"},
{Country: "Isle Of Man", City: "Isle Of Man", Hostname: "isleofman-ca-version-2.expressnetw.com"},
{Country: "Isle of Man", Hostname: "isleofman-ca-version-2.expressnetw.com"},
{Country: "Israel", Hostname: "israel-ca-version-2.expressnetw.com"},
{Country: "Italy", City: "Cosenza", Hostname: "italy-cosenza-ca-version-2.expressnetw.com"},
{Country: "Italy", City: "Milan", Hostname: "italy-milan-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Kawasaki", Hostname: "japan-kawasaki-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-1-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-2-ca-version-2.expressnetw.com"},
{Country: "Italy", City: "Naples", Hostname: "italy-naples-ca-version-2.expressnetw.com"},
{Country: "Jamaica", Hostname: "jamaica-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Osaka", Hostname: "japan-osaka-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Shibuya", Hostname: "japan-shibuya-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Yokohama", Hostname: "japan-yokohama-ca-version-2.expressnetw.com"},
{Country: "Jersey", Hostname: "jersey-ca-version-2.expressnetw.com"},
{Country: "Kazakhstan", Hostname: "kazakhstan-ca-version-2.expressnetw.com"},
{Country: "Kenya", Hostname: "kenya-ca-version-2.expressnetw.com"},
{Country: "Kyrgyzstan", Hostname: "kyrgyzstan-ca-version-2.expressnetw.com"},
{Country: "Laos", Hostname: "laos-ca-version-2.expressnetw.com"},
{Country: "Latvia", Hostname: "latvia-ca-version-2.expressnetw.com"},
{Country: "Lebanon", Hostname: "lebanon-ca-version-2.expressnetw.com"},
{Country: "Liechtenstein", Hostname: "liechtenstein-ca-version-2.expressnetw.com"},
{Country: "Lithuania", Hostname: "lithuania-ca-version-2.expressnetw.com"},
{Country: "Luxembourg", Hostname: "luxembourg-ca-version-2.expressnetw.com"},
@@ -86,21 +99,22 @@ func hardcodedServers() (servers []models.Server) {
{Country: "Monaco", Hostname: "monaco-ca-version-2.expressnetw.com"},
{Country: "Mongolia", Hostname: "mongolia-ca-version-2.expressnetw.com"},
{Country: "Montenegro", Hostname: "montenegro-ca-version-2.expressnetw.com"},
{Country: "Morocco", Hostname: "morocco-ca-version-2.expressnetw.com"},
{Country: "Myanmar", Hostname: "myanmar-ca-version-2.expressnetw.com"},
{Country: "Nepal", Hostname: "nepal-ca-version-2.expressnetw.com"},
{Country: "Netherlands", City: "Amsterdam", Hostname: "netherlands-amsterdam-2-ca-version-2.expressnetw.com"},
{Country: "Netherlands", City: "Amsterdam", Hostname: "netherlands-amsterdam-ca-version-2.expressnetw.com"},
{Country: "Netherlands", City: "Rotterdam", Hostname: "netherlands-rotterdam-ca-version-2.expressnetw.com"},
{Country: "Netherlands", City: "The Hague", Hostname: "netherlands-thehague-ca-version-2.expressnetw.com"},
{Country: "New Zealand", City: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
{Country: "North Macedonia", City: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
{Country: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
{Country: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
{Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"},
{Country: "Pakistan", Hostname: "pakistan-ca-version-2.expressnetw.com"},
{Country: "Panama", Hostname: "panama-ca-version-2.expressnetw.com"},
{Country: "Peru", Hostname: "peru-ca-version-2.expressnetw.com"},
{Country: "Philippines Via Singapore", City: "Philippines Via Singapore", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"},
{Country: "Philippines (via Singapore)", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"},
{Country: "Poland", Hostname: "poland-ca-version-2.expressnetw.com"},
{Country: "Portugal", Hostname: "portugal-ca-version-2.expressnetw.com"},
{Country: "Puerto Rico", Hostname: "puertorico-ca-version-2.expressnetw.com"},
{Country: "Romania", Hostname: "romania-ca-version-2.expressnetw.com"},
{Country: "Serbia", Hostname: "serbia-ca-version-2.expressnetw.com"},
{Country: "Singapore", City: "CBD", Hostname: "singapore-cbd-ca-version-2.expressnetw.com"},
@@ -108,43 +122,58 @@ func hardcodedServers() (servers []models.Server) {
{Country: "Singapore", City: "Marina Bay", Hostname: "singapore-marinabay-ca-version-2.expressnetw.com"},
{Country: "Slovakia", Hostname: "slovakia-ca-version-2.expressnetw.com"},
{Country: "Slovenia", Hostname: "slovenia-ca-version-2.expressnetw.com"},
{Country: "South Africa", City: "South Africa", Hostname: "southafrica-ca-version-2.expressnetw.com"},
{Country: "South Korea", City: "South Korea", Hostname: "southkorea2-ca-version-2.expressnetw.com"},
{Country: "South Africa", Hostname: "southafrica-ca-version-2.expressnetw.com"},
{Country: "South Korea", Hostname: "southkorea2-ca-version-2.expressnetw.com"},
{Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona-ca-version-2.expressnetw.com"},
{Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona2-ca-version-2.expressnetw.com"},
{Country: "Spain", City: "Madrid", Hostname: "spain-ca-version-2.expressnetw.com"},
{Country: "Sri Lanka", City: "Sri Lanka", Hostname: "srilanka-ca-version-2.expressnetw.com"},
{Country: "Sri Lanka", Hostname: "srilanka-ca-version-2.expressnetw.com"},
{Country: "Sweden", Hostname: "sweden-ca-version-2.expressnetw.com"},
{Country: "Sweden", Hostname: "sweden2-ca-version-2.expressnetw.com"},
{Country: "Switzerland", Hostname: "switzerland-2-ca-version-2.expressnetw.com"},
{Country: "Switzerland", Hostname: "switzerland-ca-version-2.expressnetw.com"},
{Country: "Taiwan", Hostname: "taiwan-2-ca-version-2.expressnetw.com"},
{Country: "Taiwan", Hostname: "taiwan-3-ca-version-2.expressnetw.com"},
{Country: "Thailand", Hostname: "thailand-ca-version-2.expressnetw.com"},
{Country: "Trinidad and Tobago", Hostname: "trinidadandtobago-ca-version-2.expressnetw.com"},
{Country: "Turkey", Hostname: "turkey-ca-version-2.expressnetw.com"},
{Country: "Ukraine", Hostname: "ukraine-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Docklands", Hostname: "uk-berkshire-2-ca-version-2.expressnetw.com"},
{Country: "UK", City: "London", Hostname: "uk-east-london-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Docklands", Hostname: "uk-1-docklands-ca-version-2.expressnetw.com"},
{Country: "UK", City: "East London", Hostname: "uk-east-london-ca-version-2.expressnetw.com"},
{Country: "UK", City: "London", Hostname: "uk-london-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Midlands", Hostname: "uk-midlands-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Tottenham", Hostname: "uk-tottenham-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Wembley", Hostname: "uk-wembley-ca-version-2.expressnetw.com"},
{Country: "Ukraine", Hostname: "ukraine-ca-version-2.expressnetw.com"},
{Country: "Uruguay", Hostname: "uruguay-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Albuquerque", Hostname: "usa-albuquerque-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Atlanta", Hostname: "usa-atlanta-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Boston", Hostname: "us-boston-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Chicago", Hostname: "usa-chicago-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Dallas", Hostname: "usa-dallas-2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Dallas", Hostname: "usa-dallas-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Denver", Hostname: "usa-denver-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-1-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Houston", Hostname: "usa-houston-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Jackson", Hostname: "us-jackson-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Lincoln Park", Hostname: "usa-lincolnpark-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Little Rock", Hostname: "us-littlerock-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-3-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles5-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles5-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Miami", Hostname: "usa-miami-2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Miami", Hostname: "usa-miami-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-1-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-3-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New York", Hostname: "us-new-york-2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Orleans", Hostname: "us-neworleans-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New York", Hostname: "usa-newyork-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Oklahoma City", Hostname: "us-oklahoma-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Phoenix", Hostname: "usa-phoenix-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Salt Lake City", Hostname: "usa-saltlakecity-ca-version-2.expressnetw.com"},
{Country: "USA", City: "San Francisco", Hostname: "usa-sanfrancisco-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Santa Monica", Hostname: "usa-santa-monica-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Seattle", Hostname: "usa-seattle-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Tampa", Hostname: "usa-tampa-1-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Washington DC", Hostname: "usa-washingtondc-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Wichita", Hostname: "us-wichita-ca-version-2.expressnetw.com"},
{Country: "Uzbekistan", Hostname: "uzbekistan-ca-version-2.expressnetw.com"},
{Country: "Venezuela", Hostname: "venezuela-ca-version-2.expressnetw.com"},
{Country: "Vietnam", Hostname: "vietnam-ca-version-2.expressnetw.com"},

View File

@@ -14,13 +14,17 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
providerSettings := utils.OpenVPNProviderSettings{
AuthUserPass: true,
Ciphers: []string{
openvpn.AES256gcm,
openvpn.AES256cbc,
},
Auth: openvpn.SHA256,
VerifyX509Type: "name",
TLSCipher: "TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA",
CAs: []string{"MIIErTCCA5WgAwIBAgIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wHhcNMTIwMTExMTkzMjIwWhcNMjgxMTAyMTkzMjIwWjCBlTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkZMMRQwEgYDVQQHEwtXaW50ZXIgUGFyazERMA8GA1UEChMISVBWYW5pc2gxFTATBgNVBAsTDElQVmFuaXNoIFZQTjEUMBIGA1UEAxMLSVBWYW5pc2ggQ0ExIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAaXB2YW5pc2guY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt9DBWNr/IKOuY3TmDP5x7vYZR0DGxLbXU8TyAzBbjUtFFMbhxlHiXVQrZHmgzih94x7BgXM7tWpmMKYVb+gNaqMdWE680Qm3nOwmhy/dulXDkEHAwD05i/iTx4ZaUdtV2vsKBxRg1vdC4AEiwD7bqV4HOi13xcG971aQ55Mj1KeCdA0aNvpat1LWx2jjWxsfI8s2Lv5Fkoi1HO1+vTnnaEsJZrBgAkLXpItqP29Lik3/OBIvkBIxlKrhiVPixE5qNiD+eSPirsmROvsyIonoJtuY4Dw5K6pcNlKyYiwo1IOFYU3YxffwFJk+bSW4WVBhsdf5dGxq/uOHmuz5gdwxCwIDAQABo4H9MIH6MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFEv9FCWJHefBcIPX9p8RHCVOGe6uMIHKBgNVHSMEgcIwgb+AFEv9FCWJHefBcIPX9p8RHCVOGe6uoYGbpIGYMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb22CCQDGCs0kvLjygzANBgkqhkiG9w0BAQ0FAAOCAQEAI2dkh/43ksV2fdYpVGhYaFZPVqCJoToCez0IvOmLeLGzow+EOSrY508oyjYeNP4VJEjApqo0NrMbKl8g/8bpLBcotOCF1c1HZ+y9v7648uumh01SMjsbBeHOuQcLb+7gX6c0pEmxWv8qj5JiW3/1L1bktnjW5Yp5oFkFSMXjOnIoYKHyKLjN2jtwH6XowUNYpg4qVtKU0CXPdOznWcd9/zSfa393HwJPeeVLbKYaFMC4IEbIUmKYtWyoJ9pJ58smU3pWsHZUg9Zc0LZZNjkNlBdQSLmUHAJ33Bd7pJS0JQeiWviC+4UTmzEWRKa7pDGnYRYNu2cUo0/voStphv8EVA=="}, //nolint:lll
CAs: []string{"MIIErzCCA5egAwIBAgIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wIBcNMjIwNTA5MjAyMDQ1WhgPMjA4MjA0MjQyMDIwNDVaMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCRkwxFDASBgNVBAcTC1dpbnRlciBQYXJrMREwDwYDVQQKEwhJUFZhbmlzaDEVMBMGA1UECxMMSVBWYW5pc2ggVlBOMRQwEgYDVQQDEwtJUFZhbmlzaCBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBpcHZhbmlzaC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC30MFY2v8go65jdOYM/nHu9hlHQMbEttdTxPIDMFuNS0UUxuHGUeJdVCtkeaDOKH3jHsGBczu1amYwphVv6A1qox1YTrzRCbec7CaHL926VcOQQcDAPTmL+JPHhlpR21Xa+woHFGDW90LgASLAPtupXgc6LXfFwb3vVpDnkyPUp4J0DRo2+lq3UtbHaONbGx8jyzYu/kWSiLUc7X69OedoSwlmsGACQteki2o/b0uKTf84Ei+QEjGUquGJU+LETmo2IP55I+KuyZE6+zIiiegm25jgPDkrqlw2UrJiLCjUg4VhTdjF9/AUmT5tJbhZUGGx1/l0bGr+44ea7PmB3DELAgMBAAGjgf0wgfowDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUS/0UJYkd58Fwg9f2nxEcJU4Z7q4wgcoGA1UdIwSBwjCBv4AUS/0UJYkd58Fwg9f2nxEcJU4Z7q6hgZukgZgwgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJGTDEUMBIGA1UEBxMLV2ludGVyIFBhcmsxETAPBgNVBAoTCElQVmFuaXNoMRUwEwYDVQQLEwxJUFZhbmlzaCBWUE4xFDASBgNVBAMTC0lQVmFuaXNoIENBMSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QGlwdmFuaXNoLmNvbYIJAMYKzSS8uPKDMA0GCSqGSIb3DQEBDQUAA4IBAQCc9JV7IR8BfBrF/BQTXg0SZMZyyMAxR2jfW9qMHKSeJuZVVjfHiqoynEgBCNbn71wZWv3OF/Thu9BJ4GiYJ2Bc9nIa90D1NGYgiOVYLGXfUUqy5FgfrsWh0Go5oYm9l7W9pWfIifwsaZynkY0rTIHn32FF0H3+wZrGrEUzVL6qi+KD8iR3cBbLT+xUzulMTBp4JYaQnxpV4fZNS0ZsNrWKFWz4Iz1SSBcsnvUhfWs1aKx4yOJQx33Pc+KwpUI+meTlMjoh+AoTriooKU2MbOqLQl32y3pR0MP3fX4HDVFRylxdckEc+VryGNHQLUJiIBKBCORih/YiRhtEhpoBxmkw"}, //nolint:lll
MssFix: 1320,
ExtraLines: []string{
"comp-lzo", // Explicitly disable compression
},
}
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
}

View File

@@ -407,7 +407,7 @@ func bindPort(ctx context.Context, client *http.Client, apiIPAddress netip.Addr,
// replaceInErr is used to remove sensitive information from errors.
func replaceInErr(err error, substitutions map[string]string) error {
s := replaceInString(err.Error(), substitutions)
return errors.New(s) //nolint:goerr113
return errors.New(s) //nolint:err113
}
// replaceInString is used to remove sensitive information.

View File

@@ -20,8 +20,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
Ciphers: []string{openvpn.AES256cbc},
Auth: openvpn.SHA512,
CAs: []string{
"MIID7jCCA1CgAwIBAgIQQTT3w3N+5i8OMfe565xaSjAKBggqhkjOPQQDBDCBojELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xGjAYBgNVBAsMEUtlZXBTb2xpZCBSb290IENBMRowGAYDVQQDDBFLZWVwU29saWQgUm9vdCBDQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5Aa2VlcHNvbGlkLmNvbTAeFw0yMDA0MDExNjI3MTRaFw0yNTAzMzExNjI3MTRaMIGgMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEVMBMGA1UECwwMS2VlcFNvbGlkIENBMR0wGwYDVQQDDBRPcGVuVlBOIFNlcnZlciBTdWJDQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5Aa2VlcHNvbGlkLmNvbTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAR9nmoZUraRSSPUhYwIQBLSx+phJdIlqU7F7Hszh95ivnWYkwuizKLaUYy6lSISDohlUtQl9URBlRrGroVctOGlOAdpL2ARTljw5gmUcaavc5cvLiAV7fPJ7BFUgVxInmaVcaMlDwGgKLxmjU2Fw85VLROHbWQjYc93x/BTSFcYO/np4o4IBIzCCAR8wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUrUCjH8xe37lJihyzpqjWwxxNOiswgeIGA1UdIwSB2jCB14AU/LRRnTRaEbxct895Pk9DoymNQIqhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb22CFEssZFYAz8WhYnIDxLeDgKTLD8p2MAsGA1UdDwQEAwIBBjAKBggqhkjOPQQDBAOBiwAwgYcCQgGuK8UNnpE8k8hAamnT9gxCSs5APqrgmdLe6BxYSz7AptpF2/MPzLFsXgj4YxC6vJP8Rs8e3Hw9VJ7DF0aYgu8DvQJBeyFWjRnk8kmu2zEU+wF9fkvN9AJ7v0xF0iEaFVsdPKv6sJQP1sAL+AIepJQ7TYvh9Q9G/WaRCfItCtcOAEz3SKA=", //nolint:lll
"MIID9zCCA1igAwIBAgIUSyxkVgDPxaFicgPEt4OApMsPynYwCgYIKoZIzj0EAwQwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb20wIBcNMTkxMjMxMTY1NzMyWhgPMjA1NzA1MTUxNjU3MzJaMIGiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEaMBgGA1UECwwRS2VlcFNvbGlkIFJvb3QgQ0ExGjAYBgNVBAMMEUtlZXBTb2xpZCBSb290IENBMSIwIAYJKoZIhvcNAQkBFhNhZG1pbkBrZWVwc29saWQuY29tMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBlmcBvPVcV7mVgiUVkO9Dh8b4Hdw/eyU8OLWJ0qyRiqI1q0ad1cxgi6asy33xwilrMkRxhArDSfB87zpUpUboTEMBSf9n+dCoGRncGfW9G+8IvhzPY3Z3nzVHBGhoKlN1jsCuKzzpjGawqTAeCkJNBPQNd75Dp6Tgl198bAowD+iPX3WjggEjMIIBHzAdBgNVHQ4EFgQU/LRRnTRaEbxct895Pk9DoymNQIowgeIGA1UdIwSB2jCB14AU/LRRnTRaEbxct895Pk9DoymNQIqhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMRowGAYDVQQLDBFLZWVwU29saWQgUm9vdCBDQTEaMBgGA1UEAwwRS2VlcFNvbGlkIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQGtlZXBzb2xpZC5jb22CFEssZFYAz8WhYnIDxLeDgKTLD8p2MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMAoGCCqGSM49BAMEA4GMADCBiAJCAbDIYRjZYaDwMf8Gq4udKVS4aWtZt73lVulCVmr951tQ9J2Dzh4OEQZvU5+M688o2N/fVxNQoxwm/NsiJxpc/prQAkIBiRbrcEGvalu9h6UqE6yAXe0JZcF5xn/BIe5XygglOput4kvZKLKtIqPe2bwBmL/dqq6XDL7s5QaTWPo5MtpzGjA=", //nolint:lll
"MIIECjCCA2ygAwIBAgIRAJ/aLZu0PCO7LlOTcPQE9UwwCgYIKoZIzj0EAwQwgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wHhcNMjUwMzMxMTQ0OTU4WhcNMzAwNjEzMTQ0OTU4WjCBqTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xHTAbBgNVBAsMFEtlZXBTb2xpZCBPcGVuVlBOIENBMR0wGwYDVQQDDBRLZWVwU29saWQgT3BlblZQTiBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAEHfJRyn9MZ7HQctQULIxVUNFFw+tWetokml5PvIsS1i3mM4NQnj0HHL5zCCQRKUmSiiWtGvbGlsHEWX/hz+NiVoQGjMqBD2ykdLimiFrceonIofEBZW8to6jTjG3wmJkRykDqsuLyBLUKGc2F5dR3YFGgwyDoRz0NaAYI+qgqWfE+cVaOCASwwggEoMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFB4IhTj1gStDx+fNq+ubBcr+lEbwMIHrBgNVHSMEgeMwgeCAFOEcFx6OcN8T1R8lTdCLhFlYuk5joYGxpIGuMIGrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRcwFQYDVQQKDA5LZWVwU29saWQgSW5jLjEeMBwGA1UECwwVS2VlcFNvbGlkIFZQTiBSb290IENBMR4wHAYDVQQDDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExIzAhBgkqhkiG9w0BCQEWFGFkbWluc0BrZWVwc29saWQuY29tghRnfb8jJuxu5dJzLm5ZdurkedrxzjALBgNVHQ8EBAMCAQYwCgYIKoZIzj0EAwQDgYsAMIGHAkIBg8Cdu474VlljCoP8WEr6xErKL6Bygy5+SO1Ey0Uu3B7q8R22F0EWvrOmqmyNZ3oRyqhpUGaEBqB2aqDGT7u7wGsCQUP3nyMlDbXqCF05byMbhQrBsCz1nyqDNnfzM2uGmT09XwWXGCYTIGdynyJJLzdOlpf3T19ZLvqLSf6Kvq45u6si", //nolint:lll
"MIIEEDCCA3GgAwIBAgIUZ32/IybsbuXScy5uWXbq5Hna8c4wCgYIKoZIzj0EAwQwgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb20wHhcNMjUwMzMxMTQ0NTUzWhcNMzUwODI2MTQ0NTUzWjCBqzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9yazEXMBUGA1UECgwOS2VlcFNvbGlkIEluYy4xHjAcBgNVBAsMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEeMBwGA1UEAwwVS2VlcFNvbGlkIFZQTiBSb290IENBMSMwIQYJKoZIhvcNAQkBFhRhZG1pbnNAa2VlcHNvbGlkLmNvbTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAN77xqCz3wrFDnRMtggwScgvO6wPFZYECTUu5WW0JaowgmuIgo+BiQQyTeUzJEICulc1Hg7EaUEV+z8jsSrB+4/EAWazn/ufWOx/51fa5FCv4YooCbgLPb1CzYDuTc7MUR5PLQ88o3W01wCCgT8RoNH8uChyPBLUBh2f4rUfpzl20Bqdo4IBLDCCASgwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQU4RwXHo5w3xPVHyVN0IuEWVi6TmMwgesGA1UdIwSB4zCB4IAU4RwXHo5w3xPVHyVN0IuEWVi6TmOhgbGkga4wgasxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlvcmsxFzAVBgNVBAoMDktlZXBTb2xpZCBJbmMuMR4wHAYDVQQLDBVLZWVwU29saWQgVlBOIFJvb3QgQ0ExHjAcBgNVBAMMFUtlZXBTb2xpZCBWUE4gUm9vdCBDQTEjMCEGCSqGSIb3DQEJARYUYWRtaW5zQGtlZXBzb2xpZC5jb22CFGd9vyMm7G7l0nMubll26uR52vHOMAsGA1UdDwQEAwIBBjAKBggqhkjOPQQDBAOBjAAwgYgCQgCZtqE+wXwH0ixjWafX3SClp8O3bYeyB/7jbzf8MprXRYBVQ8JjvugjaZTvX82Uy++LaN3oHqK+NUhJUdfZx/eIuQJCAad7HpsKyTYuUUkgAgWXJma4MstxyO9PVRNYozi1oc45Z8deSvwy404n3u1kY5QXLZQaaMY7m2pF+ECs4WkKCh5s", //nolint:lll
},
ExtraLines: []string{
"route-metric 1",

View File

@@ -36,7 +36,7 @@ func FetchMultiInfo(ctx context.Context, fetcher InfoFetcher, ips []netip.Addr)
}
results = make([]models.PublicIP, len(ips))
for range len(ips) {
for range ips {
aResult := <-resultsCh
if aResult.err != nil {
if err == nil {

View File

@@ -114,6 +114,11 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
continue
}
if !*l.settings.Enabled {
singleRunResult <- nil
continue
}
result, err := l.fetcher.FetchInfo(singleRunCtx, netip.Addr{})
if err != nil {
err = fmt.Errorf("fetching information: %w", err)

View File

@@ -26,7 +26,7 @@ type dnsHandler struct {
func (h *dnsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.RequestURI = strings.TrimPrefix(r.RequestURI, "/dns")
switch r.RequestURI {
case "/status": //nolint:goconst
case "/status":
switch r.Method {
case http.MethodGet:
h.getStatus(w)

View File

@@ -1,86 +0,0 @@
package socks5
import "fmt"
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
type authMethod byte
const (
authNotRequired authMethod = 0
authGssapi authMethod = 1
authUsernamePassword authMethod = 2
authNotAcceptable authMethod = 255
)
func (a authMethod) String() string {
switch a {
case authNotRequired:
return "no authentication required"
case authGssapi:
return "GSSAPI"
case authUsernamePassword:
return "username/password"
case authNotAcceptable:
return "no acceptable methods"
default:
return fmt.Sprintf("unknown method (%d)", a)
}
}
// Subnegotiation version
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
const (
authUsernamePasswordSubNegotiation1 byte = 1
)
// SOCKS versions.
const (
socks5Version byte = 5
)
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4
type cmdType byte
const (
connect cmdType = 1
bind cmdType = 2
udpAssociate cmdType = 3
)
func (c cmdType) String() string {
switch c {
case connect:
return "connect"
case bind:
return "bind"
case udpAssociate:
return "UDP associate"
default:
return fmt.Sprintf("unknown command (%d)", c)
}
}
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4 and
// https://datatracker.ietf.org/doc/html/rfc1928#section-5
type addrType byte
const (
ipv4 addrType = 1
domainName addrType = 3
ipv6 addrType = 4
)
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
type replyCode byte
const (
succeeded replyCode = iota
generalServerFailure
connectionNotAllowedByRuleset
networkUnreachable
hostUnreachable
connectionRefused
ttlExpired
commandNotSupported
addressTypeNotSupported
)

View File

@@ -1,6 +0,0 @@
package socks5
type Logger interface {
Infof(format string, a ...interface{})
Warnf(format string, a ...interface{})
}

View File

@@ -1,103 +0,0 @@
package socks5
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
)
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
func (c *socksConn) encodeFailedResponse(writer io.Writer, socksVersion byte, reply replyCode) {
_, err := writer.Write([]byte{
socksVersion,
byte(reply),
0, // RSV byte
// TODO do we need to set the bind addr type to 0??
})
if err != nil {
c.logger.Warnf("failed writing failed response: %s", err)
}
}
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
func (c *socksConn) encodeSuccessResponse(writer io.Writer, socksVersion byte,
reply replyCode, bindAddrType addrType, bindAddress string,
bindPort uint16) (err error) {
bindData, err := encodeBindData(bindAddrType, bindAddress, bindPort)
if err != nil { // TODO encode with below block if this changes
return err
}
const initialPacketLength = 3
capacity := initialPacketLength + len(bindData)
packet := make([]byte, initialPacketLength, capacity)
packet[0] = socksVersion
packet[1] = byte(reply)
packet[2] = 0 // RSV byte
packet = append(packet, bindData...)
_, err = writer.Write(packet)
if err != nil {
c.logger.Warnf("failed writing success response: %s", err)
}
return nil
}
var (
ErrIPVersionUnexpected = errors.New("ip version is unexpected")
ErrDomainNameTooLong = errors.New("domain name is too long")
)
func encodeBindData(addrType addrType, address string, port uint16) (
data []byte, err error) {
capacity := bindDataLength(addrType, address)
data = make([]byte, 0, capacity)
data = append(data, byte(addrType))
switch addrType {
case ipv4, ipv6:
ip, err := netip.ParseAddr(address)
if err != nil {
return nil, fmt.Errorf("parsing IP address: %w", err)
}
switch {
case addrType == ipv4 && !ip.Is4():
return nil, fmt.Errorf("%w: expected IPv4 for %s", ErrIPVersionUnexpected, ip)
case addrType == ipv6 && !ip.Is6():
return nil, fmt.Errorf("%w: expected IPv6 for %s", ErrIPVersionUnexpected, ip)
}
data = append(data, ip.AsSlice()...)
case domainName:
const maxDomainNameLength = 255
if len(address) > maxDomainNameLength {
return nil, fmt.Errorf("%w: %s", ErrDomainNameTooLong, address)
}
data = append(data, byte(len(address)))
data = append(data, []byte(address)...)
default:
panic(fmt.Sprintf("unsupported address type %d", addrType))
}
data = binary.BigEndian.AppendUint16(data, port)
return data, nil
}
func bindDataLength(addrType addrType, address string) (maxLength int) {
maxLength++ // address type
switch addrType {
case ipv4:
maxLength += net.IPv4len
case domainName:
maxLength++ // domain name length
maxLength += len([]byte(address))
case ipv6:
maxLength += net.IPv6len
default:
panic("unsupported address type: " + fmt.Sprint(addrType))
}
maxLength += 2 // port
return maxLength
}

View File

@@ -1,105 +0,0 @@
package socks5
import (
"context"
"fmt"
"net"
"sync"
"sync/atomic"
)
type Server struct {
username string
password string
address string
logger Logger
// internal fields
listener net.Listener
listening atomic.Bool
socksConnCtx context.Context //nolint:containedctx
socksConnCancel context.CancelFunc
done <-chan struct{}
stopping atomic.Bool
}
func New(settings Settings) *Server {
return &Server{
username: settings.Username,
password: settings.Password,
address: settings.Address,
logger: settings.Logger,
}
}
func (s *Server) Start(_ context.Context) (runErr <-chan error, err error) {
s.listener, err = net.Listen("tcp", s.address)
if err != nil {
return nil, fmt.Errorf("listening on %s: %w", s.address, err)
}
s.listening.Store(true)
s.socksConnCtx, s.socksConnCancel = context.WithCancel(context.Background())
ready := make(chan struct{})
runErrCh := make(chan error)
runErr = runErrCh
done := make(chan struct{})
s.done = done
go s.runServer(ready, runErrCh, done)
<-ready
return runErr, nil
}
func (s *Server) runServer(ready chan<- struct{},
runErrCh chan<- error, done chan<- struct{}) {
close(ready)
defer close(done)
wg := new(sync.WaitGroup)
defer wg.Wait()
dialer := &net.Dialer{}
for {
connection, err := s.listener.Accept()
if err != nil {
if !s.stopping.Load() {
_ = s.Stop()
runErrCh <- fmt.Errorf("accepting connection: %w", err)
}
return
}
wg.Add(1)
go func(ctx context.Context, connection net.Conn,
dialer *net.Dialer, wg *sync.WaitGroup) {
defer wg.Done()
socksConn := &socksConn{
dialer: dialer,
username: s.username,
password: s.password,
clientConn: connection,
logger: s.logger,
}
err := socksConn.run(ctx)
if err != nil {
s.logger.Infof("running socks connection: %s", err)
}
}(s.socksConnCtx, connection, dialer, wg)
}
}
func (s *Server) Stop() (err error) {
s.stopping.Store(true)
s.listening.Store(false)
err = s.listener.Close()
s.socksConnCancel() // stop ongoing socks connections
<-s.done // wait for run goroutine to finish
s.stopping.Store(false)
return err
}
func (s *Server) listeningAddress() net.Addr {
if s.listening.Load() {
return s.listener.Addr()
}
return nil
}

View File

@@ -1,8 +0,0 @@
package socks5
type Settings struct {
Username string
Password string
Address string
Logger Logger
}

View File

@@ -1,283 +0,0 @@
package socks5
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"strings"
)
type socksConn struct {
// Injected fields
dialer *net.Dialer
username string
password string
clientConn net.Conn
logger Logger
}
func (c *socksConn) closeClientConn(ctxErr error) {
err := c.clientConn.Close()
if err != nil && ctxErr == nil {
c.logger.Warnf("closing client connection: %s", err)
}
}
func (c *socksConn) run(ctx context.Context) error {
authMethod := authNotRequired
if c.username != "" || c.password != "" {
authMethod = authUsernamePassword
}
err := verifyFirstNegotiation(c.clientConn, authMethod)
if err != nil {
replyMethod := authMethod
if errors.Is(err, ErrNoMethodIdentifiers) || errors.Is(err, ErrNoValidMethodIdentifier) {
replyMethod = authNotAcceptable
}
_, writeErr := c.clientConn.Write([]byte{socks5Version, byte(replyMethod)})
if writeErr != nil {
c.logger.Warnf("failed writing first negotiation reply: %s", writeErr)
}
c.closeClientConn(ctx.Err())
return fmt.Errorf("verifying first negotiation: %w", err)
}
_, err = c.clientConn.Write([]byte{socks5Version, byte(authMethod)})
if err != nil {
c.closeClientConn(ctx.Err())
return fmt.Errorf("writing first negotiation reply: %w", err)
}
switch authMethod {
case authNotRequired, authNotAcceptable:
case authGssapi:
panic("not implemented")
// TODO
case authUsernamePassword:
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
err = usernamePasswordSubnegotiate(c.clientConn, c.username, c.password)
if err != nil {
// If the server returns a `failure' (STATUS value other than X'00') status,
// it MUST close the connection.
c.closeClientConn(ctx.Err())
return fmt.Errorf("subnegotiating username and password: %w", err)
}
default:
panic(fmt.Sprintf("unimplemented auth method %d", authMethod))
}
err = c.handleRequest(ctx)
c.closeClientConn(ctx.Err())
if err != nil {
return fmt.Errorf("handling request: %w", err)
}
return nil
}
var (
ErrCommandNotSupported = errors.New("command not supported")
)
func (c *socksConn) handleRequest(ctx context.Context) error {
const socksVersion = socks5Version
request, err := decodeRequest(c.clientConn, socksVersion)
if err != nil {
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
return err
}
if request.command != connect {
c.encodeFailedResponse(c.clientConn, socksVersion, commandNotSupported)
return fmt.Errorf("%w: %s", ErrCommandNotSupported, request.command)
}
destinationAddress := net.JoinHostPort(request.destination, fmt.Sprint(request.port))
destinationConn, err := c.dialer.DialContext(ctx, "tcp", destinationAddress)
if err != nil {
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
return err
}
defer destinationConn.Close()
destinationServerAddress := destinationConn.LocalAddr().String()
destinationAddr, destinationPortStr, err := net.SplitHostPort(destinationServerAddress)
fmt.Println("===", destinationServerAddress)
if err != nil {
return err
}
destinationPort, err := strconv.Atoi(destinationPortStr)
if err != nil {
return err
}
var bindAddrType addrType
if ip := net.ParseIP(destinationAddr); ip != nil {
if ip.To4() != nil {
bindAddrType = ipv4
} else {
bindAddrType = ipv6
}
} else {
bindAddrType = domainName
}
err = c.encodeSuccessResponse(c.clientConn, socksVersion, succeeded, bindAddrType,
destinationAddr, uint16(destinationPort))
if err != nil {
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
return fmt.Errorf("writing successful %s response: %w", request.command, err)
}
errc := make(chan error)
go func() {
_, err := io.Copy(c.clientConn, destinationConn)
if err != nil {
err = fmt.Errorf("from backend to client: %w", err)
}
errc <- err
}()
go func() {
_, err := io.Copy(destinationConn, c.clientConn)
if err != nil {
err = fmt.Errorf("from client to backend: %w", err)
}
errc <- err
}()
select {
case err := <-errc:
return err
case <-ctx.Done():
_ = destinationConn.Close()
_ = c.clientConn.Close()
return nil
}
}
var (
ErrVersionNotSupported = errors.New("version not supported")
ErrNoMethodIdentifiers = errors.New("no method identifiers")
)
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
func verifyFirstNegotiation(reader io.Reader, requiredMethod authMethod) error {
const headerLength = 2 // version + nMethods bytes
header := make([]byte, headerLength)
_, err := io.ReadFull(reader, header[:])
if err != nil {
return fmt.Errorf("reading header: %w", err)
}
if header[0] != socks5Version {
return fmt.Errorf("%w: %d", ErrVersionNotSupported, header[0])
}
nMethods := header[1]
if nMethods == 0 {
return fmt.Errorf("%w", ErrNoMethodIdentifiers)
}
methodIdentifiers := make([]byte, nMethods)
_, err = io.ReadFull(reader, methodIdentifiers)
if err != nil {
return fmt.Errorf("reading method identifiers: %w", err)
}
for _, methodIdentifier := range methodIdentifiers {
if methodIdentifier == byte(requiredMethod) {
return nil
}
}
return makeNoAcceptableMethodError(requiredMethod, methodIdentifiers)
}
var (
ErrNoValidMethodIdentifier = errors.New("no valid method identifier")
)
func makeNoAcceptableMethodError(requiredAuthMethod authMethod, methodIdentifiers []byte) error {
methodNames := make([]string, len(methodIdentifiers))
for i, methodIdentifier := range methodIdentifiers {
methodNames[i] = fmt.Sprintf("%q", authMethod(methodIdentifier))
}
return fmt.Errorf("%w: none of %s matches %s",
ErrNoValidMethodIdentifier, strings.Join(methodNames, ", "),
requiredAuthMethod)
}
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4
type request struct {
command cmdType
destination string
port uint16
addressType addrType
}
var (
ErrRequestSocksVersionMismatch = errors.New("request SOCKS version mismatch")
ErrAddressTypeNotSupported = errors.New("address type not supported")
)
func decodeRequest(reader io.Reader, expectedVersion byte) (req request, err error) {
const headerLength = 4
header := [headerLength]byte{}
_, err = io.ReadFull(reader, header[:])
if err != nil {
return request{}, fmt.Errorf("reading header: %w", err)
}
version := header[0]
if header[0] != expectedVersion {
return request{}, fmt.Errorf("%w: expected %d and got %d",
ErrRequestSocksVersionMismatch, expectedVersion, version)
}
req.command = cmdType(header[1])
// header[2] is RSV byte
req.addressType = addrType(header[3])
switch req.addressType {
case ipv4:
var ip [4]byte
_, err = io.ReadFull(reader, ip[:])
if err != nil {
return request{}, fmt.Errorf("reading IPv4 address: %w", err)
}
req.destination = netip.AddrFrom4(ip).String()
case ipv6:
var ip [16]byte
_, err = io.ReadFull(reader, ip[:])
if err != nil {
return request{}, fmt.Errorf("reading IPv6 address: %w", err)
}
req.destination = netip.AddrFrom16(ip).String()
case domainName:
var header [1]byte
_, err = io.ReadFull(reader, header[:])
if err != nil {
return request{}, fmt.Errorf("reading domain name header: %w", err)
}
domainName := make([]byte, header[0])
_, err = io.ReadFull(reader, domainName)
if err != nil {
return request{}, fmt.Errorf("reading domain name bytes: %w", err)
}
req.destination = string(domainName)
default:
return request{}, fmt.Errorf("%w: %d", ErrAddressTypeNotSupported, req.addressType)
}
var portBytes [2]byte
_, err = io.ReadFull(reader, portBytes[:])
if err != nil {
return request{}, fmt.Errorf("reading port: %w", err)
}
req.port = binary.BigEndian.Uint16(portBytes[:])
return req, nil
}

View File

@@ -1,175 +0,0 @@
package socks5
import (
"context"
"fmt"
"io"
"net"
"testing"
"time"
"github.com/qdm12/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/proxy"
)
func Test(t *testing.T) {
server := New(Settings{
Username: "test",
Password: "test",
Address: ":8000",
Logger: log.New(),
})
runErr, startErr := server.Start(context.Background())
require.NoError(t, startErr)
select {
case err := <-runErr:
require.NoError(t, err)
default:
}
t.Log("SlEEPING")
time.Sleep(15 * time.Second)
t.Log("Done sleeping")
err := server.Stop()
require.NoError(t, err)
}
func backendServer(listener net.Listener) {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
conn.Write([]byte("Test"))
conn.Close()
listener.Close()
}
func TestRead(t *testing.T) {
// backend server which we'll use SOCKS5 to connect to
listener, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
backendServerPort := listener.Addr().(*net.TCPAddr).Port
go backendServer(listener)
// SOCKS5 server
server := New(Settings{
Address: ":0",
})
_, err = server.Start(context.Background())
require.NoError(t, err)
t.Cleanup(func() {
err = server.Stop()
assert.NoError(t, err)
})
socks5Port := server.listeningAddress().(*net.TCPAddr).Port
addr := fmt.Sprintf("localhost:%d", socks5Port)
socksDialer, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
if err != nil {
t.Fatal(err)
}
addr = fmt.Sprintf("localhost:%d", backendServerPort)
conn, err := socksDialer.Dial("tcp", addr)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 4)
_, err = io.ReadFull(conn, buf)
if err != nil {
t.Fatal(err)
}
if string(buf) != "Test" {
t.Fatalf("got: %q want: Test", buf)
}
err = conn.Close()
if err != nil {
t.Fatal(err)
}
}
func TestReadPassword(t *testing.T) {
// backend server which we'll use SOCKS5 to connect to
ln, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
backendServerPort := ln.Addr().(*net.TCPAddr).Port
go backendServer(ln)
auth := &proxy.Auth{User: "foo", Password: "bar"}
server := Server{
logger: log.New(),
username: auth.User,
password: auth.Password,
address: ":0",
}
_, err = server.Start(context.Background())
require.NoError(t, err)
t.Cleanup(func() {
err = server.Stop()
assert.NoError(t, err)
})
addr := fmt.Sprintf("localhost:%d", server.listeningAddress().(*net.TCPAddr).Port)
if d, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct); err != nil {
t.Fatal(err)
} else {
if _, err := d.Dial("tcp", addr); err == nil {
t.Fatal("expected no-auth dial error")
}
}
badPwd := &proxy.Auth{User: "foo", Password: "not right"}
if d, err := proxy.SOCKS5("tcp", addr, badPwd, proxy.Direct); err != nil {
t.Fatal(err)
} else {
if _, err := d.Dial("tcp", addr); err == nil {
t.Fatal("expected bad password dial error")
}
}
badUsr := &proxy.Auth{User: "not right", Password: "bar"}
if d, err := proxy.SOCKS5("tcp", addr, badUsr, proxy.Direct); err != nil {
t.Fatal(err)
} else {
if _, err := d.Dial("tcp", addr); err == nil {
t.Fatal("expected bad username dial error")
}
}
socksDialer, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct)
if err != nil {
t.Fatal(err)
}
addr = fmt.Sprintf("localhost:%d", backendServerPort)
conn, err := socksDialer.Dial("tcp", addr)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 4)
if _, err := io.ReadFull(conn, buf); err != nil {
t.Fatal(err)
}
if string(buf) != "Test" {
t.Fatalf("got: %q want: Test", buf)
}
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}

View File

@@ -1,69 +0,0 @@
package socks5
import (
"errors"
"fmt"
"io"
)
var (
ErrSubnegotiationVersionNotSupported = errors.New("subnegotiation version not supported")
ErrUsernameNotValid = errors.New("username not valid")
ErrPasswordNotValid = errors.New("password not valid")
)
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
func usernamePasswordSubnegotiate(conn io.ReadWriter, username, password string) (err error) {
status := byte(1)
const defaultVersion = byte(1)
const headerLength = 2
var header [headerLength]byte
_, err = io.ReadFull(conn, header[:])
if err != nil {
_, _ = conn.Write([]byte{defaultVersion, status})
return fmt.Errorf("reading header: %w", err)
}
if header[0] != authUsernamePasswordSubNegotiation1 {
_, _ = conn.Write([]byte{defaultVersion, status})
return fmt.Errorf("%w: %d", ErrSubnegotiationVersionNotSupported, header[0])
}
version := header[0]
usernameBytes := make([]byte, header[1])
_, err = io.ReadFull(conn, usernameBytes)
if err != nil {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("reading username bytes: %w", err)
} else if username != string(usernameBytes) {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("%w: %s", ErrUsernameNotValid, string(usernameBytes))
}
const passwordHeaderLength = 1
passwordHeader := make([]byte, passwordHeaderLength)
_, err = io.ReadFull(conn, passwordHeader[:])
if err != nil {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("reading password length: %w", err)
}
passwordBytes := make([]byte, passwordHeader[0])
_, err = io.ReadFull(conn, passwordBytes)
if err != nil {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("reading password bytes: %w", err)
} else if password != string(passwordBytes) {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("%w: %s", ErrPasswordNotValid, string(passwordBytes))
}
status = 0
_, err = conn.Write([]byte{version, status})
if err != nil {
return fmt.Errorf("writing success status: %w", err)
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -99,3 +99,13 @@ type CmdStarter interface {
stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error)
}
type HealthChecker interface {
SetConfig(tlsDialAddr string, icmpTarget netip.Addr)
Start(ctx context.Context) (runError <-chan error, err error)
Stop() error
}
type HealthServer interface {
SetError(err error)
}

View File

@@ -17,6 +17,9 @@ type Loop struct {
state *state.State
providers Providers
storage Storage
healthSettings settings.Health
healthChecker HealthChecker
healthServer HealthServer
// Fixed parameters
buildInfo models.BuildInformation
versionInfo bool
@@ -49,7 +52,8 @@ const (
)
func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint16,
providers Providers, storage Storage, openvpnConf OpenVPN,
providers Providers, storage Storage, healthSettings settings.Health,
healthChecker HealthChecker, healthServer HealthServer, openvpnConf OpenVPN,
netLinker NetLinker, fw Firewall, routing Routing,
portForward PortForward, starter CmdStarter,
publicip PublicIPLoop, dnsLooper DNSLoop,
@@ -69,6 +73,9 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1
state: state,
providers: providers,
storage: storage,
healthSettings: healthSettings,
healthChecker: healthChecker,
healthServer: healthServer,
buildInfo: buildInfo,
versionInfo: versionInfo,
ipv6Supported: ipv6Supported,

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/provider"
)
@@ -14,39 +15,38 @@ import (
func setupOpenVPN(ctx context.Context, fw Firewall,
openvpnConf OpenVPN, providerConf provider.Provider,
settings settings.VPN, ipv6Supported bool, starter CmdStarter,
logger openvpn.Logger) (runner *openvpn.Runner, serverName string,
canPortForward bool, err error,
logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, err error,
) {
connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
if err != nil {
return nil, "", false, fmt.Errorf("finding a valid server connection: %w", err)
return nil, models.Connection{}, fmt.Errorf("finding a valid server connection: %w", err)
}
lines := providerConf.OpenVPNConfig(connection, settings.OpenVPN, ipv6Supported)
if err := openvpnConf.WriteConfig(lines); err != nil {
return nil, "", false, fmt.Errorf("writing configuration to file: %w", err)
return nil, models.Connection{}, fmt.Errorf("writing configuration to file: %w", err)
}
if *settings.OpenVPN.User != "" {
err := openvpnConf.WriteAuthFile(*settings.OpenVPN.User, *settings.OpenVPN.Password)
if err != nil {
return nil, "", false, fmt.Errorf("writing auth to file: %w", err)
return nil, models.Connection{}, fmt.Errorf("writing auth to file: %w", err)
}
}
if *settings.OpenVPN.KeyPassphrase != "" {
err := openvpnConf.WriteAskPassFile(*settings.OpenVPN.KeyPassphrase)
if err != nil {
return nil, "", false, fmt.Errorf("writing askpass file: %w", err)
return nil, models.Connection{}, fmt.Errorf("writing askpass file: %w", err)
}
}
if err := fw.SetVPNConnection(ctx, connection, settings.OpenVPN.Interface); err != nil {
return nil, "", false, fmt.Errorf("allowing VPN connection through firewall: %w", err)
return nil, models.Connection{}, fmt.Errorf("allowing VPN connection through firewall: %w", err)
}
runner = openvpn.NewRunner(settings.OpenVPN, starter, logger)
return runner, connection.ServerName, connection.PortForward, nil
return runner, connection, nil
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/log"
)
@@ -28,17 +29,17 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
var vpnRunner interface {
Run(ctx context.Context, waitError chan<- error, tunnelReady chan<- struct{})
}
var serverName, vpnInterface string
var canPortForward bool
var vpnInterface string
var connection models.Connection
var err error
subLogger := l.logger.New(log.SetComponent(settings.Type))
if settings.Type == vpn.OpenVPN {
vpnInterface = settings.OpenVPN.Interface
vpnRunner, serverName, canPortForward, err = setupOpenVPN(ctx, l.fw,
vpnRunner, connection, err = setupOpenVPN(ctx, l.fw,
l.openvpnConf, providerConf, settings, l.ipv6Supported, l.starter, subLogger)
} else { // Wireguard
vpnInterface = settings.Wireguard.Interface
vpnRunner, serverName, canPortForward, err = setupWireguard(ctx, l.netLinker, l.fw,
vpnRunner, connection, err = setupWireguard(ctx, l.netLinker, l.fw,
providerConf, settings, l.ipv6Supported, subLogger)
}
if err != nil {
@@ -46,8 +47,9 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
continue
}
tunnelUpData := tunnelUpData{
serverName: serverName,
canPortForward: canPortForward,
serverIP: connection.IP,
serverName: connection.ServerName,
canPortForward: connection.PortForward,
portForwarder: portForwarder,
vpnIntf: vpnInterface,
username: settings.Provider.PortForwarding.Username,
@@ -73,7 +75,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
for stayHere {
select {
case <-tunnelReady:
go l.onTunnelUp(openvpnCtx, tunnelUpData)
go l.onTunnelUp(openvpnCtx, ctx, tunnelUpData)
case <-ctx.Done():
l.cleanup()
openvpnCancel()

View File

@@ -2,6 +2,7 @@ package vpn
import (
"context"
"net/netip"
"github.com/qdm12/dns/v2/pkg/check"
"github.com/qdm12/gluetun/internal/constants"
@@ -9,6 +10,8 @@ import (
)
type tunnelUpData struct {
// Healthcheck
serverIP netip.Addr
// Port forwarding
vpnIntf string
serverName string // used for PIA
@@ -18,7 +21,7 @@ type tunnelUpData struct {
portForwarder PortForwarder
}
func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) {
func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
l.client.CloseIdleConnections()
for _, vpnPort := range l.vpnInputPorts {
@@ -28,6 +31,21 @@ func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) {
}
}
icmpTarget := l.healthSettings.ICMPTargetIP
if icmpTarget.IsUnspecified() {
icmpTarget = data.serverIP
}
l.healthChecker.SetConfig(l.healthSettings.TargetAddress, icmpTarget)
healthErrCh, err := l.healthChecker.Start(ctx)
l.healthServer.SetError(err)
if err != nil {
// Note this restart call must be done in a separate goroutine
// from the VPN loop goroutine.
l.restartVPN(loopCtx, err)
return
}
if *l.dnsLooper.GetSettings().DoT.Enabled {
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
} else {
@@ -37,7 +55,7 @@ func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) {
}
}
err := l.publicip.RunOnce(ctx)
err = l.publicip.RunOnce(ctx)
if err != nil {
l.logger.Error("getting public IP address information: " + err.Error())
}
@@ -56,4 +74,41 @@ func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) {
if err != nil {
l.logger.Error(err.Error())
}
l.collectHealthErrors(ctx, loopCtx, healthErrCh)
}
func (l *Loop) collectHealthErrors(ctx, loopCtx context.Context, healthErrCh <-chan error) {
var previousHealthErr error
for {
select {
case <-ctx.Done():
_ = l.healthChecker.Stop()
return
case healthErr := <-healthErrCh:
l.healthServer.SetError(healthErr)
if healthErr != nil {
if *l.healthSettings.RestartVPN {
// Note this restart call must be done in a separate goroutine
// from the VPN loop goroutine.
_ = l.healthChecker.Stop()
l.restartVPN(loopCtx, healthErr)
return
}
l.logger.Warnf("healthcheck failed: %s", healthErr)
l.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md")
} else if previousHealthErr != nil {
l.logger.Info("healthcheck passed successfully after previous failure(s)")
}
previousHealthErr = healthErr
}
}
}
func (l *Loop) restartVPN(ctx context.Context, healthErr error) {
l.logger.Warnf("restarting VPN because it failed to pass the healthcheck: %s", healthErr)
l.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md")
l.logger.Info("DO NOT OPEN AN ISSUE UNLESS YOU HAVE READ AND TRIED EVERY POSSIBLE SOLUTION")
_, _ = l.ApplyStatus(ctx, constants.Stopped)
_, _ = l.ApplyStatus(ctx, constants.Running)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/provider/utils"
"github.com/qdm12/gluetun/internal/wireguard"
@@ -16,11 +17,11 @@ import (
func setupWireguard(ctx context.Context, netlinker NetLinker,
fw Firewall, providerConf provider.Provider,
settings settings.VPN, ipv6Supported bool, logger wireguard.Logger) (
wireguarder *wireguard.Wireguard, serverName string, canPortForward bool, err error,
wireguarder *wireguard.Wireguard, connection models.Connection, err error,
) {
connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
if err != nil {
return nil, "", false, fmt.Errorf("finding a VPN server: %w", err)
return nil, models.Connection{}, fmt.Errorf("finding a VPN server: %w", err)
}
wireguardSettings := utils.BuildWireguardSettings(connection, settings.Wireguard, ipv6Supported)
@@ -31,13 +32,13 @@ func setupWireguard(ctx context.Context, netlinker NetLinker,
wireguarder, err = wireguard.New(wireguardSettings, netlinker, logger)
if err != nil {
return nil, "", false, fmt.Errorf("creating Wireguard: %w", err)
return nil, models.Connection{}, fmt.Errorf("creating Wireguard: %w", err)
}
err = fw.SetVPNConnection(ctx, connection, settings.Wireguard.Interface)
if err != nil {
return nil, "", false, fmt.Errorf("setting firewall: %w", err)
return nil, models.Connection{}, fmt.Errorf("setting firewall: %w", err)
}
return wireguarder, connection.ServerName, connection.PortForward, nil
return wireguarder, connection, nil
}

View File

@@ -32,9 +32,14 @@ func (w *Wireguard) addRoutes(link netlink.Link, destinations []netip.Prefix,
func (w *Wireguard) addRoute(link netlink.Link, dst netip.Prefix,
firewallMark uint32,
) (err error) {
family := netlink.FamilyV4
if dst.Addr().Is6() {
family = netlink.FamilyV6
}
route := netlink.Route{
LinkIndex: link.Index,
Dst: dst,
Family: family,
Table: int(firewallMark),
}

View File

@@ -37,6 +37,7 @@ func Test_Wireguard_addRoute(t *testing.T) {
expectedRoute: netlink.Route{
LinkIndex: linkIndex,
Dst: ipPrefix,
Family: netlink.FamilyV4,
Table: firewallMark,
},
},
@@ -49,6 +50,7 @@ func Test_Wireguard_addRoute(t *testing.T) {
expectedRoute: netlink.Route{
LinkIndex: linkIndex,
Dst: ipPrefix,
Family: netlink.FamilyV4,
Table: firewallMark,
},
routeAddErr: errDummy,

View File

@@ -2,6 +2,7 @@ package wireguard
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/netlink"
)
@@ -16,6 +17,10 @@ func (w *Wireguard) addRule(rulePriority int, firewallMark uint32,
rule.Table = int(firewallMark)
rule.Family = family
if err := w.netlink.RuleAdd(rule); err != nil {
if strings.HasSuffix(err.Error(), "file exists") {
w.logger.Info("if you are using Kubernetes, this may fix the error below: " +
"https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/kubernetes.md#adding-ipv6-rule--file-exists")
}
return nil, fmt.Errorf("adding %s: %w", rule, err)
}