Compare commits
74 Commits
v3.40.2
...
ivp6-level
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be935e70e6 | ||
|
|
5ca13021e7 | ||
|
|
dae44051f6 | ||
|
|
ffb0bec4da | ||
|
|
4d2b8787e0 | ||
|
|
d4831ad4a6 | ||
|
|
9e1b53a732 | ||
|
|
d0113849d6 | ||
|
|
7b25fdfee8 | ||
|
|
5ed6e82922 | ||
|
|
7dbd14df27 | ||
|
|
96d8b53338 | ||
|
|
2bd19640d9 | ||
|
|
1047508bd7 | ||
|
|
eb49306b80 | ||
|
|
43da9ddbb3 | ||
|
|
7fbc5c3c07 | ||
|
|
e03f545e07 | ||
|
|
942f1f2c0f | ||
|
|
baf566d7a5 | ||
|
|
6712adfe6b | ||
|
|
2e2e5f9df5 | ||
|
|
35e9b2365d | ||
|
|
b0b769d2c1 | ||
|
|
d3c7d3c7bc | ||
|
|
65f49ea012 | ||
|
|
5687555921 | ||
|
|
0fb75036a0 | ||
|
|
2b513dd43d | ||
|
|
687d9b4736 | ||
|
|
c70c2ef932 | ||
|
|
af3ada109b | ||
|
|
9d40564734 | ||
|
|
3734815ada | ||
|
|
b9cc5c1fdc | ||
|
|
c646ca5766 | ||
|
|
1394be5143 | ||
|
|
93442526f8 | ||
|
|
d85402050b | ||
|
|
b1c62cb525 | ||
|
|
fae64a297a | ||
|
|
6e2682a9ce | ||
|
|
555049f09c | ||
|
|
712f7c3d35 | ||
|
|
7a51c211cd | ||
|
|
c48189c1c4 | ||
|
|
9803fa1cfd | ||
|
|
cf756f561a | ||
|
|
a4021fedc3 | ||
|
|
31a36a9250 | ||
|
|
36fe349b70 | ||
|
|
3ef1cfd97c | ||
|
|
669feb45f1 | ||
|
|
85890520ab | ||
|
|
340016521e | ||
|
|
ef523df42c | ||
|
|
5306e3bab1 | ||
|
|
72a49afd2b | ||
|
|
9b8edbb81e | ||
|
|
a1554feb3f | ||
|
|
490410bf09 | ||
|
|
8c113f5268 | ||
|
|
075cbd5a0f | ||
|
|
d82df2b431 | ||
|
|
a09f8214d9 | ||
|
|
396e9c003e | ||
|
|
b0c4a28be6 | ||
|
|
85325e4a31 | ||
|
|
9933dd3ec5 | ||
|
|
13532c8b4b | ||
|
|
3926797295 | ||
|
|
febd3f784f | ||
|
|
61b053f0e1 | ||
|
|
8dae352ccc |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true,
|
||||
"ui.diagnostic.analyses": {
|
||||
"ST1000": false
|
||||
},
|
||||
"formatting.gofumpt": true,
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/provider.md
vendored
37
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -6,12 +6,35 @@ labels: ":bulb: New provider"
|
||||
|
||||
---
|
||||
|
||||
One of the following is required:
|
||||
Important notes:
|
||||
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
||||
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
||||
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
|
||||
## For Wireguard
|
||||
|
||||
Wireguard can be natively supported ONLY if:
|
||||
|
||||
- the `PrivateKey` field value is the same across all servers for one user account
|
||||
- the `Address` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `PublicKey` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `Endpoint` field value:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available
|
||||
- can be determined using a pattern, for example using country codes in hostnames
|
||||
|
||||
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
|
||||
|
||||
If **all** of these conditions are met, please provide an answer for each of them.
|
||||
|
||||
## For OpenVPN
|
||||
|
||||
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
|
||||
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
|
||||
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
|
||||
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
||||
you can only use a custom Openvpn configuration file.
|
||||
[The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.
|
||||
|
||||
12
.github/pull_request_template.md
vendored
Normal file
12
.github/pull_request_template.md
vendored
Normal 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/)
|
||||
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
@@ -66,6 +66,33 @@ jobs:
|
||||
- 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
|
||||
|
||||
2
.github/workflows/closed-issue.yml
vendored
2
.github/workflows/closed-issue.yml
vendored
@@ -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 }}
|
||||
|
||||
3
.github/workflows/configs/mlc-config.json
vendored
3
.github/workflows/configs/mlc-config.json
vendored
@@ -8,6 +8,7 @@
|
||||
"retryOn429": false,
|
||||
"fallbackRetryDelay": "30s",
|
||||
"aliveStatusCodes": [
|
||||
200
|
||||
200,
|
||||
429
|
||||
]
|
||||
}
|
||||
2
.github/workflows/labels.yml
vendored
2
.github/workflows/labels.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/markdown.yml
vendored
6
.github/workflows/markdown.yml
vendored
@@ -18,12 +18,12 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v18
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v20
|
||||
with:
|
||||
globs: "**.md"
|
||||
config: .markdownlint.json
|
||||
config: .markdownlint-cli2.jsonc
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
|
||||
2
.github/workflows/opened-issue.yml
vendored
2
.github/workflows/opened-issue.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -1,30 +1,70 @@
|
||||
linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
version: "2"
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- err113
|
||||
- containedctx
|
||||
- maintidx
|
||||
- path: "internal\\/server\\/.+\\.go"
|
||||
linters:
|
||||
- dupl
|
||||
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
|
||||
linters:
|
||||
- ireturn
|
||||
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
|
||||
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
||||
linters:
|
||||
- ireturn
|
||||
- source: "^\\/\\/ https\\:\\/\\/.+$"
|
||||
linters:
|
||||
- lll
|
||||
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$"
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- containedctx
|
||||
- dupl
|
||||
- err113
|
||||
- maintidx
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- dupl
|
||||
path: internal\/server\/.+\.go
|
||||
- linters:
|
||||
- ireturn
|
||||
text: returns interface \(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
|
||||
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
enable:
|
||||
# - cyclop
|
||||
# - errorlint
|
||||
@@ -45,7 +85,6 @@ linters:
|
||||
- exhaustive
|
||||
- fatcontext
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
@@ -54,9 +93,7 @@ linters:
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- gofumpt
|
||||
- goheader
|
||||
- goimports
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
@@ -89,7 +126,6 @@ linters:
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- tagalign
|
||||
- tenv
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
|
||||
9
.markdownlint-cli2.jsonc
Normal file
9
.markdownlint-cli2.jsonc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
},
|
||||
"ignores": [
|
||||
".github/pull_request_template.md"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"MD013": false
|
||||
}
|
||||
35
.vscode/launch.json
vendored
35
.vscode/launch.json
vendored
@@ -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
51
.vscode/tasks.json
vendored
Normal 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",
|
||||
},
|
||||
]
|
||||
}
|
||||
43
Dockerfile
43
Dockerfile
@@ -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 && \
|
||||
@@ -159,24 +159,27 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
FIREWALL_INPUT_PORTS= \
|
||||
FIREWALL_OUTBOUND_SUBNETS= \
|
||||
FIREWALL_DEBUG=off \
|
||||
# IPv6
|
||||
IPV6_CHECK_ADDRESS=[2606:4700::6810:84e5]:443 \
|
||||
# Logging
|
||||
LOG_LEVEL=info \
|
||||
# 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 \
|
||||
# DNS over TLS
|
||||
DOT=on \
|
||||
DOT_PROVIDERS=cloudflare \
|
||||
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
||||
DOT_CACHING=on \
|
||||
DOT_IPV6=off \
|
||||
HEALTH_ICMP_TARGET_IP=1.1.1.1 \
|
||||
HEALTH_RESTART_VPN=on \
|
||||
# DNS
|
||||
DNS_SERVER=on \
|
||||
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
||||
DNS_UPSTREAM_RESOLVERS=cloudflare \
|
||||
DNS_BLOCK_IPS= \
|
||||
DNS_BLOCK_IP_PREFIXES=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
||||
DNS_CACHING=on \
|
||||
DNS_UPSTREAM_IPV6=off \
|
||||
BLOCK_MALICIOUS=on \
|
||||
BLOCK_SURVEILLANCE=off \
|
||||
BLOCK_ADS=off \
|
||||
UNBLOCK= \
|
||||
DNS_UNBLOCK_HOSTNAMES= \
|
||||
DNS_UPDATE_PERIOD=24h \
|
||||
DNS_ADDRESS=127.0.0.1 \
|
||||
DNS_KEEP_NAMESERVER=off \
|
||||
@@ -204,8 +207,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
UPDATER_PERIOD=0 \
|
||||
UPDATER_MIN_RATIO=0.8 \
|
||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||
UPDATER_PROTONVPN_EMAIL= \
|
||||
UPDATER_PROTONVPN_PASSWORD= \
|
||||
# Public IP
|
||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||
PUBLICIP_ENABLED=on \
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
@@ -26,7 +26,6 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
[](https://github.com/qdm12/gluetun/issues)
|
||||
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||
[](https://github.com/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
33
ci/cmd/main.go
Normal 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
36
ci/go.mod
Normal 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
97
ci/go.sum
Normal 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
193
ci/internal/mullvad.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@@ -242,10 +243,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return err
|
||||
}
|
||||
|
||||
ipv6Supported, err := netLinker.IsIPv6Supported()
|
||||
ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel(ctx,
|
||||
allSettings.IPv6.CheckAddress, firewallConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
ipv6Supported := ipv6SupportLevel == netlink.IPv6Supported ||
|
||||
ipv6SupportLevel == netlink.IPv6Internet
|
||||
|
||||
err = allSettings.Validate(storage, ipv6Supported, logger)
|
||||
if err != nil {
|
||||
@@ -414,19 +418,25 @@ 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)
|
||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||
openvpnFileExtractor := extract.New()
|
||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||
openvpnFileExtractor, allSettings.Updater)
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
|
||||
|
||||
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,
|
||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6SupportLevel, allSettings.Firewall.VPNInputPorts,
|
||||
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))
|
||||
@@ -468,7 +478,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
logger.New(log.SetComponent("http server")),
|
||||
allSettings.ControlServer.AuthFilePath,
|
||||
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||
storage, ipv6Supported)
|
||||
storage, ipv6SupportLevel.IsSupported())
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up control server: %w", err)
|
||||
}
|
||||
@@ -477,12 +487,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),
|
||||
@@ -550,7 +554,9 @@ type netLinker interface {
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
IsIPv6Supported() (ok bool, err error)
|
||||
FindIPv6SupportLevel(ctx context.Context,
|
||||
checkAddress netip.AddrPort, firewall netlink.Firewall,
|
||||
) (level netlink.IPv6SupportLevel, err error)
|
||||
PatchLoggerLevel(level log.Level)
|
||||
}
|
||||
|
||||
|
||||
40
go.mod
40
go.mod
@@ -1,42 +1,37 @@
|
||||
module github.com/qdm12/gluetun
|
||||
|
||||
go 1.23
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-srp v0.0.7
|
||||
github.com/breml/rootcerts v0.2.19
|
||||
github.com/breml/rootcerts v0.3.3
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/klauspost/compress v1.17.11
|
||||
github.com/klauspost/compress v1.18.1
|
||||
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/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9
|
||||
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.10.0
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/vishvananda/netlink v1.2.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
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.31.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/text v0.22.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
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.0 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
@@ -47,19 +42,18 @@ require (
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/qdm12/goservices v0.1.0 // indirect
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // 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.33.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.11.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
|
||||
|
||||
104
go.sum
104
go.sum
@@ -1,23 +1,9 @@
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/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.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U=
|
||||
github.com/breml/rootcerts v0.2.19/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
|
||||
github.com/breml/rootcerts v0.3.3/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/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
@@ -30,8 +16,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -55,10 +41,8 @@ github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE9
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
@@ -69,10 +53,10 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
|
||||
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
|
||||
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9 h1:qDzRkHr6993jknNB/ZOCnZOyIG6bsZcl2MIfdeUd0kI=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||
@@ -89,83 +73,59 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/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.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.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=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.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=
|
||||
|
||||
14
internal/cli/noopfirewall.go
Normal file
14
internal/cli/noopfirewall.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type noopFirewall struct{}
|
||||
|
||||
func (f *noopFirewall) AcceptOutput(_ context.Context, _, _ string, _ netip.Addr,
|
||||
_ uint16, _ bool,
|
||||
) (err error) {
|
||||
return nil
|
||||
}
|
||||
@@ -7,4 +7,3 @@ func newNoopLogger() *noopLogger {
|
||||
}
|
||||
|
||||
func (l *noopLogger) Info(string) {}
|
||||
func (l *noopLogger) Warn(string) {}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
||||
"github.com/qdm12/gluetun/internal/provider"
|
||||
"github.com/qdm12/gluetun/internal/storage"
|
||||
@@ -40,7 +41,9 @@ type IPFetcher interface {
|
||||
}
|
||||
|
||||
type IPv6Checker interface {
|
||||
IsIPv6Supported() (supported bool, err error)
|
||||
FindIPv6SupportLevel(ctx context.Context,
|
||||
checkAddress netip.AddrPort, firewall netlink.Firewall,
|
||||
) (level netlink.IPv6SupportLevel, err error)
|
||||
}
|
||||
|
||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
@@ -58,12 +61,14 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
}
|
||||
allSettings.SetDefaults()
|
||||
|
||||
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
||||
ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel(context.Background(),
|
||||
allSettings.IPv6.CheckAddress, &noopFirewall{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
|
||||
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
|
||||
err = allSettings.Validate(storage, ipv6SupportLevel.IsSupported(), logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -76,16 +81,16 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, warner, client,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
||||
connection, err := providerConf.GetConnection(
|
||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||
allSettings.VPN.Provider.ServerSelection, ipv6SupportLevel == netlink.IPv6Internet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := providerConf.OpenVPNConfig(connection,
|
||||
allSettings.VPN.OpenVPN, ipv6Supported)
|
||||
allSettings.VPN.OpenVPN, ipv6SupportLevel.IsSupported())
|
||||
|
||||
fmt.Println(strings.Join(lines, "\n"))
|
||||
return nil
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,8 +24,6 @@ import (
|
||||
var (
|
||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||
ErrUsernameMissing = errors.New("username is required for this provider")
|
||||
ErrPasswordMissing = errors.New("password is required for this provider")
|
||||
)
|
||||
|
||||
type UpdaterLogger interface {
|
||||
@@ -38,7 +35,7 @@ type UpdaterLogger interface {
|
||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||
options := settings.Updater{}
|
||||
var endUserMode, maintainerMode, updateAll bool
|
||||
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
||||
var csvProviders, ipToken string
|
||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||
@@ -50,10 +47,6 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
||||
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
||||
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
||||
flagSet.StringVar(&protonUsername, "proton-username", "",
|
||||
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
|
||||
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
|
||||
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
||||
if err := flagSet.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -71,16 +64,6 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
options.Providers = strings.Split(csvProviders, ",")
|
||||
}
|
||||
|
||||
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||
if protonEmail == "" && protonUsername != "" {
|
||||
protonEmail = protonUsername + "@protonmail.com"
|
||||
logger.Warn("use -proton-email instead of -proton-username in the future. " +
|
||||
"This assumes the email is " + protonEmail + " and may not work.")
|
||||
}
|
||||
options.ProtonEmail = &protonEmail
|
||||
options.ProtonPassword = &protonPassword
|
||||
}
|
||||
|
||||
options.SetDefaults(options.Providers[0])
|
||||
|
||||
err := options.Validate()
|
||||
@@ -111,7 +94,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
|
||||
updater := updater.New(httpClient, storage, providers, logger)
|
||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
|
||||
func readObsolete(r *reader.Reader) (warnings []string) {
|
||||
keyToMessage := map[string]string{
|
||||
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
|
||||
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
|
||||
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
|
||||
"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)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -11,10 +15,31 @@ import (
|
||||
|
||||
// DNS contains settings to configure DNS.
|
||||
type DNS struct {
|
||||
// ServerEnabled is true if the server should be running
|
||||
// and used. It defaults to true, and cannot be nil
|
||||
// in the internal state.
|
||||
ServerEnabled *bool
|
||||
// UpstreamType can be dot or plain, and defaults to dot.
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
// UpdatePeriod is the period to update DNS block lists.
|
||||
// It can be set to 0 to disable the update.
|
||||
// It defaults to 24h and cannot be nil in
|
||||
// the internal state.
|
||||
UpdatePeriod *time.Duration
|
||||
// Providers is a list of DNS providers
|
||||
Providers []string `json:"providers"`
|
||||
// Caching is true if the server should cache
|
||||
// DNS responses.
|
||||
Caching *bool `json:"caching"`
|
||||
// IPv6 is true if the server should connect over IPv6.
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
// Blacklist contains settings to configure the filter
|
||||
// block lists.
|
||||
Blacklist DNSBlacklist
|
||||
// ServerAddress is the DNS server to use inside
|
||||
// the Go program and for the system.
|
||||
// It defaults to '127.0.0.1' to be used with the
|
||||
// DoT server. It cannot be the zero value in the internal
|
||||
// local server. It cannot be the zero value in the internal
|
||||
// state.
|
||||
ServerAddress netip.Addr
|
||||
// KeepNameserver is true if the existing DNS server
|
||||
@@ -23,20 +48,40 @@ type DNS struct {
|
||||
// outside the VPN tunnel since it would go through
|
||||
// the local DNS server of your Docker/Kubernetes
|
||||
// configuration, which is likely not going through the tunnel.
|
||||
// This will also disable the DNS over TLS server and the
|
||||
// This will also disable the DNS forwarder server and the
|
||||
// `ServerAddress` field will be ignored.
|
||||
// It defaults to false and cannot be nil in the
|
||||
// internal state.
|
||||
KeepNameserver *bool
|
||||
// DOT contains settings to configure the DoT
|
||||
// server.
|
||||
DoT DoT
|
||||
}
|
||||
|
||||
var (
|
||||
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
|
||||
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
|
||||
)
|
||||
|
||||
func (d DNS) validate() (err error) {
|
||||
err = d.DoT.validate()
|
||||
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
|
||||
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
|
||||
}
|
||||
|
||||
const minUpdatePeriod = 30 * time.Second
|
||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||
}
|
||||
|
||||
providers := provider.NewProviders()
|
||||
for _, providerName := range d.Providers {
|
||||
_, err := providers.Get(providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating DoT settings: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -44,9 +89,15 @@ func (d DNS) validate() (err error) {
|
||||
|
||||
func (d *DNS) Copy() (copied DNS) {
|
||||
return DNS{
|
||||
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
|
||||
UpstreamType: d.UpstreamType,
|
||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||
Providers: gosettings.CopySlice(d.Providers),
|
||||
Caching: gosettings.CopyPointer(d.Caching),
|
||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||
Blacklist: d.Blacklist.copy(),
|
||||
ServerAddress: d.ServerAddress,
|
||||
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||
DoT: d.DoT.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,16 +105,48 @@ func (d *DNS) Copy() (copied DNS) {
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DNS) overrideWith(other DNS) {
|
||||
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
|
||||
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
|
||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||
d.Blacklist.overrideWith(other.Blacklist)
|
||||
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||
d.DoT.overrideWith(other.DoT)
|
||||
}
|
||||
|
||||
func (d *DNS) setDefaults() {
|
||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
||||
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
|
||||
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||
provider.Cloudflare().Name,
|
||||
})
|
||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||
d.Blacklist.setDefaults()
|
||||
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
|
||||
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
|
||||
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||
d.DoT.setDefaults()
|
||||
}
|
||||
|
||||
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
|
||||
return d.ServerAddress
|
||||
}
|
||||
|
||||
providers := provider.NewProviders()
|
||||
provider, err := providers.Get(d.Providers[0])
|
||||
if err != nil {
|
||||
// Settings should be validated before calling this function,
|
||||
// so an error happening here is a programming error.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return provider.Plain.IPv4[0].Addr()
|
||||
}
|
||||
|
||||
func (d DNS) String() string {
|
||||
@@ -77,11 +160,63 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||
return node
|
||||
}
|
||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||
node.AppendNode(d.DoT.toLinesNode())
|
||||
|
||||
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled))
|
||||
if !*d.ServerEnabled {
|
||||
return node
|
||||
}
|
||||
|
||||
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
|
||||
|
||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||
for _, provider := range d.Providers {
|
||||
upstreamResolvers.Append(provider)
|
||||
}
|
||||
|
||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||
|
||||
update := "disabled"
|
||||
if *d.UpdatePeriod > 0 {
|
||||
update = "every " + d.UpdatePeriod.String()
|
||||
}
|
||||
node.Appendf("Update period: %s", update)
|
||||
|
||||
node.AppendNode(d.Blacklist.toLinesNode())
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
||||
|
||||
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
|
||||
|
||||
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Blacklist.read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -92,10 +227,5 @@ func (d *DNS) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.DoT.read(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DNS over TLS settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -149,23 +149,45 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
|
||||
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
|
||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
|
||||
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||
|
||||
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
|
||||
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
||||
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO v4 remove this block below
|
||||
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ips = append(ips, privateIPs...)
|
||||
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
|
||||
|
||||
return ips, ipPrefixes, nil
|
||||
}
|
||||
|
||||
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||
|
||||
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
|
||||
if len(privateAddresses) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// DoT contains settings to configure the DoT server.
|
||||
type DoT struct {
|
||||
// Enabled is true if the DoT server should be running
|
||||
// and used. It defaults to true, and cannot be nil
|
||||
// in the internal state.
|
||||
Enabled *bool
|
||||
// UpdatePeriod is the period to update DNS block lists.
|
||||
// It can be set to 0 to disable the update.
|
||||
// It defaults to 24h and cannot be nil in
|
||||
// the internal state.
|
||||
UpdatePeriod *time.Duration
|
||||
// Providers is a list of DNS over TLS providers
|
||||
Providers []string `json:"providers"`
|
||||
// Caching is true if the DoT server should cache
|
||||
// DNS responses.
|
||||
Caching *bool `json:"caching"`
|
||||
// IPv6 is true if the DoT server should connect over IPv6.
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
// Blacklist contains settings to configure the filter
|
||||
// block lists.
|
||||
Blacklist DNSBlacklist
|
||||
}
|
||||
|
||||
var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
|
||||
|
||||
func (d DoT) validate() (err error) {
|
||||
const minUpdatePeriod = 30 * time.Second
|
||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||
}
|
||||
|
||||
providers := provider.NewProviders()
|
||||
for _, providerName := range d.Providers {
|
||||
_, err := providers.Get(providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DoT) copy() (copied DoT) {
|
||||
return DoT{
|
||||
Enabled: gosettings.CopyPointer(d.Enabled),
|
||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||
Providers: gosettings.CopySlice(d.Providers),
|
||||
Caching: gosettings.CopyPointer(d.Caching),
|
||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||
Blacklist: d.Blacklist.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DoT) overrideWith(other DoT) {
|
||||
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||
d.Blacklist.overrideWith(other.Blacklist)
|
||||
}
|
||||
|
||||
func (d *DoT) setDefaults() {
|
||||
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||
provider.Cloudflare().Name,
|
||||
})
|
||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||
d.Blacklist.setDefaults()
|
||||
}
|
||||
|
||||
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||
providers := provider.NewProviders()
|
||||
provider, err := providers.Get(d.Providers[0])
|
||||
if err != nil {
|
||||
// Settings should be validated before calling this function,
|
||||
// so an error happening here is a programming error.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return provider.DoT.IPv4[0].Addr()
|
||||
}
|
||||
|
||||
func (d DoT) String() string {
|
||||
return d.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (d DoT) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS over TLS settings:")
|
||||
|
||||
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
|
||||
if !*d.Enabled {
|
||||
return node
|
||||
}
|
||||
|
||||
update := "disabled" //nolint:goconst
|
||||
if *d.UpdatePeriod > 0 {
|
||||
update = "every " + d.UpdatePeriod.String()
|
||||
}
|
||||
node.Appendf("Update period: %s", update)
|
||||
|
||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||
for _, provider := range d.Providers {
|
||||
upstreamResolvers.Append(provider)
|
||||
}
|
||||
|
||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||
|
||||
node.AppendNode(d.Blacklist.toLinesNode())
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (d *DoT) read(reader *reader.Reader) (err error) {
|
||||
d.Enabled, err = reader.BoolPtr("DOT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Providers = reader.CSV("DOT_PROVIDERS")
|
||||
|
||||
d.Caching, err = reader.BoolPtr("DOT_CACHING")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Blacklist.read(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -36,8 +36,6 @@ var (
|
||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
|
||||
ErrUpdaterProtonEmailMissing = errors.New("proton email is missing")
|
||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||
|
||||
@@ -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 (0.0.0.0)
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
51
internal/configuration/settings/ipv6.go
Normal file
51
internal/configuration/settings/ipv6.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// IPv6 contains settings regarding IPv6 configuration.
|
||||
type IPv6 struct {
|
||||
// CheckAddress is the TCP ip:port address to dial to check
|
||||
// IPv6 is supported, in case a default IPv6 route is found.
|
||||
// It defaults to cloudflare.com address [2606:4700::6810:84e5]:443
|
||||
CheckAddress netip.AddrPort
|
||||
}
|
||||
|
||||
func (i IPv6) validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IPv6) copy() (copied IPv6) {
|
||||
return IPv6{
|
||||
CheckAddress: i.CheckAddress,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IPv6) overrideWith(other IPv6) {
|
||||
i.CheckAddress = gosettings.OverrideWithValidator(i.CheckAddress, other.CheckAddress)
|
||||
}
|
||||
|
||||
func (i *IPv6) setDefaults() {
|
||||
defaultCheckAddress := netip.MustParseAddrPort("[2606:4700::6810:84e5]:443")
|
||||
i.CheckAddress = gosettings.DefaultComparable(i.CheckAddress, defaultCheckAddress)
|
||||
}
|
||||
|
||||
func (i IPv6) String() string {
|
||||
return i.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (i IPv6) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("IPv6 settings:")
|
||||
node.Appendf("Check address: %s", i.CheckAddress)
|
||||
return node
|
||||
}
|
||||
|
||||
func (i *IPv6) read(r *reader.Reader) (err error) {
|
||||
i.CheckAddress, err = r.NetipAddrPort("IPV6_CHECK_ADDRESS")
|
||||
return err
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -27,6 +27,7 @@ type Settings struct {
|
||||
Updater Updater
|
||||
Version Version
|
||||
VPN VPN
|
||||
IPv6 IPv6
|
||||
Pprof pprof.Settings
|
||||
}
|
||||
|
||||
@@ -53,6 +54,7 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
|
||||
"system": s.System.validate,
|
||||
"updater": s.Updater.Validate,
|
||||
"version": s.Version.validate,
|
||||
"ipv6": s.IPv6.validate,
|
||||
// Pprof validation done in pprof constructor
|
||||
"VPN": func() error {
|
||||
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||
@@ -85,6 +87,7 @@ func (s *Settings) copy() (copied Settings) {
|
||||
Version: s.Version.copy(),
|
||||
VPN: s.VPN.Copy(),
|
||||
Pprof: s.Pprof.Copy(),
|
||||
IPv6: s.IPv6.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +109,7 @@ func (s *Settings) OverrideWith(other Settings,
|
||||
patchedSettings.Version.overrideWith(other.Version)
|
||||
patchedSettings.VPN.OverrideWith(other.VPN)
|
||||
patchedSettings.Pprof.OverrideWith(other.Pprof)
|
||||
patchedSettings.IPv6.overrideWith(other.IPv6)
|
||||
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -121,6 +125,7 @@ func (s *Settings) SetDefaults() {
|
||||
s.Health.SetDefaults()
|
||||
s.HTTPProxy.setDefaults()
|
||||
s.Log.setDefaults()
|
||||
s.IPv6.setDefaults()
|
||||
s.PublicIP.setDefaults()
|
||||
s.Shadowsocks.setDefaults()
|
||||
s.Storage.setDefaults()
|
||||
@@ -142,6 +147,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
|
||||
node.AppendNode(s.DNS.toLinesNode())
|
||||
node.AppendNode(s.Firewall.toLinesNode())
|
||||
node.AppendNode(s.Log.toLinesNode())
|
||||
node.AppendNode(s.IPv6.toLinesNode())
|
||||
node.AppendNode(s.Health.toLinesNode())
|
||||
node.AppendNode(s.Shadowsocks.toLinesNode())
|
||||
node.AppendNode(s.HTTPProxy.toLinesNode())
|
||||
@@ -177,10 +183,10 @@ func (s Settings) Warnings() (warnings []string) {
|
||||
// TODO remove in v4
|
||||
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
||||
" so the DNS over TLS (DoT) server will not be used."+
|
||||
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
|
||||
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||
" corresponding to the first DoT provider chosen is used.")
|
||||
" so the local forwarding DNS server will not be used."+
|
||||
" The default value changed to 127.0.0.1 so it uses the internal DNS server."+
|
||||
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||
" corresponding to the first DNS provider chosen is used.")
|
||||
}
|
||||
|
||||
return warnings
|
||||
@@ -208,6 +214,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
|
||||
"updater": s.Updater.read,
|
||||
"version": s.Version.read,
|
||||
"VPN": s.VPN.read,
|
||||
"IPv6": s.IPv6.read,
|
||||
"profiling": s.Pprof.Read,
|
||||
}
|
||||
|
||||
|
||||
@@ -40,30 +40,28 @@ func Test_Settings_String(t *testing.T) {
|
||||
├── DNS settings:
|
||||
| ├── Keep existing nameserver(s): no
|
||||
| ├── DNS server address to use: 127.0.0.1
|
||||
| └── DNS over TLS settings:
|
||||
| ├── Enabled: yes
|
||||
| ├── Update period: every 24h0m0s
|
||||
| ├── Upstream resolvers:
|
||||
| | └── Cloudflare
|
||||
| ├── Caching: yes
|
||||
| ├── IPv6: no
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
| ├── DNS forwarder server enabled: yes
|
||||
| ├── Upstream resolver type: dot
|
||||
| ├── Upstream resolvers:
|
||||
| | └── Cloudflare
|
||||
| ├── Caching: yes
|
||||
| ├── IPv6: no
|
||||
| ├── Update period: every 24h0m0s
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
├── Firewall settings:
|
||||
| └── Enabled: yes
|
||||
├── Log settings:
|
||||
| └── Log level: INFO
|
||||
├── IPv6 settings:
|
||||
| └── Check address: [2606:4700::6810:84e5]:443
|
||||
├── 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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,10 +31,6 @@ type Updater struct {
|
||||
// Providers is the list of VPN service providers
|
||||
// to update server information for.
|
||||
Providers []string
|
||||
// ProtonEmail is the email to authenticate with the Proton API.
|
||||
ProtonEmail *string
|
||||
// ProtonPassword is the password to authenticate with the Proton API.
|
||||
ProtonPassword *string
|
||||
}
|
||||
|
||||
func (u Updater) Validate() (err error) {
|
||||
@@ -56,18 +51,6 @@ func (u Updater) Validate() (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
||||
}
|
||||
|
||||
if provider == providers.Protonvpn {
|
||||
authenticatedAPI := *u.ProtonEmail != "" || *u.ProtonPassword != ""
|
||||
if authenticatedAPI {
|
||||
switch {
|
||||
case *u.ProtonEmail == "":
|
||||
return fmt.Errorf("%w", ErrUpdaterProtonEmailMissing)
|
||||
case *u.ProtonPassword == "":
|
||||
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -75,12 +58,10 @@ func (u Updater) Validate() (err error) {
|
||||
|
||||
func (u *Updater) copy() (copied Updater) {
|
||||
return Updater{
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
|
||||
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,8 +73,6 @@ func (u *Updater) overrideWith(other Updater) {
|
||||
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
||||
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
|
||||
u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword)
|
||||
}
|
||||
|
||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
@@ -108,10 +87,6 @@ func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
||||
u.Providers = []string{vpnProvider}
|
||||
}
|
||||
|
||||
// Set these to empty strings to avoid nil pointer panics
|
||||
u.ProtonEmail = gosettings.DefaultPointer(u.ProtonEmail, "")
|
||||
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
|
||||
}
|
||||
|
||||
func (u Updater) String() string {
|
||||
@@ -128,10 +103,6 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||
if slices.Contains(u.Providers, providers.Protonvpn) {
|
||||
node.Appendf("Proton API email: %s", *u.ProtonEmail)
|
||||
node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
@@ -154,14 +125,6 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
||||
|
||||
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
||||
|
||||
u.ProtonEmail = r.Get("UPDATER_PROTONVPN_EMAIL")
|
||||
if u.ProtonEmail == nil {
|
||||
protonUsername := r.String("UPDATER_PROTONVPN_USERNAME", reader.IsRetro("UPDATER_PROTONVPN_EMAIL"))
|
||||
protonEmail := protonUsername + "@protonmail.com"
|
||||
u.ProtonEmail = &protonEmail
|
||||
}
|
||||
u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,15 +10,7 @@ import (
|
||||
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
settings := l.GetSettings()
|
||||
|
||||
// Try with user provided plaintext ip address
|
||||
// if it's not 127.0.0.1 (default for DoT), otherwise
|
||||
// use the first DoT provider ipv4 address found.
|
||||
var targetIP netip.Addr
|
||||
if settings.ServerAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
targetIP = settings.ServerAddress
|
||||
} else {
|
||||
targetIP = settings.DoT.GetFirstPlaintextIPv4()
|
||||
}
|
||||
targetIP := settings.GetFirstPlaintextIPv4()
|
||||
|
||||
if fallback {
|
||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||
@@ -27,14 +19,15 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
}
|
||||
|
||||
const dialTimeout = 3 * time.Second
|
||||
const defaultDNSPort = 53
|
||||
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
||||
IP: targetIP,
|
||||
Timeout: dialTimeout,
|
||||
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
|
||||
Timeout: dialTimeout,
|
||||
}
|
||||
nameserver.UseDNSInternally(settingsInternalDNS)
|
||||
|
||||
settingsSystemWide := nameserver.SettingsSystemDNS{
|
||||
IP: targetIP,
|
||||
IPs: []netip.Addr{targetIP},
|
||||
ResolvPath: l.resolvConf,
|
||||
}
|
||||
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
||||
|
||||
@@ -26,17 +26,18 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
// Upper scope variables for the DNS over TLS server only
|
||||
// Upper scope variables for the DNS forwarder server only
|
||||
// Their values are to be used if DOT=off
|
||||
var runError <-chan error
|
||||
|
||||
settings := l.GetSettings()
|
||||
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
||||
for !*settings.KeepNameserver && *settings.ServerEnabled {
|
||||
var err error
|
||||
runError, err = l.setupServer(ctx)
|
||||
if err == nil {
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.logger.Info("ready")
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -53,10 +54,9 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
l.logAndWait(ctx, err)
|
||||
settings = l.GetSettings()
|
||||
}
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
|
||||
settings = l.GetSettings()
|
||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||
if !*settings.KeepNameserver && !*settings.ServerEnabled {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
@@ -74,19 +74,15 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !*l.GetSettings().KeepNameserver {
|
||||
l.stopServer()
|
||||
// TODO revert OS and Go nameserver when exiting
|
||||
}
|
||||
l.stopServer()
|
||||
// TODO revert OS and Go nameserver when exiting
|
||||
return true
|
||||
case <-l.stop:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("stopping")
|
||||
if !*l.GetSettings().KeepNameserver {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.stopServer()
|
||||
}
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.stopServer()
|
||||
l.stopped <- struct{}{}
|
||||
case <-l.start:
|
||||
l.userTrigger = true
|
||||
@@ -105,6 +101,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
func (l *Loop) stopServer() {
|
||||
stopErr := l.server.Stop()
|
||||
if stopErr != nil {
|
||||
l.logger.Error("stopping DoT server: " + stopErr.Error())
|
||||
l.logger.Error("stopping server: " + stopErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
"github.com/qdm12/dns/v2/pkg/dot"
|
||||
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
|
||||
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||
"github.com/qdm12/dns/v2/pkg/plain"
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/dns/v2/pkg/server"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
@@ -22,33 +24,62 @@ func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
return l.state.SetSettings(ctx, settings)
|
||||
}
|
||||
|
||||
func buildDoTSettings(settings settings.DNS,
|
||||
func buildServerSettings(settings settings.DNS,
|
||||
filter *mapfilter.Filter, logger Logger) (
|
||||
serverSettings server.Settings, err error,
|
||||
) {
|
||||
serverSettings.Logger = logger
|
||||
|
||||
var dotSettings dot.Settings
|
||||
providersData := provider.NewProviders()
|
||||
dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.DoT.Providers))
|
||||
for i := range settings.DoT.Providers {
|
||||
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
|
||||
for i := range settings.Providers {
|
||||
var err error
|
||||
dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.DoT.Providers[i])
|
||||
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
|
||||
if err != nil {
|
||||
panic(err) // this should already had been checked
|
||||
}
|
||||
}
|
||||
dotSettings.IPVersion = "ipv4"
|
||||
if *settings.DoT.IPv6 {
|
||||
dotSettings.IPVersion = "ipv6"
|
||||
|
||||
ipVersion := "ipv4"
|
||||
if *settings.IPv6 {
|
||||
ipVersion = "ipv6"
|
||||
}
|
||||
|
||||
serverSettings.Dialer, err = dot.New(dotSettings)
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
||||
var dialer server.Dialer
|
||||
switch settings.UpstreamType {
|
||||
case "dot":
|
||||
dialerSettings := dot.Settings{
|
||||
UpstreamResolvers: upstreamResolvers,
|
||||
IPVersion: ipVersion,
|
||||
}
|
||||
dialer, err = dot.New(dialerSettings)
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
||||
}
|
||||
case "doh":
|
||||
dialerSettings := doh.Settings{
|
||||
UpstreamResolvers: upstreamResolvers,
|
||||
IPVersion: ipVersion,
|
||||
}
|
||||
dialer, err = doh.New(dialerSettings)
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err)
|
||||
}
|
||||
case "plain":
|
||||
dialerSettings := plain.Settings{
|
||||
UpstreamResolvers: upstreamResolvers,
|
||||
IPVersion: ipVersion,
|
||||
}
|
||||
dialer, err = plain.New(dialerSettings)
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err)
|
||||
}
|
||||
default:
|
||||
panic("unknown upstream type: " + settings.UpstreamType)
|
||||
}
|
||||
serverSettings.Dialer = dialer
|
||||
|
||||
if *settings.DoT.Caching {
|
||||
if *settings.Caching {
|
||||
lruCache, err := lru.New(lru.Settings{})
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/check"
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
@@ -20,14 +21,14 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
||||
|
||||
settings := l.GetSettings()
|
||||
|
||||
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger)
|
||||
serverSettings, err := buildServerSettings(settings, l.filter, l.logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("building DoT settings: %w", err)
|
||||
return nil, fmt.Errorf("building server settings: %w", err)
|
||||
}
|
||||
|
||||
server, err := server.New(dotSettings)
|
||||
server, err := server.New(serverSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating DoT server: %w", err)
|
||||
return nil, fmt.Errorf("creating server: %w", err)
|
||||
}
|
||||
|
||||
runError, err = server.Start(ctx)
|
||||
@@ -37,11 +38,12 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
||||
l.server = server
|
||||
|
||||
// use internal DNS server
|
||||
const defaultDNSPort = 53
|
||||
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
|
||||
IP: settings.ServerAddress,
|
||||
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
|
||||
})
|
||||
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
||||
IP: settings.ServerAddress,
|
||||
IPs: []netip.Addr{settings.ServerAddress},
|
||||
ResolvPath: l.resolvConf,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -27,7 +27,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
|
||||
// Check for only update period change
|
||||
tempSettings := s.settings.Copy()
|
||||
*tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod
|
||||
*tempSettings.UpdatePeriod = *settings.UpdatePeriod
|
||||
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
||||
|
||||
s.settings = settings
|
||||
@@ -40,7 +40,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
|
||||
// Restart
|
||||
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
||||
if *settings.DoT.Enabled {
|
||||
if *settings.ServerEnabled {
|
||||
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||
}
|
||||
return outcome
|
||||
|
||||
@@ -14,7 +14,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
timer.Stop()
|
||||
timerIsStopped := true
|
||||
settings := l.GetSettings()
|
||||
if period := *settings.DoT.UpdatePeriod; period > 0 {
|
||||
if period := *settings.UpdatePeriod; period > 0 {
|
||||
timer.Reset(period)
|
||||
timerIsStopped = false
|
||||
}
|
||||
@@ -43,14 +43,14 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
||||
|
||||
settings := l.GetSettings()
|
||||
timer.Reset(*settings.DoT.UpdatePeriod)
|
||||
timer.Reset(*settings.UpdatePeriod)
|
||||
case <-l.updateTicker:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
timerIsStopped = true
|
||||
settings := l.GetSettings()
|
||||
newUpdatePeriod := *settings.DoT.UpdatePeriod
|
||||
newUpdatePeriod := *settings.UpdatePeriod
|
||||
if newUpdatePeriod == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
||||
settings := l.GetSettings()
|
||||
|
||||
l.logger.Info("downloading hostnames and IP block lists")
|
||||
blacklistSettings := settings.DoT.Blacklist.ToBlockBuilderSettings(l.client)
|
||||
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
|
||||
|
||||
blockBuilder, err := blockbuilder.New(blacklistSettings)
|
||||
if err != nil {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -162,6 +162,24 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
|
||||
return c.runIP6tablesInstruction(ctx, instruction)
|
||||
}
|
||||
|
||||
func (c *Config) AcceptOutput(ctx context.Context,
|
||||
protocol, intf string, ip netip.Addr, port uint16, remove bool,
|
||||
) error {
|
||||
interfaceFlag := "-o " + intf
|
||||
if intf == "*" { // all interfaces
|
||||
interfaceFlag = ""
|
||||
}
|
||||
|
||||
instruction := fmt.Sprintf("%s OUTPUT -d %s %s -p %s -m %s --dport %d -j ACCEPT",
|
||||
appendOrDelete(remove), ip, interfaceFlag, protocol, protocol, port)
|
||||
if ip.Is4() {
|
||||
return c.runIptablesInstruction(ctx, instruction)
|
||||
} else if c.ip6Tables == "" {
|
||||
return fmt.Errorf("accept output to VPN server: %w", ErrNeedIP6Tables)
|
||||
}
|
||||
return c.runIP6tablesInstruction(ctx, instruction)
|
||||
}
|
||||
|
||||
// Thanks to @npawelek.
|
||||
func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context,
|
||||
intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool,
|
||||
|
||||
@@ -323,12 +323,12 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
|
||||
|
||||
func parseProtocol(s string) (protocol string, err error) {
|
||||
switch s {
|
||||
case "0":
|
||||
case "1":
|
||||
case "0", "all":
|
||||
case "1", "icmp":
|
||||
protocol = "icmp"
|
||||
case "6":
|
||||
case "6", "tcp":
|
||||
protocol = "tcp"
|
||||
case "17":
|
||||
case "17", "udp":
|
||||
protocol = "udp"
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)
|
||||
|
||||
@@ -58,6 +58,7 @@ num pkts bytes target prot opt in out source destinati
|
||||
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
||||
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
|
||||
5 0 0 ACCEPT all -- tun0 * 1.2.3.4 0.0.0.0/0
|
||||
`,
|
||||
table: chain{
|
||||
name: "INPUT",
|
||||
@@ -111,6 +112,17 @@ num pkts bytes target prot opt in out source destinati
|
||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
},
|
||||
{
|
||||
lineNumber: 5,
|
||||
packets: 0,
|
||||
bytes: 0,
|
||||
target: "ACCEPT",
|
||||
protocol: "",
|
||||
inputInterface: "tun0",
|
||||
outputInterface: "*",
|
||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
250
internal/healthcheck/checker.go
Normal file
250
internal/healthcheck/checker.go
Normal 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, "; "))
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
dialer: dialer,
|
||||
config: settings.Health{
|
||||
TargetAddress: address,
|
||||
},
|
||||
checker := &Checker{
|
||||
dialer: dialer,
|
||||
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{
|
||||
dialer: dialer,
|
||||
config: settings.Health{
|
||||
TargetAddress: listeningAddress.String(),
|
||||
},
|
||||
checker := &Checker{
|
||||
dialer: dialer,
|
||||
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)
|
||||
})
|
||||
66
internal/healthcheck/dns/dns.go
Normal file
66
internal/healthcheck/dns/dns.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
serverAddrs []netip.AddrPort
|
||||
dnsIPIndex int
|
||||
}
|
||||
|
||||
func New() *Client {
|
||||
return &Client{
|
||||
serverAddrs: concatAddrPorts([][]netip.AddrPort{
|
||||
provider.Cloudflare().Plain.IPv4,
|
||||
provider.Google().Plain.IPv4,
|
||||
provider.Quad9().Plain.IPv4,
|
||||
provider.OpenDNS().Plain.IPv4,
|
||||
provider.LibreDNS().Plain.IPv4,
|
||||
provider.Quadrant().Plain.IPv4,
|
||||
provider.CiraProtected().Plain.IPv4,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func concatAddrPorts(addrs [][]netip.AddrPort) []netip.AddrPort {
|
||||
var result []netip.AddrPort
|
||||
for _, addrList := range addrs {
|
||||
result = append(result, addrList...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var ErrLookupNoIPs = errors.New("no IPs found from DNS lookup")
|
||||
|
||||
func (c *Client) Check(ctx context.Context) error {
|
||||
dnsAddr := c.serverAddrs[c.dnsIPIndex].Addr()
|
||||
resolver := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
dialer := net.Dialer{}
|
||||
return dialer.DialContext(ctx, "udp", dnsAddr.String())
|
||||
},
|
||||
}
|
||||
ips, err := resolver.LookupIP(ctx, "ip", "github.com")
|
||||
switch {
|
||||
case err != nil:
|
||||
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
||||
return err
|
||||
case len(ips) == 0:
|
||||
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
||||
return fmt.Errorf("%w", ErrLookupNoIPs)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
49
internal/healthcheck/icmp/apple_ipv4.go
Normal file
49
internal/healthcheck/icmp/apple_ipv4.go
Normal 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)
|
||||
}
|
||||
193
internal/healthcheck/icmp/echo.go
Normal file
193
internal/healthcheck/icmp/echo.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
6
internal/healthcheck/icmp/interfaces.go
Normal file
6
internal/healthcheck/icmp/interfaces.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package icmp
|
||||
|
||||
type Logger interface {
|
||||
Debugf(format string, args ...any)
|
||||
Warnf(format string, args ...any)
|
||||
}
|
||||
35
internal/healthcheck/icmp/listen.go
Normal file
35
internal/healthcheck/icmp/listen.go
Normal 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
|
||||
}
|
||||
9
internal/healthcheck/interfaces.go
Normal file
9
internal/healthcheck/interfaces.go
Normal 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)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package healthcheck
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
config: config,
|
||||
vpn: vpnHealth{
|
||||
loop: vpnLoop,
|
||||
healthyWait: *config.VPN.Initial,
|
||||
},
|
||||
handler: newHandler(logger),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) SetError(err error) {
|
||||
s.handler.setErr(err)
|
||||
}
|
||||
|
||||
type StatusApplier interface {
|
||||
ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
||||
outcome string, err error)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
package netlink
|
||||
|
||||
import "github.com/qdm12/log"
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
type DebugLogger interface {
|
||||
Debug(message string)
|
||||
Debugf(format string, args ...any)
|
||||
Patch(options ...log.Option)
|
||||
}
|
||||
|
||||
type Firewall interface {
|
||||
AcceptOutput(ctx context.Context, protocol, intf string, ip netip.Addr,
|
||||
port uint16, remove bool) (err error)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,106 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (n *NetLink) IsIPv6Supported() (supported bool, err error) {
|
||||
type IPv6SupportLevel uint8
|
||||
|
||||
const (
|
||||
IPv6Unsupported = iota
|
||||
// IPv6Supported indicates the host supports IPv6 but has no access to the
|
||||
// Internet via IPv6. It is true if one IPv6 route is found and no default
|
||||
// IPv6 route is found.
|
||||
IPv6Supported
|
||||
// IPv6Internet indicates the host has access to the Internet via IPv6,
|
||||
// which is detected when a default IPv6 route is found.
|
||||
IPv6Internet
|
||||
)
|
||||
|
||||
func (i IPv6SupportLevel) IsSupported() bool {
|
||||
return i == IPv6Supported || i == IPv6Internet
|
||||
}
|
||||
|
||||
func (n *NetLink) FindIPv6SupportLevel(ctx context.Context,
|
||||
checkAddress netip.AddrPort, firewall Firewall,
|
||||
) (level IPv6SupportLevel, err error) {
|
||||
routes, err := n.RouteList(FamilyV6)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("listing IPv6 routes: %w", err)
|
||||
return IPv6Unsupported, fmt.Errorf("listing IPv6 routes: %w", err)
|
||||
}
|
||||
|
||||
// Check each route for IPv6 due to Podman bug listing IPv4 routes
|
||||
// as IPv6 routes at container start, see:
|
||||
// https://github.com/qdm12/gluetun/issues/1241#issuecomment-1333405949
|
||||
level = IPv6Unsupported
|
||||
for _, route := range routes {
|
||||
link, err := n.LinkByIndex(route.LinkIndex)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("finding link corresponding to route: %w", err)
|
||||
return IPv6Unsupported, fmt.Errorf("finding link corresponding to route: %w", err)
|
||||
}
|
||||
|
||||
sourceIsIPv6 := route.Src.IsValid() && route.Src.Is6()
|
||||
sourceIsIPv4 := route.Src.IsValid() && route.Src.Is4()
|
||||
destinationIsIPv4 := route.Dst.IsValid() && route.Dst.Addr().Is4()
|
||||
destinationIsIPv6 := route.Dst.IsValid() && route.Dst.Addr().Is6()
|
||||
switch {
|
||||
case !sourceIsIPv6 && !destinationIsIPv6,
|
||||
case sourceIsIPv4 && destinationIsIPv4,
|
||||
destinationIsIPv6 && route.Dst.Addr().IsLoopback():
|
||||
continue
|
||||
case route.Dst.Addr().IsUnspecified(): // default ipv6 route
|
||||
n.debugLogger.Debugf("IPv6 default route found on link %s", link.Name)
|
||||
err = dialAddrThroughFirewall(ctx, link.Name, checkAddress, firewall)
|
||||
if err != nil {
|
||||
n.debugLogger.Debugf("IPv6 query failed on %s: %w", link.Name, err)
|
||||
level = IPv6Supported
|
||||
continue
|
||||
}
|
||||
n.debugLogger.Debugf("IPv6 internet is accessible through link %s", link.Name)
|
||||
return IPv6Internet, nil
|
||||
default: // non-default ipv6 route found
|
||||
n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name)
|
||||
level = IPv6Supported
|
||||
}
|
||||
|
||||
n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
n.debugLogger.Debugf("IPv6 is not supported after searching %d routes",
|
||||
len(routes))
|
||||
return false, nil
|
||||
if level == IPv6Unsupported {
|
||||
n.debugLogger.Debugf("no IPv6 route found in %d routes", len(routes))
|
||||
}
|
||||
return level, nil
|
||||
}
|
||||
|
||||
func dialAddrThroughFirewall(ctx context.Context, intf string,
|
||||
checkAddress netip.AddrPort, firewall Firewall,
|
||||
) (err error) {
|
||||
const protocol = "tcp"
|
||||
remove := false
|
||||
err = firewall.AcceptOutput(ctx, protocol, intf,
|
||||
checkAddress.Addr(), checkAddress.Port(), remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("accepting output traffic: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
remove = true
|
||||
firewallErr := firewall.AcceptOutput(ctx, protocol, intf,
|
||||
checkAddress.Addr(), checkAddress.Port(), remove)
|
||||
if err == nil && firewallErr != nil {
|
||||
err = fmt.Errorf("removing output traffic rule: %w", firewallErr)
|
||||
}
|
||||
}()
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Second,
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, protocol, checkAddress.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing: %w", err)
|
||||
}
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing connection: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
167
internal/netlink/ipv6_test.go
Normal file
167
internal/netlink/ipv6_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func isIPv6LocallySupported() bool {
|
||||
dialer := net.Dialer{Timeout: time.Millisecond}
|
||||
_, err := dialer.Dial("tcp6", "[::1]:9999")
|
||||
return !strings.HasSuffix(err.Error(), "connect: cannot assign requested address")
|
||||
}
|
||||
|
||||
// Susceptible to TOCTOU but it should be fine for the use case.
|
||||
func findAvailableTCPPort(t *testing.T) (port uint16) {
|
||||
t.Helper()
|
||||
|
||||
config := &net.ListenConfig{}
|
||||
listener, err := config.Listen(context.Background(), "tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := listener.Addr().String()
|
||||
err = listener.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
addrPort, err := netip.ParseAddrPort(addr)
|
||||
require.NoError(t, err)
|
||||
|
||||
return addrPort.Port()
|
||||
}
|
||||
|
||||
func Test_dialAddrThroughFirewall(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
errTest := errors.New("test error")
|
||||
|
||||
const ipv6InternetWorks = false
|
||||
|
||||
testCases := map[string]struct {
|
||||
getIPv6CheckAddr func(t *testing.T) netip.AddrPort
|
||||
firewallAddErr error
|
||||
firewallRemoveErr error
|
||||
errMessageRegex func() string
|
||||
}{
|
||||
"cloudflare.com": {
|
||||
getIPv6CheckAddr: func(_ *testing.T) netip.AddrPort {
|
||||
return netip.MustParseAddrPort("[2606:4700::6810:84e5]:443")
|
||||
},
|
||||
errMessageRegex: func() string {
|
||||
if ipv6InternetWorks {
|
||||
return ""
|
||||
}
|
||||
return "dialing: dial tcp \\[2606:4700::6810:84e5\\]:443: " +
|
||||
"connect: (cannot assign requested address|network is unreachable)"
|
||||
},
|
||||
},
|
||||
"local_server": {
|
||||
getIPv6CheckAddr: func(t *testing.T) netip.AddrPort {
|
||||
t.Helper()
|
||||
|
||||
network := "tcp6"
|
||||
loopback := netip.MustParseAddr("::1")
|
||||
if !isIPv6LocallySupported() {
|
||||
network = "tcp4"
|
||||
loopback = netip.MustParseAddr("127.0.0.1")
|
||||
}
|
||||
|
||||
listener, err := net.ListenTCP(network, nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := listener.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
addrPort := netip.MustParseAddrPort(listener.Addr().String())
|
||||
return netip.AddrPortFrom(loopback, addrPort.Port())
|
||||
},
|
||||
},
|
||||
"no_local_server": {
|
||||
getIPv6CheckAddr: func(t *testing.T) netip.AddrPort {
|
||||
t.Helper()
|
||||
|
||||
loopback := netip.MustParseAddr("::1")
|
||||
if !ipv6InternetWorks {
|
||||
loopback = netip.MustParseAddr("127.0.0.1")
|
||||
}
|
||||
|
||||
availablePort := findAvailableTCPPort(t)
|
||||
return netip.AddrPortFrom(loopback, availablePort)
|
||||
},
|
||||
errMessageRegex: func() string {
|
||||
return "dialing: dial tcp (\\[::1\\]|127\\.0\\.0\\.1):[1-9][0-9]{1,4}: " +
|
||||
"connect: connection refused"
|
||||
},
|
||||
},
|
||||
"firewall_add_error": {
|
||||
firewallAddErr: errTest,
|
||||
errMessageRegex: func() string {
|
||||
return "accepting output traffic: test error"
|
||||
},
|
||||
},
|
||||
"firewall_remove_error": {
|
||||
getIPv6CheckAddr: func(t *testing.T) netip.AddrPort {
|
||||
t.Helper()
|
||||
|
||||
network := "tcp4"
|
||||
loopback := netip.MustParseAddr("127.0.0.1")
|
||||
listener, err := net.ListenTCP(network, nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := listener.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
addrPort := netip.MustParseAddrPort(listener.Addr().String())
|
||||
return netip.AddrPortFrom(loopback, addrPort.Port())
|
||||
},
|
||||
firewallRemoveErr: errTest,
|
||||
errMessageRegex: func() string {
|
||||
return "removing output traffic rule: test error"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
var checkAddr netip.AddrPort
|
||||
if testCase.getIPv6CheckAddr != nil {
|
||||
checkAddr = testCase.getIPv6CheckAddr(t)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
const intf = "eth0"
|
||||
firewall := NewMockFirewall(ctrl)
|
||||
call := firewall.EXPECT().AcceptOutput(ctx, "tcp", intf,
|
||||
checkAddr.Addr(), checkAddr.Port(), false).
|
||||
Return(testCase.firewallAddErr)
|
||||
if testCase.firewallAddErr == nil {
|
||||
firewall.EXPECT().AcceptOutput(ctx, "tcp", intf,
|
||||
checkAddr.Addr(), checkAddr.Port(), true).
|
||||
Return(testCase.firewallRemoveErr).After(call)
|
||||
}
|
||||
|
||||
err := dialAddrThroughFirewall(ctx, intf, checkAddr, firewall)
|
||||
var errMessageRegex string
|
||||
if testCase.errMessageRegex != nil {
|
||||
errMessageRegex = testCase.errMessageRegex()
|
||||
}
|
||||
if errMessageRegex == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Regexp(t, errMessageRegex, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
3
internal/netlink/mocks_generate_test.go
Normal file
3
internal/netlink/mocks_generate_test.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package netlink
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Firewall
|
||||
50
internal/netlink/mocks_test.go
Normal file
50
internal/netlink/mocks_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/netlink (interfaces: Firewall)
|
||||
|
||||
// Package netlink is a generated GoMock package.
|
||||
package netlink
|
||||
|
||||
import (
|
||||
context "context"
|
||||
netip "net/netip"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockFirewall is a mock of Firewall interface.
|
||||
type MockFirewall struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockFirewallMockRecorder
|
||||
}
|
||||
|
||||
// MockFirewallMockRecorder is the mock recorder for MockFirewall.
|
||||
type MockFirewallMockRecorder struct {
|
||||
mock *MockFirewall
|
||||
}
|
||||
|
||||
// NewMockFirewall creates a new mock instance.
|
||||
func NewMockFirewall(ctrl *gomock.Controller) *MockFirewall {
|
||||
mock := &MockFirewall{ctrl: ctrl}
|
||||
mock.recorder = &MockFirewallMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockFirewall) EXPECT() *MockFirewallMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AcceptOutput mocks base method.
|
||||
func (m *MockFirewall) AcceptOutput(arg0 context.Context, arg1, arg2 string, arg3 netip.Addr, arg4 uint16, arg5 bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AcceptOutput", arg0, arg1, arg2, arg3, arg4, arg5)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AcceptOutput indicates an expected call of AcceptOutput.
|
||||
func (mr *MockFirewallMockRecorder) AcceptOutput(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptOutput", reflect.TypeOf((*MockFirewall)(nil).AcceptOutput), arg0, arg1, arg2, arg3, arg4, arg5)
|
||||
}
|
||||
@@ -13,7 +13,6 @@ var (
|
||||
ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
ErrIPFetcherUnsupported = errors.New("IP fetcher not supported")
|
||||
ErrCredentialsMissing = errors.New("credentials missing")
|
||||
)
|
||||
|
||||
type Fetcher interface {
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -18,12 +18,11 @@ type Provider struct {
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
client *http.Client, updaterWarner common.Warner,
|
||||
email, password string,
|
||||
) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(client, updaterWarner, email, password),
|
||||
Fetcher: updater.New(client, updaterWarner),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,562 +1,15 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
srp "github.com/ProtonMail/go-srp"
|
||||
)
|
||||
|
||||
// apiClient is a minimal Proton v4 API client which can handle all the
|
||||
// oddities of Proton's authentication flow they want to keep hidden
|
||||
// from the public.
|
||||
type apiClient struct {
|
||||
apiURLBase string
|
||||
httpClient *http.Client
|
||||
appVersion string
|
||||
userAgent string
|
||||
generator *rand.ChaCha8
|
||||
}
|
||||
|
||||
// newAPIClient returns an [apiClient] with sane defaults matching Proton's
|
||||
// insane expectations.
|
||||
func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClient, err error) {
|
||||
var seed [32]byte
|
||||
_, _ = crand.Read(seed[:])
|
||||
generator := rand.NewChaCha8(seed)
|
||||
|
||||
// Pick a random user agent from this list. Because I'm not going to tell
|
||||
// Proton shit on where all these funny requests are coming from, given their
|
||||
// unhelpfulness in figuring out their authentication flow.
|
||||
userAgents := [...]string{
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||
}
|
||||
userAgent := userAgents[generator.Uint64()%uint64(len(userAgents))]
|
||||
|
||||
appVersion, err := getMostRecentStableTag(ctx, httpClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting most recent version for proton app: %w", err)
|
||||
}
|
||||
|
||||
return &apiClient{
|
||||
apiURLBase: "https://account.proton.me/api",
|
||||
httpClient: httpClient,
|
||||
appVersion: appVersion,
|
||||
userAgent: userAgent,
|
||||
generator: generator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var ErrCodeNotSuccess = errors.New("response code is not success")
|
||||
|
||||
// setHeaders sets the minimal necessary headers for Proton API requests
|
||||
// to succeed without being blocked by their "security" measures.
|
||||
// See for example [getMostRecentStableTag] on how the app version must
|
||||
// be set to a recent version or they block your request. "SeCuRiTy"...
|
||||
func (c *apiClient) setHeaders(request *http.Request, cookie cookie) {
|
||||
request.Header.Set("Cookie", cookie.String())
|
||||
request.Header.Set("User-Agent", c.userAgent)
|
||||
request.Header.Set("x-pm-appversion", c.appVersion)
|
||||
request.Header.Set("x-pm-locale", "en_US")
|
||||
request.Header.Set("x-pm-uid", cookie.uid)
|
||||
}
|
||||
|
||||
// authenticate performs the full Proton authentication flow
|
||||
// to obtain an authenticated cookie (uid, token and session ID).
|
||||
func (c *apiClient) authenticate(ctx context.Context, email, password string,
|
||||
) (authCookie cookie, err error) {
|
||||
sessionID, err := c.getSessionID(ctx)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("getting session ID: %w", err)
|
||||
}
|
||||
|
||||
tokenType, accessToken, refreshToken, uid, err := c.getUnauthSession(ctx, sessionID)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("getting unauthenticated session data: %w", err)
|
||||
}
|
||||
|
||||
cookieToken, err := c.cookieToken(ctx, sessionID, tokenType, accessToken, refreshToken, uid)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("getting cookie token: %w", err)
|
||||
}
|
||||
|
||||
unauthCookie := cookie{
|
||||
uid: uid,
|
||||
token: cookieToken,
|
||||
sessionID: sessionID,
|
||||
}
|
||||
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64,
|
||||
srpSessionHex, version, err := c.authInfo(ctx, email, unauthCookie)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("getting auth information: %w", err)
|
||||
}
|
||||
|
||||
// Prepare SRP proof generator using Proton's official SRP parameters and hashing.
|
||||
srpAuth, err := srp.NewAuth(version, username, []byte(password),
|
||||
saltBase64, modulusPGPClearSigned, serverEphemeralBase64)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("initializing SRP auth: %w", err)
|
||||
}
|
||||
|
||||
// Generate SRP proofs (A, M1) with the usual 2048-bit modulus.
|
||||
const modulusBits = 2048
|
||||
proofs, err := srpAuth.GenerateProofs(modulusBits)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("generating SRP proofs: %w", err)
|
||||
}
|
||||
|
||||
authCookie, err = c.auth(ctx, unauthCookie, email, srpSessionHex, proofs)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("authentifying: %w", err)
|
||||
}
|
||||
|
||||
return authCookie, nil
|
||||
}
|
||||
|
||||
var ErrSessionIDNotFound = errors.New("session ID not found in cookies")
|
||||
|
||||
func (c *apiClient) getSessionID(ctx context.Context) (sessionID string, err error) {
|
||||
const url = "https://account.proton.me/vpn"
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("closing response body: %w", err)
|
||||
}
|
||||
|
||||
for _, cookie := range response.Cookies() {
|
||||
if cookie.Name == "Session-Id" {
|
||||
return cookie.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w", ErrSessionIDNotFound)
|
||||
}
|
||||
|
||||
var ErrDataFieldMissing = errors.New("data field missing in response")
|
||||
|
||||
func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
|
||||
tokenType, accessToken, refreshToken, uid string, err error,
|
||||
) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/auth/v4/sessions", nil)
|
||||
if err != nil {
|
||||
return "", "", "", "", fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
unauthCookie := cookie{
|
||||
sessionID: sessionID,
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", "", "", "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", "", "", "", fmt.Errorf("reading response body: %w", err)
|
||||
} else if response.StatusCode != http.StatusOK {
|
||||
return "", "", "", "", buildError(response.StatusCode, responseBody)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Code uint `json:"Code"` // 1000 on success
|
||||
AccessToken string `json:"AccessToken"` // 32-chars lowercase and digits
|
||||
RefreshToken string `json:"RefreshToken"` // 32-chars lowercase and digits
|
||||
TokenType string `json:"TokenType"` // "Bearer"
|
||||
Scopes []string `json:"Scopes"` // should be [] for our usage
|
||||
UID string `json:"UID"` // 32-chars lowercase and digits
|
||||
LocalID uint `json:"LocalID"` // 0 in my case
|
||||
}
|
||||
|
||||
err = json.Unmarshal(responseBody, &data)
|
||||
if err != nil {
|
||||
return "", "", "", "", fmt.Errorf("decoding response body: %w", err)
|
||||
}
|
||||
|
||||
const successCode = 1000
|
||||
switch {
|
||||
case data.Code != successCode:
|
||||
return "", "", "", "", fmt.Errorf("%w: expected %d got %d",
|
||||
ErrCodeNotSuccess, successCode, data.Code)
|
||||
case data.AccessToken == "":
|
||||
return "", "", "", "", fmt.Errorf("%w: access token is empty", ErrDataFieldMissing)
|
||||
case data.RefreshToken == "":
|
||||
return "", "", "", "", fmt.Errorf("%w: refresh token is empty", ErrDataFieldMissing)
|
||||
case data.TokenType == "":
|
||||
return "", "", "", "", fmt.Errorf("%w: token type is empty", ErrDataFieldMissing)
|
||||
case data.UID == "":
|
||||
return "", "", "", "", fmt.Errorf("%w: UID is empty", ErrDataFieldMissing)
|
||||
}
|
||||
// Ignore Scopes and LocalID fields, we don't use them.
|
||||
|
||||
return data.TokenType, data.AccessToken, data.RefreshToken, data.UID, nil
|
||||
}
|
||||
|
||||
var ErrUIDMismatch = errors.New("UID in response does not match request UID")
|
||||
|
||||
func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, accessToken,
|
||||
refreshToken, uid string,
|
||||
) (cookieToken string, err error) {
|
||||
type requestBodySchema struct {
|
||||
GrantType string `json:"GrantType"` // "refresh_token"
|
||||
Persistent uint `json:"Persistent"` // 0
|
||||
RedirectURI string `json:"RedirectURI"` // "https://protonmail.com"
|
||||
RefreshToken string `json:"RefreshToken"` // 32-chars lowercase and digits
|
||||
ResponseType string `json:"ResponseType"` // "token"
|
||||
State string `json:"State"` // 24-chars letters and digits
|
||||
UID string `json:"UID"` // 32-chars lowercase and digits
|
||||
}
|
||||
requestBody := requestBodySchema{
|
||||
GrantType: "refresh_token",
|
||||
Persistent: 0,
|
||||
RedirectURI: "https://protonmail.com",
|
||||
RefreshToken: refreshToken,
|
||||
ResponseType: "token",
|
||||
State: generateLettersDigits(c.generator, 24), //nolint:mnd
|
||||
UID: uid,
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
if err := encoder.Encode(requestBody); err != nil {
|
||||
return "", fmt.Errorf("encoding request body: %w", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth/cookies", buffer)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
unauthCookie := cookie{
|
||||
uid: uid,
|
||||
sessionID: sessionID,
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading response body: %w", err)
|
||||
} else if response.StatusCode != http.StatusOK {
|
||||
return "", buildError(response.StatusCode, responseBody)
|
||||
}
|
||||
|
||||
var cookies struct {
|
||||
Code uint `json:"Code"` // 1000 on success
|
||||
UID string `json:"UID"` // should match request UID
|
||||
LocalID uint `json:"LocalID"` // 0
|
||||
RefreshCounter uint `json:"RefreshCounter"` // 1
|
||||
}
|
||||
err = json.Unmarshal(responseBody, &cookies)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding response body: %w", err)
|
||||
}
|
||||
|
||||
const successCode = 1000
|
||||
switch {
|
||||
case cookies.Code != successCode:
|
||||
return "", fmt.Errorf("%w: expected %d got %d",
|
||||
ErrCodeNotSuccess, successCode, cookies.Code)
|
||||
case cookies.UID != requestBody.UID:
|
||||
return "", fmt.Errorf("%w: expected %s got %s",
|
||||
ErrUIDMismatch, requestBody.UID, cookies.UID)
|
||||
}
|
||||
// Ignore LocalID and RefreshCounter fields, we don't use them.
|
||||
|
||||
for _, cookie := range response.Cookies() {
|
||||
if cookie.Name == "AUTH-"+uid {
|
||||
return cookie.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w", ErrAuthCookieNotFound)
|
||||
}
|
||||
|
||||
var ErrUsernameDoesNotExist = errors.New("username does not exist")
|
||||
|
||||
// authInfo fetches SRP parameters for the account.
|
||||
func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie cookie) (
|
||||
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64, srpSessionHex string,
|
||||
version int, err error,
|
||||
) {
|
||||
type requestBodySchema struct {
|
||||
Intent string `json:"Intent"` // "Proton"
|
||||
Username string `json:"Username"`
|
||||
}
|
||||
requestBody := requestBodySchema{
|
||||
Intent: "Proton",
|
||||
Username: email,
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
if err := encoder.Encode(requestBody); err != nil {
|
||||
return "", "", "", "", "", 0, fmt.Errorf("encoding request body: %w", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth/info", buffer)
|
||||
if err != nil {
|
||||
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", "", "", "", "", 0, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", "", "", "", "", 0, fmt.Errorf("reading response body: %w", err)
|
||||
} else if response.StatusCode != http.StatusOK {
|
||||
return "", "", "", "", "", 0, buildError(response.StatusCode, responseBody)
|
||||
}
|
||||
|
||||
var info struct {
|
||||
Code uint `json:"Code"` // 1000 on success
|
||||
Modulus string `json:"Modulus"` // PGP clearsigned modulus string
|
||||
ServerEphemeral string `json:"ServerEphemeral"` // base64
|
||||
Version *uint `json:"Version,omitempty"` // 4 as of 2025-10-26
|
||||
Salt string `json:"Salt"` // base64
|
||||
SRPSession string `json:"SRPSession"` // hexadecimal
|
||||
Username string `json:"Username"` // user without @domain.com. Mine has its first letter capitalized.
|
||||
}
|
||||
err = json.Unmarshal(responseBody, &info)
|
||||
if err != nil {
|
||||
return "", "", "", "", "", 0, fmt.Errorf("decoding response body: %w", err)
|
||||
}
|
||||
|
||||
const successCode = 1000
|
||||
switch {
|
||||
case info.Code != successCode:
|
||||
return "", "", "", "", "", 0, fmt.Errorf("%w: expected %d got %d",
|
||||
ErrCodeNotSuccess, successCode, info.Code)
|
||||
case info.Modulus == "":
|
||||
return "", "", "", "", "", 0, fmt.Errorf("%w: modulus is empty", ErrDataFieldMissing)
|
||||
case info.ServerEphemeral == "":
|
||||
return "", "", "", "", "", 0, fmt.Errorf("%w: server ephemeral is empty", ErrDataFieldMissing)
|
||||
case info.Salt == "":
|
||||
return "", "", "", "", "", 0, fmt.Errorf("%w (salt data field is empty)", ErrUsernameDoesNotExist)
|
||||
case info.SRPSession == "":
|
||||
return "", "", "", "", "", 0, fmt.Errorf("%w: SRP session is empty", ErrDataFieldMissing)
|
||||
case info.Username == "":
|
||||
return "", "", "", "", "", 0, fmt.Errorf("%w: username is empty", ErrDataFieldMissing)
|
||||
case info.Version == nil:
|
||||
return "", "", "", "", "", 0, fmt.Errorf("%w: version is missing", ErrDataFieldMissing)
|
||||
}
|
||||
|
||||
version = int(*info.Version) //nolint:gosec
|
||||
return info.Username, info.Modulus, info.ServerEphemeral, info.Salt,
|
||||
info.SRPSession, version, nil
|
||||
}
|
||||
|
||||
type cookie struct {
|
||||
uid string
|
||||
token string
|
||||
sessionID string
|
||||
}
|
||||
|
||||
func (c *cookie) String() string {
|
||||
s := ""
|
||||
if c.token != "" {
|
||||
s += fmt.Sprintf("AUTH-%s=%s; ", c.uid, c.token)
|
||||
}
|
||||
if c.sessionID != "" {
|
||||
s += fmt.Sprintf("Session-Id=%s; ", c.sessionID)
|
||||
}
|
||||
if c.token != "" {
|
||||
s += "Tag=default; iaas=W10; Domain=proton.me; Feature=VPNDashboard:A"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrServerProofNotValid indicates the M2 from the server didn't match the expected proof.
|
||||
ErrServerProofNotValid = errors.New("server proof from server is not valid")
|
||||
ErrVPNScopeNotFound = errors.New("VPN scope not found in scopes")
|
||||
ErrTwoFANotSupported = errors.New("two factor authentication not supported in this client")
|
||||
ErrAuthCookieNotFound = errors.New("auth cookie not found")
|
||||
)
|
||||
|
||||
// auth performs the SRP proof submission (and optionally TOTP) to obtain tokens.
|
||||
func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
|
||||
username, srpSession string, proofs *srp.Proofs,
|
||||
) (authCookie cookie, err error) {
|
||||
clientEphemeral := base64.StdEncoding.EncodeToString(proofs.ClientEphemeral)
|
||||
clientProof := base64.StdEncoding.EncodeToString(proofs.ClientProof)
|
||||
|
||||
type requestBodySchema struct {
|
||||
ClientEphemeral string `json:"ClientEphemeral"` // base64(A)
|
||||
ClientProof string `json:"ClientProof"` // base64(M1)
|
||||
Payload map[string]string `json:"Payload,omitempty"` // not sure
|
||||
SRPSession string `json:"SRPSession"` // hexadecimal
|
||||
Username string `json:"Username"` // user@protonmail.com
|
||||
}
|
||||
requestBody := requestBodySchema{
|
||||
ClientEphemeral: clientEphemeral,
|
||||
ClientProof: clientProof,
|
||||
SRPSession: srpSession,
|
||||
Username: username,
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
if err := encoder.Encode(requestBody); err != nil {
|
||||
return cookie{}, fmt.Errorf("encoding request body: %w", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURLBase+"/core/v4/auth", buffer)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return cookie{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("reading response body: %w", err)
|
||||
} else if response.StatusCode != http.StatusOK {
|
||||
return cookie{}, buildError(response.StatusCode, responseBody)
|
||||
}
|
||||
|
||||
type twoFAStatus uint
|
||||
//nolint:unused
|
||||
const (
|
||||
twoFADisabled twoFAStatus = iota
|
||||
twoFAHasTOTP
|
||||
twoFAHasFIDO2
|
||||
twoFAHasFIDO2AndTOTP
|
||||
)
|
||||
type twoFAInfo struct {
|
||||
Enabled twoFAStatus `json:"Enabled"`
|
||||
FIDO2 struct {
|
||||
AuthenticationOptions any `json:"AuthenticationOptions"`
|
||||
RegisteredKeys []any `json:"RegisteredKeys"`
|
||||
} `json:"FIDO2"`
|
||||
TOTP uint `json:"TOTP"`
|
||||
}
|
||||
|
||||
var auth struct {
|
||||
Code uint `json:"Code"` // 1000 on success
|
||||
LocalID uint `json:"LocalID"` // 7 in my case
|
||||
Scopes []string `json:"Scopes"` // this should contain "vpn". Same as `Scope` field value.
|
||||
UID string `json:"UID"` // same as `Uid` field value
|
||||
UserID string `json:"UserID"` // base64
|
||||
EventID string `json:"EventID"` // base64
|
||||
PasswordMode uint `json:"PasswordMode"` // 1 in my case
|
||||
ServerProof string `json:"ServerProof"` // base64(M2)
|
||||
TwoFactor uint `json:"TwoFactor"` // 0 if 2FA not required
|
||||
TwoFA twoFAInfo `json:"2FA"`
|
||||
TemporaryPassword uint `json:"TemporaryPassword"` // 0 in my case
|
||||
}
|
||||
|
||||
err = json.Unmarshal(responseBody, &auth)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("decoding response body: %w", err)
|
||||
}
|
||||
|
||||
m2, err := base64.StdEncoding.DecodeString(auth.ServerProof)
|
||||
if err != nil {
|
||||
return cookie{}, fmt.Errorf("decoding server proof: %w", err)
|
||||
}
|
||||
if !bytes.Equal(m2, proofs.ExpectedServerProof) {
|
||||
return cookie{}, fmt.Errorf("%w: expected %x got %x",
|
||||
ErrServerProofNotValid, proofs.ExpectedServerProof, m2)
|
||||
}
|
||||
|
||||
const successCode = 1000
|
||||
switch {
|
||||
case auth.Code != successCode:
|
||||
return cookie{}, fmt.Errorf("%w: expected %d got %d",
|
||||
ErrCodeNotSuccess, successCode, auth.Code)
|
||||
case auth.UID != unauthCookie.uid:
|
||||
return cookie{}, fmt.Errorf("%w: expected %s got %s",
|
||||
ErrUIDMismatch, unauthCookie.uid, auth.UID)
|
||||
case auth.TwoFactor != 0:
|
||||
return cookie{}, fmt.Errorf("%w", ErrTwoFANotSupported)
|
||||
case !slices.Contains(auth.Scopes, "vpn"):
|
||||
return cookie{}, fmt.Errorf("%w: in %v", ErrVPNScopeNotFound, auth.Scopes)
|
||||
}
|
||||
|
||||
for _, setCookieHeader := range response.Header.Values("Set-Cookie") {
|
||||
parts := strings.Split(setCookieHeader, ";")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "AUTH-"+unauthCookie.uid+"=") {
|
||||
authCookie = unauthCookie
|
||||
authCookie.token = strings.TrimPrefix(part, "AUTH-"+unauthCookie.uid+"=")
|
||||
return authCookie, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cookie{}, fmt.Errorf("%w: in HTTP headers %s",
|
||||
ErrAuthCookieNotFound, httpHeadersToString(response.Header))
|
||||
}
|
||||
|
||||
// generateLettersDigits mimicing Proton's own random string generator:
|
||||
// https://github.com/ProtonMail/WebClients/blob/e4d7e4ab9babe15b79a131960185f9f8275512cd/packages/utils/generateLettersDigits.ts
|
||||
func generateLettersDigits(rng *rand.ChaCha8, length uint) string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
return generateFromCharset(rng, length, charset)
|
||||
}
|
||||
|
||||
func generateFromCharset(rng *rand.ChaCha8, length uint, charset string) string {
|
||||
result := make([]byte, length)
|
||||
randomBytes := make([]byte, length)
|
||||
_, _ = rng.Read(randomBytes)
|
||||
for i := range length {
|
||||
result[i] = charset[int(randomBytes[i])%len(charset)]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func httpHeadersToString(headers http.Header) string {
|
||||
var builder strings.Builder
|
||||
first := true
|
||||
for key, values := range headers {
|
||||
for _, value := range values {
|
||||
if !first {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("%s: %s", key, value))
|
||||
first = false
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
|
||||
type apiData struct {
|
||||
LogicalServers []logicalServer `json:"LogicalServers"`
|
||||
@@ -580,25 +33,25 @@ type physicalServer struct {
|
||||
X25519PublicKey string `json:"X25519PublicKey"`
|
||||
}
|
||||
|
||||
func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
||||
data apiData, err error,
|
||||
) {
|
||||
const url = "https://account.proton.me/api/vpn/logicals"
|
||||
const url = "https://api.protonmail.ch/vpn/logicals"
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
c.setHeaders(request, cookie)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(response.Body)
|
||||
return data, buildError(response.StatusCode, b)
|
||||
return data, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
||||
response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
@@ -606,31 +59,9 @@ func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
||||
return data, fmt.Errorf("decoding response body: %w", err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
|
||||
func buildError(httpCode int, body []byte) error {
|
||||
prettyCode := http.StatusText(httpCode)
|
||||
var protonError struct {
|
||||
Code *int `json:"Code,omitempty"`
|
||||
Error *string `json:"Error,omitempty"`
|
||||
Details map[string]string `json:"Details"`
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(&protonError)
|
||||
if err != nil || protonError.Error == nil || protonError.Code == nil {
|
||||
return fmt.Errorf("%w: %s: %s",
|
||||
ErrHTTPStatusCodeNotOK, prettyCode, body)
|
||||
}
|
||||
|
||||
details := make([]string, 0, len(protonError.Details))
|
||||
for key, value := range protonError.Details {
|
||||
details = append(details, fmt.Sprintf("%s: %s", key, value))
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s: %s (code %d with details: %s)",
|
||||
ErrHTTPStatusCodeNotOK, prettyCode, *protonError.Error, *protonError.Code, strings.Join(details, ", "))
|
||||
}
|
||||
|
||||
@@ -13,26 +13,9 @@ import (
|
||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||
servers []models.Server, err error,
|
||||
) {
|
||||
switch {
|
||||
case u.email == "":
|
||||
return nil, fmt.Errorf("%w: email is empty", common.ErrCredentialsMissing)
|
||||
case u.password == "":
|
||||
return nil, fmt.Errorf("%w: password is empty", common.ErrCredentialsMissing)
|
||||
}
|
||||
|
||||
apiClient, err := newAPIClient(ctx, u.client)
|
||||
data, err := fetchAPI(ctx, u.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating API client: %w", err)
|
||||
}
|
||||
|
||||
cookie, err := apiClient.authenticate(ctx, u.email, u.password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authentifying with Proton: %w", err)
|
||||
}
|
||||
|
||||
data, err := apiClient.fetchServers(ctx, cookie)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching logical servers: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
countryCodes := constants.CountryCodes()
|
||||
|
||||
@@ -7,17 +7,13 @@ import (
|
||||
)
|
||||
|
||||
type Updater struct {
|
||||
client *http.Client
|
||||
email string
|
||||
password string
|
||||
warner common.Warner
|
||||
client *http.Client
|
||||
warner common.Warner
|
||||
}
|
||||
|
||||
func New(client *http.Client, warner common.Warner, email, password string) *Updater {
|
||||
func New(client *http.Client, warner common.Warner) *Updater {
|
||||
return &Updater{
|
||||
client: client,
|
||||
email: email,
|
||||
password: password,
|
||||
warner: warner,
|
||||
client: client,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// getMostRecentStableTag finds the most recent proton-account stable tag version,
|
||||
// in order to use it in the x-pm-appversion http request header. Because if we do
|
||||
// fall behind on versioning, Proton doesn't like it because they like to create
|
||||
// complications where there is no need for it. Hence this function.
|
||||
func getMostRecentStableTag(ctx context.Context, client *http.Client) (version string, err error) {
|
||||
page := 1
|
||||
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
||||
for ctx.Err() == nil {
|
||||
url := "https://api.github.com/repos/ProtonMail/WebClients/tags?per_page=30&page=" + fmt.Sprint(page)
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
request.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("%w: %s: %s", ErrHTTPStatusCodeNotOK, response.Status, data)
|
||||
}
|
||||
|
||||
var tags []struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
err = json.Unmarshal(data, &tags)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding JSON response: %w", err)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if !regexVersion.MatchString(tag.Name) {
|
||||
continue
|
||||
}
|
||||
version := "web-account@" + strings.TrimPrefix(tag.Name, "proton-account@")
|
||||
return version, nil
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w (queried %d pages)", context.Canceled, page)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ type Extractor interface {
|
||||
func NewProviders(storage Storage, timeNow func() time.Time,
|
||||
updaterWarner common.Warner, client *http.Client, unzipper common.Unzipper,
|
||||
parallelResolver common.ParallelResolver, ipFetcher common.IPFetcher,
|
||||
extractor custom.Extractor, credentials settings.Updater,
|
||||
extractor custom.Extractor,
|
||||
) *Providers {
|
||||
randSource := rand.NewSource(timeNow().UnixNano())
|
||||
|
||||
@@ -75,7 +75,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
||||
providers.Privado: privado.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
|
||||
providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword),
|
||||
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner),
|
||||
providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -15,6 +15,7 @@ type infoWarner interface {
|
||||
|
||||
type infoer interface {
|
||||
Info(s string)
|
||||
Infof(format string, args ...any)
|
||||
}
|
||||
|
||||
type warner interface {
|
||||
|
||||
@@ -13,9 +13,6 @@ import (
|
||||
func Read(filepath string) (settings Settings, err error) {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return Settings{}, nil
|
||||
}
|
||||
return settings, fmt.Errorf("opening file: %w", err)
|
||||
}
|
||||
decoder := toml.NewDecoder(file)
|
||||
|
||||
@@ -38,7 +38,7 @@ func (m *logMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
m.childHandler.ServeHTTP(statefulWriter, r)
|
||||
duration := m.timeNow().Sub(tStart)
|
||||
m.logger.Info(strconv.Itoa(statefulWriter.statusCode) + " " +
|
||||
r.Method + " " + r.URL.String() +
|
||||
r.Method + " " + r.RequestURI +
|
||||
" wrote " + strconv.Itoa(statefulWriter.length) + "B to " +
|
||||
r.RemoteAddr + " in " + duration.String())
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/httpserver"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
@@ -17,8 +19,12 @@ func New(ctx context.Context, address string, logEnabled bool, logger Logger,
|
||||
server *httpserver.Server, err error,
|
||||
) {
|
||||
authSettings, err := auth.Read(authConfigPath)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist): // no auth file present
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("reading auth settings: %w", err)
|
||||
default:
|
||||
logger.Infof("read %d roles from authentication file", len(authSettings.Roles))
|
||||
}
|
||||
authSettings.SetDefaults()
|
||||
err = authSettings.Validate()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package storage
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Logger
|
||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Infoer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Logger)
|
||||
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Infoer)
|
||||
|
||||
// Package storage is a generated GoMock package.
|
||||
package storage
|
||||
@@ -10,49 +10,37 @@ import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
// MockInfoer is a mock of Infoer interface.
|
||||
type MockInfoer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
recorder *MockInfoerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
// MockInfoerMockRecorder is the mock recorder for MockInfoer.
|
||||
type MockInfoerMockRecorder struct {
|
||||
mock *MockInfoer
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
// NewMockInfoer creates a new mock instance.
|
||||
func NewMockInfoer(ctrl *gomock.Controller) *MockInfoer {
|
||||
mock := &MockInfoer{ctrl: ctrl}
|
||||
mock.recorder = &MockInfoerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
func (m *MockInfoer) EXPECT() *MockInfoerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Info mocks base method.
|
||||
func (m *MockLogger) Info(arg0 string) {
|
||||
func (m *MockInfoer) 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 {
|
||||
func (mr *MockInfoerMockRecorder) 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)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockInfoer)(nil).Info), arg0)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger := NewMockInfoer(ctrl)
|
||||
var previousLogCall *gomock.Call
|
||||
for _, logged := range testCase.logged {
|
||||
call := logger.EXPECT().Info(logged)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,20 +13,18 @@ type Storage struct {
|
||||
// the embedded JSON file on every call to the
|
||||
// SyncServers method.
|
||||
hardcodedServers models.AllServers
|
||||
logger Logger
|
||||
logger Infoer
|
||||
filepath string
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
type Infoer interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
}
|
||||
|
||||
// New creates a new storage and reads the servers from the
|
||||
// embedded servers file and the file on disk.
|
||||
// Passing an empty filepath disables the reading and writing of
|
||||
// servers.
|
||||
func New(logger Logger, filepath string) (storage *Storage, err error) {
|
||||
// Passing an empty filepath disables writing servers to a file.
|
||||
func New(logger Infoer, filepath string) (storage *Storage, err error) {
|
||||
// A unit test prevents any error from being returned
|
||||
// and ensures all providers are part of the servers returned.
|
||||
hardcodedServers, _ := parseHardcodedServers()
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *Storage) syncServers() (err error) {
|
||||
|
||||
err = s.flushToFile(s.filepath)
|
||||
if err != nil {
|
||||
s.logger.Warn("failed writing servers to file: " + err.Error())
|
||||
return fmt.Errorf("writing servers to file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func (u *Updater) updateProvider(ctx context.Context, provider Provider,
|
||||
u.logger.Warn("note: if running the update manually, you can use the flag " +
|
||||
"-minratio to allow the update to succeed with less servers found")
|
||||
}
|
||||
return fmt.Errorf("getting %s servers: %w", providerName, err)
|
||||
return fmt.Errorf("getting servers: %w", err)
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
|
||||
@@ -2,11 +2,9 @@ package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
@@ -50,22 +48,22 @@ func (u *Updater) UpdateServers(ctx context.Context, providers []string,
|
||||
// TODO support servers offering only TCP or only UDP
|
||||
// for NordVPN and PureVPN
|
||||
err := u.updateProvider(ctx, fetcher, minRatio)
|
||||
switch {
|
||||
case err == nil:
|
||||
if err == nil {
|
||||
continue
|
||||
case errors.Is(err, common.ErrCredentialsMissing):
|
||||
u.logger.Warn(err.Error() + " - skipping update for " + providerName)
|
||||
continue
|
||||
case len(providers) == 1:
|
||||
// return the only error for the single provider.
|
||||
return err
|
||||
case ctx.Err() != nil:
|
||||
// stop updating other providers if context is done
|
||||
return ctx.Err()
|
||||
default: // error encountered updating one of multiple providers
|
||||
// Log the error and continue updating the next provider.
|
||||
u.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
// return the only error for the single provider.
|
||||
if len(providers) == 1 {
|
||||
return err
|
||||
}
|
||||
|
||||
// stop updating the next providers if context is canceled.
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
|
||||
// Log the error and continue updating the next provider.
|
||||
u.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user