mirror of
https://github.com/qdm12/gluetun.git
synced 2026-08-01 18:03:20 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01e9274f7b | |||
| daff23bfb3 | |||
| aa6d26e062 | |||
| b2859d5a06 | |||
| ad8b0657cb | |||
| c930a4e1be | |||
| 22834e9477 | |||
| 62c2679da2 | |||
| 5e9ae9fa1f | |||
| 0f19bcfebd | |||
| 83fc91d3c6 | |||
| 4adeec8223 | |||
| 64bfbaa45d |
@@ -1,2 +1,2 @@
|
|||||||
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
FROM qmcgaw/godevcontainer:v0.20-alpine
|
||||||
RUN apk add wireguard-tools htop openssl
|
RUN apk add wireguard-tools htop openssl
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ It works on Linux, Windows (WSL2) and OSX.
|
|||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
|
1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker.
|
||||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
1. 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
|
## Customization
|
||||||
|
|
||||||
For any customization to take effect, you should "rebuild and reopen":
|
For any customization to take effect, you should "rebuild and reopen":
|
||||||
|
|
||||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
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:
|
Changes you can make are notably:
|
||||||
|
|
||||||
|
|||||||
@@ -82,9 +82,6 @@
|
|||||||
"gopls": {
|
"gopls": {
|
||||||
"usePlaceholders": false,
|
"usePlaceholders": false,
|
||||||
"staticcheck": true,
|
"staticcheck": true,
|
||||||
"ui.diagnostic.analyses": {
|
|
||||||
"ST1000": false
|
|
||||||
},
|
|
||||||
"formatting.gofumpt": true,
|
"formatting.gofumpt": true,
|
||||||
},
|
},
|
||||||
"go.lintTool": "golangci-lint",
|
"go.lintTool": "golangci-lint",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ body:
|
|||||||
- VPNSecure.me
|
- VPNSecure.me
|
||||||
- VPNUnlimited
|
- VPNUnlimited
|
||||||
- VyprVPN
|
- VyprVPN
|
||||||
|
- WeVPN
|
||||||
- Windscribe
|
- Windscribe
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -6,35 +6,12 @@ labels: ":bulb: New provider"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Important notes:
|
One of the following is required:
|
||||||
|
|
||||||
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
||||||
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||||
- 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
|
- 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.
|
||||||
|
|||||||
@@ -86,6 +86,8 @@
|
|||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ Vyprvpn"
|
- name: "☁️ Vyprvpn"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ WeVPN"
|
||||||
|
color: "cfe8d4"
|
||||||
- name: "☁️ Windscribe"
|
- name: "☁️ Windscribe"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
# Description
|
|
||||||
|
|
||||||
<!-- Please describe the reason for the changes being proposed. -->
|
|
||||||
|
|
||||||
# Issue
|
|
||||||
|
|
||||||
<!-- Please link to the issue(s) this change relates to. -->
|
|
||||||
|
|
||||||
# Assertions
|
|
||||||
|
|
||||||
* [ ] I am aware that 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/)
|
|
||||||
+12
-47
@@ -37,7 +37,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: "1"
|
DOCKER_BUILDKIT: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: reviewdog/action-misspell@v1
|
- uses: reviewdog/action-misspell@v1
|
||||||
with:
|
with:
|
||||||
@@ -66,36 +66,6 @@ jobs:
|
|||||||
- name: Build final image
|
- name: Build final image
|
||||||
run: docker build -t final-image .
|
run: docker build -t final-image .
|
||||||
|
|
||||||
verify-private:
|
|
||||||
if: |
|
|
||||||
github.repository == 'passteque/gluetun' &&
|
|
||||||
(
|
|
||||||
github.event_name == 'push' ||
|
|
||||||
github.event_name == 'release' ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
|
||||||
)
|
|
||||||
needs: [verify]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: secrets
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- run: docker build -t qmcgaw/gluetun .
|
|
||||||
|
|
||||||
- name: Setup Go for CI utility
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: ci/go.mod
|
|
||||||
|
|
||||||
- name: Build utility
|
|
||||||
run: go build -C ./ci -o runner ./cmd/main.go
|
|
||||||
|
|
||||||
- name: Run Gluetun container with Mullvad configuration
|
|
||||||
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{ secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
|
|
||||||
|
|
||||||
- name: Run Gluetun container with ProtonVPN configuration
|
|
||||||
run: echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner protonvpn
|
|
||||||
|
|
||||||
codeql:
|
codeql:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -103,33 +73,32 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version: "^1.23"
|
||||||
- uses: github/codeql-action/init@v4
|
- uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: go
|
languages: go
|
||||||
- uses: github/codeql-action/autobuild@v4
|
- uses: github/codeql-action/autobuild@v3
|
||||||
- uses: github/codeql-action/analyze@v4
|
- uses: github/codeql-action/analyze@v3
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
if: |
|
if: |
|
||||||
github.repository == 'passteque/gluetun' &&
|
github.repository == 'qdm12/gluetun' &&
|
||||||
(
|
(
|
||||||
github.event_name == 'push' ||
|
github.event_name == 'push' ||
|
||||||
github.event_name == 'release' ||
|
github.event_name == 'release' ||
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||||
)
|
)
|
||||||
needs: [verify, verify-private, codeql]
|
needs: [verify, codeql]
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: secrets
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# extract metadata (tags, labels) for Docker
|
# extract metadata (tags, labels) for Docker
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
@@ -162,15 +131,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: qdm12
|
username: qdm12
|
||||||
password: ${{ secrets.GHCR_PAT }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Short commit
|
- name: Short commit
|
||||||
id: shortcommit
|
id: shortcommit
|
||||||
run: |
|
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||||
# Use the PR head SHA if it exists, otherwise fallback to GITHUB_SHA
|
|
||||||
FULL_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
|
|
||||||
SHORT_SHA=
|
|
||||||
echo "value=$(echo $FULL_SHA | cut -c1-7)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push final image
|
- name: Build and push final image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: peter-evans/create-or-update-comment@v5
|
- uses: peter-evans/create-or-update-comment@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"retryOn429": false,
|
"retryOn429": false,
|
||||||
"fallbackRetryDelay": "30s",
|
"fallbackRetryDelay": "30s",
|
||||||
"aliveStatusCodes": [
|
"aliveStatusCodes": [
|
||||||
200,
|
200
|
||||||
429
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- uses: crazy-max/ghaction-github-labeler@v5
|
- uses: crazy-max/ghaction-github-labeler@v5
|
||||||
with:
|
with:
|
||||||
yaml-file: .github/labels.yml
|
yaml-file: .github/labels.yml
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: DavidAnson/markdownlint-cli2-action@v21
|
- uses: DavidAnson/markdownlint-cli2-action@v18
|
||||||
with:
|
with:
|
||||||
globs: "**.md"
|
globs: "**.md"
|
||||||
config: .markdownlint-cli2.jsonc
|
config: .markdownlint.json
|
||||||
|
|
||||||
- uses: reviewdog/action-misspell@v1
|
- uses: reviewdog/action-misspell@v1
|
||||||
with:
|
with:
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
config-file: .github/workflows/configs/mlc-config.json
|
config-file: .github/workflows/configs/mlc-config.json
|
||||||
|
|
||||||
- uses: peter-evans/dockerhub-description@v4
|
- uses: peter-evans/dockerhub-description@v4
|
||||||
if: github.repository == 'passteque/gluetun' && github.event_name == 'push'
|
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
|
||||||
with:
|
with:
|
||||||
username: qmcgaw
|
username: qmcgaw
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: peter-evans/create-or-update-comment@v5
|
- uses: peter-evans/create-or-update-comment@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
|||||||
+28
-67
@@ -1,73 +1,30 @@
|
|||||||
version: "2"
|
linters-settings:
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
|
||||||
formatters:
|
issues:
|
||||||
enable:
|
exclude-rules:
|
||||||
- gci
|
- path: _test\.go
|
||||||
- gofumpt
|
linters:
|
||||||
- goimports
|
- dupl
|
||||||
exclusions:
|
- err113
|
||||||
generated: lax
|
- containedctx
|
||||||
paths:
|
- maintidx
|
||||||
- third_party$
|
- path: "internal\\/server\\/.+\\.go"
|
||||||
- builtin$
|
linters:
|
||||||
- examples$
|
- 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
|
||||||
|
|
||||||
linters:
|
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:
|
|
||||||
- lll
|
|
||||||
source: "^// https://.+$"
|
|
||||||
- linters:
|
|
||||||
- err113
|
|
||||||
- mnd
|
|
||||||
path: ci\/.+\.go
|
|
||||||
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
enable:
|
enable:
|
||||||
# - cyclop
|
# - cyclop
|
||||||
# - errorlint
|
# - errorlint
|
||||||
@@ -88,6 +45,7 @@ linters:
|
|||||||
- exhaustive
|
- exhaustive
|
||||||
- fatcontext
|
- fatcontext
|
||||||
- forcetypeassert
|
- forcetypeassert
|
||||||
|
- gci
|
||||||
- gocheckcompilerdirectives
|
- gocheckcompilerdirectives
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
@@ -96,7 +54,9 @@ linters:
|
|||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godot
|
- godot
|
||||||
|
- gofumpt
|
||||||
- goheader
|
- goheader
|
||||||
|
- goimports
|
||||||
- gomoddirectives
|
- gomoddirectives
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
@@ -129,6 +89,7 @@ linters:
|
|||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- tagalign
|
- tagalign
|
||||||
|
- tenv
|
||||||
- thelper
|
- thelper
|
||||||
- tparallel
|
- tparallel
|
||||||
- unconvert
|
- unconvert
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"default": true,
|
|
||||||
"MD013": false,
|
|
||||||
},
|
|
||||||
"ignores": [
|
|
||||||
".github/pull_request_template.md"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"MD013": false
|
||||||
|
}
|
||||||
Vendored
+35
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Update a VPN provider servers data",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"program": "cmd/gluetun/main.go",
|
||||||
|
"args": [
|
||||||
|
"update",
|
||||||
|
"${input:updateMode}",
|
||||||
|
"-providers",
|
||||||
|
"${input:provider}"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "provider",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Please enter a provider (or comma separated list of providers)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "updateMode",
|
||||||
|
"type": "pickString",
|
||||||
|
"description": "Update mode to use",
|
||||||
|
"options": [
|
||||||
|
"-maintainer",
|
||||||
|
"-enduser"
|
||||||
|
],
|
||||||
|
"default": "-maintainer"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
-51
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "Update a VPN provider servers data",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "go",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"./cmd/gluetun/main.go",
|
|
||||||
"update",
|
|
||||||
"${input:updateMode}",
|
|
||||||
"-providers",
|
|
||||||
"${input:provider}"
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Add a Gluetun Github Git remote",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "git",
|
|
||||||
"args": [
|
|
||||||
"remote",
|
|
||||||
"add",
|
|
||||||
"${input:githubRemoteUsername}",
|
|
||||||
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+22
-26
@@ -1,14 +1,14 @@
|
|||||||
ARG ALPINE_VERSION=3.22
|
ARG ALPINE_VERSION=3.20
|
||||||
ARG GO_ALPINE_VERSION=3.22
|
ARG GO_ALPINE_VERSION=3.20
|
||||||
ARG GO_VERSION=1.25
|
ARG GO_VERSION=1.23
|
||||||
ARG XCPUTRANSLATE_VERSION=v0.9.0
|
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||||
ARG GOLANGCI_LINT_VERSION=v2.4.0
|
ARG GOLANGCI_LINT_VERSION=v1.61.0
|
||||||
ARG MOCKGEN_VERSION=v1.6.0
|
ARG MOCKGEN_VERSION=v1.6.0
|
||||||
ARG BUILDPLATFORM=linux/amd64
|
ARG BUILDPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
||||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||||
@@ -32,7 +32,7 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
|
|||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS lint
|
FROM --platform=${BUILDPLATFORM} base AS lint
|
||||||
COPY .golangci.yml ./
|
COPY .golangci.yml ./
|
||||||
RUN golangci-lint run
|
RUN golangci-lint run --timeout=10m
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS mocks
|
FROM --platform=${BUILDPLATFORM} base AS mocks
|
||||||
RUN git init && \
|
RUN git init && \
|
||||||
@@ -163,23 +163,20 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
LOG_LEVEL=info \
|
LOG_LEVEL=info \
|
||||||
# Health
|
# Health
|
||||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||||
HEALTH_TARGET_ADDRESSES=cloudflare.com:443,github.com:443 \
|
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||||
HEALTH_ICMP_TARGET_IPS=1.1.1.1,8.8.8.8 \
|
HEALTH_SUCCESS_WAIT_DURATION=5s \
|
||||||
HEALTH_SMALL_CHECK_TYPE=icmp \
|
HEALTH_VPN_DURATION_INITIAL=6s \
|
||||||
HEALTH_RESTART_VPN=on \
|
HEALTH_VPN_DURATION_ADDITION=5s \
|
||||||
# DNS
|
# DNS over TLS
|
||||||
DNS_SERVER=on \
|
DOT=on \
|
||||||
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
DOT_PROVIDERS=cloudflare \
|
||||||
DNS_UPSTREAM_RESOLVERS=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 \
|
||||||
DNS_BLOCK_IPS= \
|
DOT_CACHING=on \
|
||||||
DNS_BLOCK_IP_PREFIXES= \
|
DOT_IPV6=off \
|
||||||
DNS_CACHING=on \
|
|
||||||
DNS_UPSTREAM_IPV6=off \
|
|
||||||
BLOCK_MALICIOUS=on \
|
BLOCK_MALICIOUS=on \
|
||||||
BLOCK_SURVEILLANCE=off \
|
BLOCK_SURVEILLANCE=off \
|
||||||
BLOCK_ADS=off \
|
BLOCK_ADS=off \
|
||||||
DNS_UNBLOCK_HOSTNAMES= \
|
UNBLOCK= \
|
||||||
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
|
||||||
DNS_UPDATE_PERIOD=24h \
|
DNS_UPDATE_PERIOD=24h \
|
||||||
DNS_ADDRESS=127.0.0.1 \
|
DNS_ADDRESS=127.0.0.1 \
|
||||||
DNS_KEEP_NAMESERVER=off \
|
DNS_KEEP_NAMESERVER=off \
|
||||||
@@ -203,7 +200,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
HTTP_CONTROL_SERVER_LOG=on \
|
HTTP_CONTROL_SERVER_LOG=on \
|
||||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||||
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||||
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
|
|
||||||
# Server data updater
|
# Server data updater
|
||||||
UPDATER_PERIOD=0 \
|
UPDATER_PERIOD=0 \
|
||||||
UPDATER_MIN_RATIO=0.8 \
|
UPDATER_MIN_RATIO=0.8 \
|
||||||
@@ -225,8 +221,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# Extras
|
# Extras
|
||||||
VERSION_INFORMATION=on \
|
VERSION_INFORMATION=on \
|
||||||
TZ= \
|
TZ= \
|
||||||
PUID=1000 \
|
PUID= \
|
||||||
PGID=1000
|
PGID=
|
||||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
ENTRYPOINT ["/gluetun-entrypoint"]
|
||||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
||||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# Gluetun VPN client
|
# Gluetun VPN client
|
||||||
|
|
||||||
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
|
Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||||
|
|
||||||
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -28,6 +26,7 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
|||||||
[](https://github.com/qdm12/gluetun/issues)
|
[](https://github.com/qdm12/gluetun/issues)
|
||||||
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
||||||
|
|
||||||
|
[](https://github.com/qdm12/gluetun)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -57,12 +56,12 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Alpine 3.22 for a small Docker image of 41.1MB
|
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
||||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers
|
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||||
- Supports OpenVPN for all providers listed
|
- Supports OpenVPN for all providers listed
|
||||||
- Supports Wireguard both kernelspace and userspace
|
- Supports Wireguard both kernelspace and userspace
|
||||||
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
||||||
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
||||||
- DNS over TLS baked in with service provider(s) of your choice
|
- DNS over TLS baked in with service provider(s) of your choice
|
||||||
@@ -89,7 +88,7 @@ Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
|||||||
Here's a docker-compose.yml for the laziest:
|
Here's a docker-compose.yml for the laziest:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
---
|
version: "3"
|
||||||
services:
|
services:
|
||||||
gluetun:
|
gluetun:
|
||||||
image: qmcgaw/gluetun
|
image: qmcgaw/gluetun
|
||||||
@@ -125,10 +124,6 @@ services:
|
|||||||
|
|
||||||
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
||||||
|
|
||||||
## Fun graphs
|
|
||||||
|
|
||||||
[](https://www.star-history.com/#qdm12/gluetun&type=date&legend=top-left)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[](https://github.com/qdm12/gluetun/blob/master/LICENSE)
|
[](https://github.com/qdm12/gluetun/blob/master/LICENSE)
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/ci/internal"
|
|
||||||
"github.com/qdm12/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
logger := log.New()
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
logger.Error("Usage: " + os.Args[0] + " <command>")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "mullvad":
|
|
||||||
err = internal.MullvadTest(ctx, logger)
|
|
||||||
case "protonvpn":
|
|
||||||
err = internal.ProtonVPNTest(ctx, logger)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unknown command: %s", os.Args[1])
|
|
||||||
}
|
|
||||||
stop()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logger.Info("test completed successfully")
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
module github.com/qdm12/gluetun/ci
|
|
||||||
|
|
||||||
go 1.25.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/docker/docker v28.5.1+incompatible
|
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
|
||||||
github.com/moby/term v0.5.2 // indirect
|
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/qdm12/log v0.1.0 // indirect
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
|
||||||
golang.org/x/time v0.14.0 // indirect
|
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
|
||||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
|
||||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
|
||||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
|
||||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
|
||||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
|
||||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
|
||||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MullvadTest(ctx context.Context, logger Logger) error {
|
|
||||||
expectedSecrets := []string{
|
|
||||||
"Wireguard private key",
|
|
||||||
"Wireguard address",
|
|
||||||
}
|
|
||||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading secrets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
env := []string{
|
|
||||||
"VPN_SERVICE_PROVIDER=mullvad",
|
|
||||||
"VPN_TYPE=wireguard",
|
|
||||||
"LOG_LEVEL=debug",
|
|
||||||
"SERVER_COUNTRIES=USA",
|
|
||||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
|
||||||
"WIREGUARD_ADDRESSES=" + secrets[1],
|
|
||||||
}
|
|
||||||
return simpleTest(ctx, env, logger)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ProtonVPNTest(ctx context.Context, logger Logger) error {
|
|
||||||
expectedSecrets := []string{
|
|
||||||
"Wireguard private key",
|
|
||||||
}
|
|
||||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading secrets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
env := []string{
|
|
||||||
"VPN_SERVICE_PROVIDER=protonvpn",
|
|
||||||
"VPN_TYPE=wireguard",
|
|
||||||
"LOG_LEVEL=debug",
|
|
||||||
"SERVER_COUNTRIES=United States",
|
|
||||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
|
||||||
}
|
|
||||||
return simpleTest(ctx, env, logger)
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger interface {
|
|
||||||
Info(msg string)
|
|
||||||
Infof(format string, args ...any)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readSecrets(ctx context.Context, expectedSecrets []string,
|
|
||||||
logger Logger,
|
|
||||||
) (lines []string, err error) {
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
lines = make([]string, 0, len(expectedSecrets))
|
|
||||||
|
|
||||||
for i := range expectedSecrets {
|
|
||||||
logger.Infof("🤫 reading %s from Stdin...", expectedSecrets[i])
|
|
||||||
if !scanner.Scan() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
|
||||||
logger.Infof("🤫 %s secret read successfully", expectedSecrets[i])
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("reading secrets from stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(lines) < len(expectedSecrets) {
|
|
||||||
return nil, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
|
|
||||||
len(expectedSecrets), len(lines))
|
|
||||||
}
|
|
||||||
for i, line := range lines {
|
|
||||||
if line == "" {
|
|
||||||
return nil, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines, nil
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/api/types/network"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ptrTo[T any](v T) *T { return &v }
|
|
||||||
|
|
||||||
func simpleTest(ctx context.Context, env []string, logger Logger) error {
|
|
||||||
const timeout = 30 * 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: env,
|
|
||||||
}
|
|
||||||
hostConfig := &container.HostConfig{
|
|
||||||
AutoRemove: true,
|
|
||||||
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
|
|
||||||
}
|
|
||||||
networkConfig := (*network.NetworkingConfig)(nil)
|
|
||||||
platform := (*v1.Platform)(nil)
|
|
||||||
const containerName = "" // auto-generated name
|
|
||||||
|
|
||||||
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating container: %w", err)
|
|
||||||
}
|
|
||||||
for _, warning := range response.Warnings {
|
|
||||||
fmt.Println("Warning during container creation:", warning)
|
|
||||||
}
|
|
||||||
containerID := response.ID
|
|
||||||
defer stopContainer(client, containerID)
|
|
||||||
|
|
||||||
beforeStartTime := time.Now()
|
|
||||||
|
|
||||||
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return waitForLogLine(ctx, client, containerID, beforeStartTime, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopContainer(client *client.Client, containerID string) {
|
|
||||||
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
|
|
||||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
|
||||||
defer stopCancel()
|
|
||||||
|
|
||||||
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("failed to stop container:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
|
|
||||||
|
|
||||||
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
|
|
||||||
beforeStartTime time.Time, logger Logger,
|
|
||||||
) error {
|
|
||||||
logOptions := container.LogsOptions{
|
|
||||||
ShowStdout: true,
|
|
||||||
Follow: true,
|
|
||||||
Since: beforeStartTime.Format(time.RFC3339Nano),
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting container logs: %w", err)
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
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(logger, linesSeen)
|
|
||||||
return fmt.Errorf("reading log stream: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The scanner is either done or cannot read because of EOF
|
|
||||||
logger.Info("the log scanner stopped")
|
|
||||||
logSeenLines(logger, 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(logger Logger, lines []string) {
|
|
||||||
fmt.Println("Logs seen so far:")
|
|
||||||
for _, line := range lines {
|
|
||||||
fmt.Println(" " + line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+39
-64
@@ -15,8 +15,6 @@ import (
|
|||||||
_ "time/tzdata"
|
_ "time/tzdata"
|
||||||
|
|
||||||
_ "github.com/breml/rootcerts"
|
_ "github.com/breml/rootcerts"
|
||||||
"github.com/qdm12/dns/v2/pkg/doh"
|
|
||||||
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
|
||||||
"github.com/qdm12/gluetun/internal/alpine"
|
"github.com/qdm12/gluetun/internal/alpine"
|
||||||
"github.com/qdm12/gluetun/internal/cli"
|
"github.com/qdm12/gluetun/internal/cli"
|
||||||
"github.com/qdm12/gluetun/internal/command"
|
"github.com/qdm12/gluetun/internal/command"
|
||||||
@@ -24,7 +22,6 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
copenvpn "github.com/qdm12/gluetun/internal/constants/openvpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/dns"
|
"github.com/qdm12/gluetun/internal/dns"
|
||||||
"github.com/qdm12/gluetun/internal/firewall"
|
"github.com/qdm12/gluetun/internal/firewall"
|
||||||
"github.com/qdm12/gluetun/internal/healthcheck"
|
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||||
@@ -41,6 +38,7 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/server"
|
"github.com/qdm12/gluetun/internal/server"
|
||||||
"github.com/qdm12/gluetun/internal/shadowsocks"
|
"github.com/qdm12/gluetun/internal/shadowsocks"
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
|
"github.com/qdm12/gluetun/internal/tun"
|
||||||
updater "github.com/qdm12/gluetun/internal/updater/loop"
|
updater "github.com/qdm12/gluetun/internal/updater/loop"
|
||||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||||
@@ -77,6 +75,7 @@ func main() {
|
|||||||
logger := log.New(log.SetLevel(log.LevelInfo))
|
logger := log.New(log.SetLevel(log.LevelInfo))
|
||||||
|
|
||||||
args := os.Args
|
args := os.Args
|
||||||
|
tun := tun.New()
|
||||||
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
||||||
netLinker := netlink.New(netLinkDebugLogger)
|
netLinker := netlink.New(netLinkDebugLogger)
|
||||||
cli := cli.New()
|
cli := cli.New()
|
||||||
@@ -96,7 +95,7 @@ func main() {
|
|||||||
|
|
||||||
errorCh := make(chan error)
|
errorCh := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, netLinker, cmder, cli)
|
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for OS signal or run error
|
// Wait for OS signal or run error
|
||||||
@@ -143,7 +142,7 @@ var errCommandUnknown = errors.New("command is unknown")
|
|||||||
//nolint:gocognit,gocyclo,maintidx
|
//nolint:gocognit,gocyclo,maintidx
|
||||||
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||||
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
||||||
netLinker netLinker, cmder RunStarter,
|
tun Tun, netLinker netLinker, cmder RunStarter,
|
||||||
cli clier,
|
cli clier,
|
||||||
) error {
|
) error {
|
||||||
if len(args) > 1 { // cli operation
|
if len(args) > 1 { // cli operation
|
||||||
@@ -165,8 +164,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer fmt.Println(gluetunLogo)
|
|
||||||
|
|
||||||
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
|
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -263,21 +260,18 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
|
|
||||||
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
||||||
|
|
||||||
const clientTimeout = 35 * time.Second
|
const clientTimeout = 15 * time.Second
|
||||||
httpClient := &http.Client{Timeout: clientTimeout}
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
// Create configurators
|
// Create configurators
|
||||||
alpineConf := alpine.New()
|
alpineConf := alpine.New()
|
||||||
ovpnConf := openvpn.New(
|
ovpnConf := openvpn.New(
|
||||||
logger.New(log.SetComponent("openvpn configurator")),
|
logger.New(log.SetComponent("openvpn configurator")),
|
||||||
cmder, puid, pgid)
|
cmder, puid, pgid)
|
||||||
ovpnVersion := ovpnConf.Version26
|
|
||||||
if allSettings.VPN.OpenVPN.Version == copenvpn.Openvpn25 {
|
|
||||||
ovpnVersion = ovpnConf.Version25
|
|
||||||
}
|
|
||||||
|
|
||||||
err = printVersions(ctx, logger, []printVersionElement{
|
err = printVersions(ctx, logger, []printVersionElement{
|
||||||
{name: "Alpine", getVersion: alpineConf.Version},
|
{name: "Alpine", getVersion: alpineConf.Version},
|
||||||
{name: "OpenVPN", getVersion: ovpnVersion},
|
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||||
|
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
||||||
{name: "IPtables", getVersion: firewallConf.Version},
|
{name: "IPtables", getVersion: firewallConf.Version},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -335,6 +329,19 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return fmt.Errorf("adding local rules: %w", err)
|
return fmt.Errorf("adding local rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tunDevice = "/dev/net/tun"
|
||||||
|
err = tun.Check(tunDevice)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
|
||||||
|
}
|
||||||
|
logger.Info(err.Error() + "; creating it...")
|
||||||
|
err = tun.Create(tunDevice)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating tun device: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, port := range allSettings.Firewall.InputPorts {
|
for _, port := range allSettings.Firewall.InputPorts {
|
||||||
for _, defaultRoute := range defaultRoutes {
|
for _, defaultRoute := range defaultRoutes {
|
||||||
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
|
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
|
||||||
@@ -407,25 +414,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return fmt.Errorf("starting public ip loop: %w", err)
|
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)
|
|
||||||
|
|
||||||
// Note: we use a separate DoH dialer for the VPN servers data updater, separate from the
|
|
||||||
// main DNS local server to make sure no request is blocked by filters.
|
|
||||||
dohDialer, err := doh.New(doh.Settings{
|
|
||||||
UpstreamResolvers: []dnsprovider.Provider{dnsprovider.Cloudflare(), dnsprovider.Google()},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating updater DoH dialer: %w", err)
|
|
||||||
}
|
|
||||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||||
|
|
||||||
unzipper := unzip.New(httpClient)
|
unzipper := unzip.New(httpClient)
|
||||||
parallelResolver := resolver.NewParallelResolver(dohDialer)
|
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||||
@@ -433,8 +425,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
|
|
||||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||||
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
|
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
|
||||||
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
||||||
buildInfo, *allSettings.Version.Enabled)
|
buildInfo, *allSettings.Version.Enabled)
|
||||||
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
||||||
"vpn", goroutine.OptionTimeout(time.Second))
|
"vpn", goroutine.OptionTimeout(time.Second))
|
||||||
@@ -468,10 +460,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
||||||
otherGroupHandler.Add(shadowsocksHandler)
|
otherGroupHandler.Add(shadowsocksHandler)
|
||||||
|
|
||||||
|
controlServerAddress := *allSettings.ControlServer.Address
|
||||||
|
controlServerLogging := *allSettings.ControlServer.Log
|
||||||
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
||||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
|
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
||||||
logger.New(log.SetComponent("http server")),
|
logger.New(log.SetComponent("http server")),
|
||||||
|
allSettings.ControlServer.AuthFilePath,
|
||||||
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||||
storage, ipv6Supported)
|
storage, ipv6Supported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -482,6 +477,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
<-httpServerReady
|
<-httpServerReady
|
||||||
controlGroupHandler.Add(httpServerHandler)
|
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",
|
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
||||||
order.OptionTimeout(totalShutdownTimeout),
|
order.OptionTimeout(totalShutdownTimeout),
|
||||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||||
@@ -548,7 +549,7 @@ type netLinker interface {
|
|||||||
Router
|
Router
|
||||||
Ruler
|
Ruler
|
||||||
Linker
|
Linker
|
||||||
IsWireguardSupported() bool
|
IsWireguardSupported() (ok bool, err error)
|
||||||
IsIPv6Supported() (ok bool, err error)
|
IsIPv6Supported() (ok bool, err error)
|
||||||
PatchLoggerLevel(level log.Level)
|
PatchLoggerLevel(level log.Level)
|
||||||
}
|
}
|
||||||
@@ -591,39 +592,13 @@ type clier interface {
|
|||||||
GenKey(args []string) error
|
GenKey(args []string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Tun interface {
|
||||||
|
Check(tunDevice string) error
|
||||||
|
Create(tunDevice string) error
|
||||||
|
}
|
||||||
|
|
||||||
type RunStarter interface {
|
type RunStarter interface {
|
||||||
Run(cmd *exec.Cmd) (output string, err error)
|
Run(cmd *exec.Cmd) (output string, err error)
|
||||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||||
waitError <-chan error, err error)
|
waitError <-chan error, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const gluetunLogo = ` @@@
|
|
||||||
@@@@
|
|
||||||
@@@@@@
|
|
||||||
@@@@.@@ @@@@@@@@@@
|
|
||||||
@@@@.@@@ @@@@@@@@==@@@@
|
|
||||||
@@@.@..@@ @@@@@@@=@..==@@@@
|
|
||||||
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
|
|
||||||
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
|
|
||||||
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
|
|
||||||
@@@.--@..@@@@.-@@@@@@@==============@@@@
|
|
||||||
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
|
|
||||||
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
|
|
||||||
@@@@.@--@@@@@@@@@@================@@@@@@@
|
|
||||||
@@@..--@@*@@@@@@================@@@@+*@@
|
|
||||||
@@@.---@@.@@@@=================@@@@--@@
|
|
||||||
@@@-.---@@@@@@================@@@@*--@@@
|
|
||||||
@@@.:-#@@@@@@===============*@@@@.---@@
|
|
||||||
@@@.-------.@@@============@@@@@@.--@@@
|
|
||||||
@@@..--------:@@@=========@@@@@@@@.--@@@
|
|
||||||
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
|
|
||||||
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
|
|
||||||
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
|
|
||||||
@@@@@@=============@@@@@ @@@...------------.*@@@
|
|
||||||
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
|
|
||||||
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
|
|
||||||
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
|
|
||||||
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
|
|
||||||
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
|
|
||||||
@@@@@@@@ @@@@ @@@@
|
|
||||||
`
|
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
module github.com/qdm12/gluetun
|
module github.com/qdm12/gluetun
|
||||||
|
|
||||||
go 1.25.0
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/go-srp v0.0.7
|
github.com/ProtonMail/go-srp v0.0.7
|
||||||
github.com/breml/rootcerts v0.3.3
|
github.com/breml/rootcerts v0.2.19
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/klauspost/compress v1.18.1
|
github.com/klauspost/compress v1.17.11
|
||||||
github.com/klauspost/pgzip v1.2.6
|
github.com/klauspost/pgzip v1.2.6
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.3
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
github.com/qdm12/dns/v2 v2.0.0-rc8
|
||||||
github.com/qdm12/gosettings v0.4.4
|
github.com/qdm12/gosettings v0.4.4
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
github.com/qdm12/gosplash v0.2.0
|
github.com/qdm12/gosplash v0.2.0
|
||||||
github.com/qdm12/gotree v0.3.0
|
github.com/qdm12/gotree v0.3.0
|
||||||
github.com/qdm12/log v0.1.0
|
github.com/qdm12/log v0.1.0
|
||||||
github.com/qdm12/ss-server v0.6.0
|
github.com/qdm12/ss-server v0.6.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.11
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.2.1
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
golang.org/x/net v0.47.0
|
golang.org/x/net v0.31.0
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.30.0
|
||||||
golang.org/x/text v0.31.0
|
golang.org/x/text v0.22.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
@@ -35,7 +35,7 @@ require (
|
|||||||
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.0 // indirect
|
||||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
@@ -53,14 +53,13 @@ require (
|
|||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.60.1 // indirect
|
github.com/prometheus/common v0.60.1 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
github.com/qdm12/goservices v0.1.0 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
golang.org/x/crypto v0.45.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/tools v0.26.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/protobuf v1.35.1 // indirect
|
google.golang.org/protobuf v1.35.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1J
|
|||||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
|
github.com/breml/rootcerts v0.2.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U=
|
||||||
github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
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/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
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 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -30,8 +30,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/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 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -55,8 +55,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/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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -69,10 +69,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/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
|
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
||||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||||
@@ -89,14 +89,14 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
|
|||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
|
||||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
@@ -106,15 +106,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -122,14 +122,14 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
|||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -144,8 +144,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -155,17 +155,17 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
+4
-25
@@ -10,8 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/doh"
|
|
||||||
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
@@ -40,12 +38,12 @@ type UpdaterLogger interface {
|
|||||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||||
options := settings.Updater{}
|
options := settings.Updater{}
|
||||||
var endUserMode, maintainerMode, updateAll bool
|
var endUserMode, maintainerMode, updateAll bool
|
||||||
var dnsServer, csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
||||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||||
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
|
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
|
||||||
flagSet.StringVar(&dnsServer, "dns", "", "no longer used, your DNS will use DoH with Cloudflare and Google")
|
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
|
||||||
const defaultMinRatio = 0.8
|
const defaultMinRatio = 0.8
|
||||||
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
|
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
|
||||||
"Minimum ratio of servers to find for the update to succeed")
|
"Minimum ratio of servers to find for the update to succeed")
|
||||||
@@ -60,10 +58,6 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if dnsServer != "" {
|
|
||||||
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !endUserMode && !maintainerMode {
|
if !endUserMode && !maintainerMode {
|
||||||
return fmt.Errorf("%w", ErrModeUnspecified)
|
return fmt.Errorf("%w", ErrModeUnspecified)
|
||||||
}
|
}
|
||||||
@@ -94,30 +88,15 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
return fmt.Errorf("options validation failed: %w", err)
|
return fmt.Errorf("options validation failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serversDataPath := constants.ServersData
|
storage, err := storage.New(logger, constants.ServersData)
|
||||||
if maintainerMode {
|
|
||||||
serversDataPath = ""
|
|
||||||
}
|
|
||||||
storage, err := storage.New(logger, serversDataPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating servers storage: %w", err)
|
return fmt.Errorf("creating servers storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dohSettings := doh.Settings{
|
|
||||||
UpstreamResolvers: []dnsprovider.Provider{
|
|
||||||
dnsprovider.Cloudflare(),
|
|
||||||
dnsprovider.Google(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dnsDialer, err := doh.New(dohSettings)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating DoH dialer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientTimeout = 10 * time.Second
|
const clientTimeout = 10 * time.Second
|
||||||
httpClient := &http.Client{Timeout: clientTimeout}
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
unzipper := unzip.New(httpClient)
|
unzipper := unzip.New(httpClient)
|
||||||
parallelResolver := resolver.NewParallelResolver(dnsDialer)
|
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
||||||
nameTokenPairs := []api.NameToken{
|
nameTokenPairs := []api.NameToken{
|
||||||
{Name: string(api.IPInfo), Token: ipToken},
|
{Name: string(api.IPInfo), Token: ipToken},
|
||||||
{Name: string(api.IP2Location)},
|
{Name: string(api.IP2Location)},
|
||||||
|
|||||||
+21
-18
@@ -9,9 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Start launches a command and streams stdout and stderr to channels.
|
// Start launches a command and streams stdout and stderr to channels.
|
||||||
// stdoutLines and stderrLines channels will be closed when there is no more
|
// All the channels returned are ready only and won't be closed
|
||||||
// output to read, in order for the caller to catch all lines even after the
|
// if the command fails later.
|
||||||
// command has finished. The waitError channel returned will never be closed.
|
|
||||||
func (c *Cmder) Start(cmd *exec.Cmd) (
|
func (c *Cmder) Start(cmd *exec.Cmd) (
|
||||||
stdoutLines, stderrLines <-chan string,
|
stdoutLines, stderrLines <-chan string,
|
||||||
waitError <-chan error, startErr error,
|
waitError <-chan error, startErr error,
|
||||||
@@ -22,6 +21,7 @@ func (c *Cmder) Start(cmd *exec.Cmd) (
|
|||||||
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||||
waitError <-chan error, startErr error,
|
waitError <-chan error, startErr error,
|
||||||
) {
|
) {
|
||||||
|
stop := make(chan struct{})
|
||||||
stdoutReady := make(chan struct{})
|
stdoutReady := make(chan struct{})
|
||||||
stdoutLinesCh := make(chan string)
|
stdoutLinesCh := make(chan string)
|
||||||
stdoutDone := make(chan struct{})
|
stdoutDone := make(chan struct{})
|
||||||
@@ -33,47 +33,43 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
go streamToChannel(stdoutReady, stdoutDone, stdout, stdoutLinesCh)
|
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
|
||||||
|
|
||||||
stderr, err := cmd.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
|
close(stop)
|
||||||
<-stdoutDone
|
<-stdoutDone
|
||||||
close(stdoutLinesCh)
|
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
|
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
|
||||||
|
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
<-stdoutDone
|
|
||||||
close(stdoutLinesCh)
|
|
||||||
_ = stderr.Close()
|
_ = stderr.Close()
|
||||||
|
close(stop)
|
||||||
|
<-stdoutDone
|
||||||
<-stderrDone
|
<-stderrDone
|
||||||
close(stderrLinesCh)
|
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
waitErrorCh := make(chan error)
|
waitErrorCh := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
<-stdoutDone
|
|
||||||
close(stdoutLinesCh)
|
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
<-stderrDone
|
|
||||||
close(stderrLinesCh)
|
|
||||||
_ = stderr.Close()
|
_ = stderr.Close()
|
||||||
|
close(stop)
|
||||||
|
<-stdoutDone
|
||||||
|
<-stderrDone
|
||||||
waitErrorCh <- err
|
waitErrorCh <- err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
<-stdoutReady
|
|
||||||
<-stderrReady
|
|
||||||
|
|
||||||
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamToChannel(ready chan<- struct{}, done chan<- struct{},
|
func streamToChannel(ready chan<- struct{},
|
||||||
|
stop <-chan struct{}, done chan<- struct{},
|
||||||
stream io.Reader, lines chan<- string,
|
stream io.Reader, lines chan<- string,
|
||||||
) {
|
) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
@@ -93,5 +89,12 @@ func streamToChannel(ready chan<- struct{}, done chan<- struct{},
|
|||||||
if err == nil || errors.Is(err, os.ErrClosed) {
|
if err == nil || errors.Is(err, os.ErrClosed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lines <- "stream error: " + err.Error()
|
|
||||||
|
// ignore the error if it is stopped.
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
lines <- "stream error: " + err.Error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,48 +89,30 @@ func Test_start(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
collectAndCheckChannels(t, stdoutLines, stderrLines, waitError,
|
var stdoutIndex, stderrIndex int
|
||||||
testCase.stdout, testCase.stderr, testCase.waitErr)
|
|
||||||
|
done := false
|
||||||
|
for !done {
|
||||||
|
select {
|
||||||
|
case line := <-stdoutLines:
|
||||||
|
assert.Equal(t, testCase.stdout[stdoutIndex], line)
|
||||||
|
stdoutIndex++
|
||||||
|
case line := <-stderrLines:
|
||||||
|
assert.Equal(t, testCase.stderr[stderrIndex], line)
|
||||||
|
stderrIndex++
|
||||||
|
case err := <-waitError:
|
||||||
|
if testCase.waitErr != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.waitErr.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, len(testCase.stdout), stdoutIndex)
|
||||||
|
assert.Equal(t, len(testCase.stderr), stderrIndex)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectAndCheckChannels(t *testing.T, stdoutLines, stderrLines <-chan string,
|
|
||||||
waitError <-chan error, expectedStdout, expectedStderr []string, expectedWaitErr error,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
stdoutIndex := 0
|
|
||||||
stderrIndex := 0
|
|
||||||
|
|
||||||
done := false
|
|
||||||
for !done {
|
|
||||||
select {
|
|
||||||
case line, ok := <-stdoutLines:
|
|
||||||
if !ok {
|
|
||||||
stdoutLines = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
assert.Equal(t, expectedStdout[stdoutIndex], line)
|
|
||||||
stdoutIndex++
|
|
||||||
case line, ok := <-stderrLines:
|
|
||||||
if !ok {
|
|
||||||
stderrLines = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
assert.Equal(t, expectedStderr[stderrIndex], line)
|
|
||||||
stderrIndex++
|
|
||||||
case err := <-waitError:
|
|
||||||
if expectedWaitErr != nil {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Equal(t, expectedWaitErr.Error(), err.Error())
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, len(expectedStdout), stdoutIndex)
|
|
||||||
assert.Equal(t, len(expectedStderr), stderrIndex)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import (
|
|||||||
|
|
||||||
func readObsolete(r *reader.Reader) (warnings []string) {
|
func readObsolete(r *reader.Reader) (warnings []string) {
|
||||||
keyToMessage := map[string]string{
|
keyToMessage := map[string]string{
|
||||||
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
|
"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_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_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)
|
sortedKeys := maps.Keys(keyToMessage)
|
||||||
slices.Sort(sortedKeys)
|
slices.Sort(sortedKeys)
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"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"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
@@ -15,31 +11,10 @@ import (
|
|||||||
|
|
||||||
// DNS contains settings to configure DNS.
|
// DNS contains settings to configure DNS.
|
||||||
type DNS struct {
|
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
|
// ServerAddress is the DNS server to use inside
|
||||||
// the Go program and for the system.
|
// the Go program and for the system.
|
||||||
// It defaults to '127.0.0.1' to be used with the
|
// It defaults to '127.0.0.1' to be used with the
|
||||||
// local server. It cannot be the zero value in the internal
|
// DoT server. It cannot be the zero value in the internal
|
||||||
// state.
|
// state.
|
||||||
ServerAddress netip.Addr
|
ServerAddress netip.Addr
|
||||||
// KeepNameserver is true if the existing DNS server
|
// KeepNameserver is true if the existing DNS server
|
||||||
@@ -48,40 +23,20 @@ type DNS struct {
|
|||||||
// outside the VPN tunnel since it would go through
|
// outside the VPN tunnel since it would go through
|
||||||
// the local DNS server of your Docker/Kubernetes
|
// the local DNS server of your Docker/Kubernetes
|
||||||
// configuration, which is likely not going through the tunnel.
|
// configuration, which is likely not going through the tunnel.
|
||||||
// This will also disable the DNS forwarder server and the
|
// This will also disable the DNS over TLS server and the
|
||||||
// `ServerAddress` field will be ignored.
|
// `ServerAddress` field will be ignored.
|
||||||
// It defaults to false and cannot be nil in the
|
// It defaults to false and cannot be nil in the
|
||||||
// internal state.
|
// internal state.
|
||||||
KeepNameserver *bool
|
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) {
|
func (d DNS) validate() (err error) {
|
||||||
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
|
err = d.DoT.validate()
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("validating DoT settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -89,15 +44,9 @@ func (d DNS) validate() (err error) {
|
|||||||
|
|
||||||
func (d *DNS) Copy() (copied DNS) {
|
func (d *DNS) Copy() (copied DNS) {
|
||||||
return DNS{
|
return DNS{
|
||||||
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
|
|
||||||
UpstreamType: d.UpstreamType,
|
|
||||||
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,
|
ServerAddress: d.ServerAddress,
|
||||||
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||||
|
DoT: d.DoT.copy(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,48 +54,16 @@ func (d *DNS) Copy() (copied DNS) {
|
|||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (d *DNS) overrideWith(other DNS) {
|
func (d *DNS) overrideWith(other DNS) {
|
||||||
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
|
|
||||||
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
|
|
||||||
d.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.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||||
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||||
|
d.DoT.overrideWith(other.DoT)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) setDefaults() {
|
func (d *DNS) setDefaults() {
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
|
||||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||||
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
|
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
||||||
return d.ServerAddress
|
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||||
}
|
d.DoT.setDefaults()
|
||||||
|
|
||||||
providers := provider.NewProviders()
|
|
||||||
provider, err := providers.Get(d.Providers[0])
|
|
||||||
if err != nil {
|
|
||||||
// Settings should be validated before calling this function,
|
|
||||||
// so an error happening here is a programming error.
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.Plain.IPv4[0].Addr()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DNS) String() string {
|
func (d DNS) String() string {
|
||||||
@@ -160,63 +77,11 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
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
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) read(r *reader.Reader) (err error) {
|
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||||
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
|
||||||
|
|
||||||
d.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"))
|
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -227,5 +92,10 @@ func (d *DNS) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = d.DoT.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DNS over TLS settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ type DNSBlacklist struct {
|
|||||||
AddBlockedHosts []string
|
AddBlockedHosts []string
|
||||||
AddBlockedIPs []netip.Addr
|
AddBlockedIPs []netip.Addr
|
||||||
AddBlockedIPPrefixes []netip.Prefix
|
AddBlockedIPPrefixes []netip.Prefix
|
||||||
// RebindingProtectionExemptHostnames is a list of hostnames
|
|
||||||
// exempt from DNS rebinding protection.
|
|
||||||
RebindingProtectionExemptHostnames []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DNSBlacklist) setDefaults() {
|
func (b *DNSBlacklist) setDefaults() {
|
||||||
@@ -36,9 +33,8 @@ func (b *DNSBlacklist) setDefaults() {
|
|||||||
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||||
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt host is not valid")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b DNSBlacklist) validate() (err error) {
|
func (b DNSBlacklist) validate() (err error) {
|
||||||
@@ -54,25 +50,18 @@ func (b DNSBlacklist) validate() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, host := range b.RebindingProtectionExemptHostnames {
|
|
||||||
if !hostRegex.MatchString(host) {
|
|
||||||
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
||||||
return DNSBlacklist{
|
return DNSBlacklist{
|
||||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||||
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||||
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +73,6 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
|||||||
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||||
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||||
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||||
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
|
|
||||||
other.RebindingProtectionExemptHostnames)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
||||||
@@ -142,13 +129,6 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b.RebindingProtectionExemptHostnames) > 0 {
|
|
||||||
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
|
|
||||||
for _, host := range b.RebindingProtectionExemptHostnames {
|
|
||||||
exemptHostsNode.Append(host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,47 +149,23 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
|
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
|
||||||
|
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
|
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
|
||||||
|
|
||||||
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
|
|
||||||
ipPrefixes []netip.Prefix, err error,
|
|
||||||
) {
|
|
||||||
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO v4 remove this block below
|
|
||||||
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
ips = append(ips, privateIPs...)
|
|
||||||
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
|
|
||||||
|
|
||||||
return ips, ipPrefixes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||||
|
|
||||||
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
|
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
|
||||||
ipPrefixes []netip.Prefix, err error,
|
ipPrefixes []netip.Prefix, err error,
|
||||||
) {
|
) {
|
||||||
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
|
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
||||||
if len(privateAddresses) == 0 {
|
if len(privateAddresses) == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DoT contains settings to configure the DoT server.
|
||||||
|
type DoT struct {
|
||||||
|
// Enabled is true if the DoT server should be running
|
||||||
|
// and used. It defaults to true, and cannot be nil
|
||||||
|
// in the internal state.
|
||||||
|
Enabled *bool
|
||||||
|
// UpdatePeriod is the period to update DNS block lists.
|
||||||
|
// It can be set to 0 to disable the update.
|
||||||
|
// It defaults to 24h and cannot be nil in
|
||||||
|
// the internal state.
|
||||||
|
UpdatePeriod *time.Duration
|
||||||
|
// Providers is a list of DNS over TLS providers
|
||||||
|
Providers []string `json:"providers"`
|
||||||
|
// Caching is true if the DoT server should cache
|
||||||
|
// DNS responses.
|
||||||
|
Caching *bool `json:"caching"`
|
||||||
|
// IPv6 is true if the DoT server should connect over IPv6.
|
||||||
|
IPv6 *bool `json:"ipv6"`
|
||||||
|
// Blacklist contains settings to configure the filter
|
||||||
|
// block lists.
|
||||||
|
Blacklist DNSBlacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
|
||||||
|
|
||||||
|
func (d DoT) validate() (err error) {
|
||||||
|
const minUpdatePeriod = 30 * time.Second
|
||||||
|
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||||
|
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||||
|
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
for _, providerName := range d.Providers {
|
||||||
|
_, err := providers.Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DoT) copy() (copied DoT) {
|
||||||
|
return DoT{
|
||||||
|
Enabled: gosettings.CopyPointer(d.Enabled),
|
||||||
|
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||||
|
Providers: gosettings.CopySlice(d.Providers),
|
||||||
|
Caching: gosettings.CopyPointer(d.Caching),
|
||||||
|
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||||
|
Blacklist: d.Blacklist.copy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (d *DoT) overrideWith(other DoT) {
|
||||||
|
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
||||||
|
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||||
|
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||||
|
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||||
|
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||||
|
d.Blacklist.overrideWith(other.Blacklist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DoT) setDefaults() {
|
||||||
|
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
||||||
|
const defaultUpdatePeriod = 24 * time.Hour
|
||||||
|
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||||
|
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||||
|
provider.Cloudflare().Name,
|
||||||
|
})
|
||||||
|
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||||
|
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||||
|
d.Blacklist.setDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
provider, err := providers.Get(d.Providers[0])
|
||||||
|
if err != nil {
|
||||||
|
// Settings should be validated before calling this function,
|
||||||
|
// so an error happening here is a programming error.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.DoT.IPv4[0].Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DoT) String() string {
|
||||||
|
return d.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DoT) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("DNS over TLS settings:")
|
||||||
|
|
||||||
|
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
|
||||||
|
if !*d.Enabled {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
update := "disabled" //nolint:goconst
|
||||||
|
if *d.UpdatePeriod > 0 {
|
||||||
|
update = "every " + d.UpdatePeriod.String()
|
||||||
|
}
|
||||||
|
node.Appendf("Update period: %s", update)
|
||||||
|
|
||||||
|
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||||
|
for _, provider := range d.Providers {
|
||||||
|
upstreamResolvers.Append(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||||
|
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||||
|
|
||||||
|
node.AppendNode(d.Blacklist.toLinesNode())
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DoT) read(reader *reader.Reader) (err error) {
|
||||||
|
d.Enabled, err = reader.BoolPtr("DOT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Providers = reader.CSV("DOT_PROVIDERS")
|
||||||
|
|
||||||
|
d.Caching, err = reader.BoolPtr("DOT_CACHING")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.read(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
@@ -18,51 +17,34 @@ type Health struct {
|
|||||||
// for the health check server.
|
// for the health check server.
|
||||||
// It cannot be the empty string in the internal state.
|
// It cannot be the empty string in the internal state.
|
||||||
ServerAddress string
|
ServerAddress string
|
||||||
// TargetAddresses are the addresses (host or host:port)
|
// ReadHeaderTimeout is the HTTP server header read timeout
|
||||||
// to TCP TLS dial to periodically for the health check.
|
// duration of the HTTP server. It defaults to 100 milliseconds.
|
||||||
// Addresses after the first one are used as fallbacks for retries.
|
ReadHeaderTimeout time.Duration
|
||||||
// It cannot be empty in the internal state.
|
// ReadTimeout is the HTTP read timeout duration of the
|
||||||
TargetAddresses []string
|
// HTTP server. It defaults to 500 milliseconds.
|
||||||
// ICMPTargetIPs are the IP addresses to use for ICMP echo requests
|
ReadTimeout time.Duration
|
||||||
// in the health checker. The slice can be set to a single
|
// TargetAddress is the address (host or host:port)
|
||||||
// unspecified address (0.0.0.0) such that the VPN server IP is used,
|
// to TCP dial to periodically for the health check.
|
||||||
// although this can be less reliable. It defaults to [1.1.1.1,8.8.8.8],
|
// It cannot be the empty string in the internal state.
|
||||||
// and cannot be left empty in the internal state.
|
TargetAddress string
|
||||||
ICMPTargetIPs []netip.Addr
|
// SuccessWait is the duration to wait to re-run the
|
||||||
// SmallCheckType is the type of small health check to perform.
|
// healthcheck after a successful healthcheck.
|
||||||
// It can be "icmp" or "dns", and defaults to "icmp".
|
// It defaults to 5 seconds and cannot be zero in
|
||||||
// Note it changes automatically to dns if icmp is not supported.
|
// the internal state.
|
||||||
SmallCheckType string
|
SuccessWait time.Duration
|
||||||
// RestartVPN indicates whether to restart the VPN connection
|
// VPN has health settings specific to the VPN loop.
|
||||||
// when the healthcheck fails.
|
VPN HealthyWait
|
||||||
RestartVPN *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
ErrICMPTargetIPNotValid = errors.New("ICMP target IP address is not valid")
|
|
||||||
ErrICMPTargetIPsNotCompatible = errors.New("ICMP target IP addresses are not compatible")
|
|
||||||
ErrSmallCheckTypeNotValid = errors.New("small check type is not valid")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h Health) Validate() (err error) {
|
func (h Health) Validate() (err error) {
|
||||||
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
|
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("server listening address is not valid: %w", err)
|
return fmt.Errorf("server listening address is not valid: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ip := range h.ICMPTargetIPs {
|
err = h.VPN.validate()
|
||||||
switch {
|
|
||||||
case !ip.IsValid():
|
|
||||||
return fmt.Errorf("%w: %s", ErrICMPTargetIPNotValid, ip)
|
|
||||||
case ip.IsUnspecified() && len(h.ICMPTargetIPs) > 1:
|
|
||||||
return fmt.Errorf("%w: only a single IP address must be set if it is to be unspecified",
|
|
||||||
ErrICMPTargetIPsNotCompatible)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validate.IsOneOf(h.SmallCheckType, "icmp", "dns")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrSmallCheckTypeNotValid, err)
|
return fmt.Errorf("health VPN settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -70,11 +52,12 @@ func (h Health) Validate() (err error) {
|
|||||||
|
|
||||||
func (h *Health) copy() (copied Health) {
|
func (h *Health) copy() (copied Health) {
|
||||||
return Health{
|
return Health{
|
||||||
ServerAddress: h.ServerAddress,
|
ServerAddress: h.ServerAddress,
|
||||||
TargetAddresses: h.TargetAddresses,
|
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||||
ICMPTargetIPs: gosettings.CopySlice(h.ICMPTargetIPs),
|
ReadTimeout: h.ReadTimeout,
|
||||||
SmallCheckType: h.SmallCheckType,
|
TargetAddress: h.TargetAddress,
|
||||||
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
SuccessWait: h.SuccessWait,
|
||||||
|
VPN: h.VPN.copy(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,21 +66,23 @@ func (h *Health) copy() (copied Health) {
|
|||||||
// settings.
|
// settings.
|
||||||
func (h *Health) OverrideWith(other Health) {
|
func (h *Health) OverrideWith(other Health) {
|
||||||
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
||||||
h.TargetAddresses = gosettings.OverrideWithSlice(h.TargetAddresses, other.TargetAddresses)
|
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||||
h.ICMPTargetIPs = gosettings.OverrideWithSlice(h.ICMPTargetIPs, other.ICMPTargetIPs)
|
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
||||||
h.SmallCheckType = gosettings.OverrideWithComparable(h.SmallCheckType, other.SmallCheckType)
|
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
||||||
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
|
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
|
||||||
|
h.VPN.overrideWith(other.VPN)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Health) SetDefaults() {
|
func (h *Health) SetDefaults() {
|
||||||
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
||||||
h.TargetAddresses = gosettings.DefaultSlice(h.TargetAddresses, []string{"cloudflare.com:443", "github.com:443"})
|
const defaultReadHeaderTimeout = 100 * time.Millisecond
|
||||||
h.ICMPTargetIPs = gosettings.DefaultSlice(h.ICMPTargetIPs, []netip.Addr{
|
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||||
netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
const defaultReadTimeout = 500 * time.Millisecond
|
||||||
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
|
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
||||||
})
|
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
||||||
h.SmallCheckType = gosettings.DefaultComparable(h.SmallCheckType, "icmp")
|
const defaultSuccessWait = 5 * time.Second
|
||||||
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
|
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
|
||||||
|
h.VPN.setDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Health) String() string {
|
func (h Health) String() string {
|
||||||
@@ -107,40 +92,28 @@ func (h Health) String() string {
|
|||||||
func (h Health) toLinesNode() (node *gotree.Node) {
|
func (h Health) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Health settings:")
|
node = gotree.New("Health settings:")
|
||||||
node.Appendf("Server listening address: %s", h.ServerAddress)
|
node.Appendf("Server listening address: %s", h.ServerAddress)
|
||||||
targetAddrs := node.Appendf("Target addresses:")
|
node.Appendf("Target address: %s", h.TargetAddress)
|
||||||
for _, targetAddr := range h.TargetAddresses {
|
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
|
||||||
targetAddrs.Append(targetAddr)
|
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
||||||
}
|
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
||||||
switch h.SmallCheckType {
|
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
||||||
case "icmp":
|
|
||||||
icmpNode := node.Appendf("Small health check type: ICMP echo request")
|
|
||||||
if len(h.ICMPTargetIPs) == 1 && h.ICMPTargetIPs[0].IsUnspecified() {
|
|
||||||
icmpNode.Appendf("ICMP target IP: VPN server IP address")
|
|
||||||
} else {
|
|
||||||
icmpIPs := icmpNode.Appendf("ICMP target IPs:")
|
|
||||||
for _, ip := range h.ICMPTargetIPs {
|
|
||||||
icmpIPs.Append(ip.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "dns":
|
|
||||||
node.Appendf("Small health check type: Plain DNS lookup over UDP")
|
|
||||||
}
|
|
||||||
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Health) Read(r *reader.Reader) (err error) {
|
func (h *Health) Read(r *reader.Reader) (err error) {
|
||||||
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
||||||
h.TargetAddresses = r.CSV("HEALTH_TARGET_ADDRESSES",
|
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
|
||||||
reader.RetroKeys("HEALTH_ADDRESS_TO_PING", "HEALTH_TARGET_ADDRESS"))
|
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
|
||||||
h.ICMPTargetIPs, err = r.CSVNetipAddresses("HEALTH_ICMP_TARGET_IPS", reader.RetroKeys("HEALTH_ICMP_TARGET_IP"))
|
|
||||||
|
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h.SmallCheckType = r.String("HEALTH_SMALL_CHECK_TYPE")
|
|
||||||
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
|
err = h.VPN.read(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("VPN health settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HealthyWait struct {
|
||||||
|
// Initial is the initial duration to wait for the program
|
||||||
|
// to be healthy before taking action.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
Initial *time.Duration
|
||||||
|
// Addition is the duration to add to the Initial duration
|
||||||
|
// after Initial has expired to wait longer for the program
|
||||||
|
// to be healthy.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
Addition *time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HealthyWait) validate() (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthyWait) copy() (copied HealthyWait) {
|
||||||
|
return HealthyWait{
|
||||||
|
Initial: gosettings.CopyPointer(h.Initial),
|
||||||
|
Addition: gosettings.CopyPointer(h.Addition),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (h *HealthyWait) overrideWith(other HealthyWait) {
|
||||||
|
h.Initial = gosettings.OverrideWithPointer(h.Initial, other.Initial)
|
||||||
|
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthyWait) setDefaults() {
|
||||||
|
const initialDurationDefault = 6 * time.Second
|
||||||
|
const additionDurationDefault = 5 * time.Second
|
||||||
|
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault)
|
||||||
|
h.Addition = gosettings.DefaultPointer(h.Addition, additionDurationDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HealthyWait) String() string {
|
||||||
|
return h.toLinesNode("Health").String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
|
||||||
|
node = gotree.New(kind + " wait durations:")
|
||||||
|
node.Appendf("Initial duration: %s", *h.Initial)
|
||||||
|
node.Appendf("Additional duration: %s", *h.Addition)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HealthyWait) read(r *reader.Reader) (err error) {
|
||||||
|
h.Initial, err = r.DurationPtr(
|
||||||
|
"HEALTH_VPN_DURATION_INITIAL",
|
||||||
|
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Addition, err = r.DurationPtr(
|
||||||
|
"HEALTH_VPN_DURATION_ADDITION",
|
||||||
|
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -209,7 +209,8 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
|
|||||||
case
|
case
|
||||||
providers.Airvpn,
|
providers.Airvpn,
|
||||||
providers.Cyberghost,
|
providers.Cyberghost,
|
||||||
providers.VPNUnlimited:
|
providers.VPNUnlimited,
|
||||||
|
providers.Wevpn:
|
||||||
if clientKey == "" {
|
if clientKey == "" {
|
||||||
return fmt.Errorf("%w", ErrMissingValue)
|
return fmt.Errorf("%w", ErrMissingValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
@@ -25,12 +24,6 @@ type OpenVPNSelection struct {
|
|||||||
// and can be udp or tcp. It cannot be the empty string
|
// and can be udp or tcp. It cannot be the empty string
|
||||||
// in the internal state.
|
// in the internal state.
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
// EndpointIP is the server endpoint IP address.
|
|
||||||
// If set, it overrides any IP address from the picked
|
|
||||||
// built-in server connection. To indicate it should
|
|
||||||
// not be used, it should be set to [netip.IPv4Unspecified].
|
|
||||||
// It can never be the zero value in the internal state.
|
|
||||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
|
||||||
// CustomPort is the OpenVPN server endpoint port.
|
// CustomPort is the OpenVPN server endpoint port.
|
||||||
// It can be set to 0 to indicate no custom port should
|
// It can be set to 0 to indicate no custom port should
|
||||||
// be used. It cannot be nil in the internal state.
|
// be used. It cannot be nil in the internal state.
|
||||||
@@ -60,6 +53,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
providers.Giganews,
|
providers.Giganews,
|
||||||
providers.Ipvanish,
|
providers.Ipvanish,
|
||||||
providers.Perfectprivacy,
|
providers.Perfectprivacy,
|
||||||
|
providers.Privado,
|
||||||
providers.Vyprvpn,
|
providers.Vyprvpn,
|
||||||
) {
|
) {
|
||||||
return fmt.Errorf("%w: for VPN service provider %s",
|
return fmt.Errorf("%w: for VPN service provider %s",
|
||||||
@@ -74,8 +68,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
providers.Privatevpn, providers.Torguard:
|
providers.Privatevpn, providers.Torguard:
|
||||||
// no custom port allowed
|
// no custom port allowed
|
||||||
case providers.Expressvpn, providers.Fastestvpn,
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
providers.Giganews, providers.Ipvanish,
|
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||||
providers.Nordvpn, providers.Purevpn,
|
providers.Privado, providers.Purevpn,
|
||||||
providers.Surfshark, providers.VPNSecure,
|
providers.Surfshark, providers.VPNSecure,
|
||||||
providers.VPNUnlimited, providers.Vyprvpn:
|
providers.VPNUnlimited, providers.Vyprvpn:
|
||||||
return fmt.Errorf("%w: for VPN service provider %s",
|
return fmt.Errorf("%w: for VPN service provider %s",
|
||||||
@@ -98,18 +92,18 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
case providers.Perfectprivacy:
|
case providers.Perfectprivacy:
|
||||||
allowedTCP = []uint16{44, 443, 4433}
|
allowedTCP = []uint16{44, 443, 4433}
|
||||||
allowedUDP = []uint16{44, 443, 4433}
|
allowedUDP = []uint16{44, 443, 4433}
|
||||||
case providers.Privado:
|
|
||||||
allowedTCP = []uint16{443, 1194, 8080, 8443}
|
|
||||||
allowedUDP = []uint16{443, 1194, 8080, 8443}
|
|
||||||
case providers.PrivateInternetAccess:
|
case providers.PrivateInternetAccess:
|
||||||
allowedTCP = []uint16{80, 110, 443, 501, 502, 8443}
|
allowedTCP = []uint16{80, 110, 443}
|
||||||
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
|
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
|
||||||
case providers.Protonvpn:
|
case providers.Protonvpn:
|
||||||
allowedTCP = []uint16{443, 5995, 8443}
|
allowedTCP = []uint16{443, 5995, 8443}
|
||||||
allowedUDP = []uint16{80, 443, 1194, 4569, 5060, 51820}
|
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
|
||||||
case providers.SlickVPN:
|
case providers.SlickVPN:
|
||||||
allowedTCP = []uint16{443, 8080, 8888}
|
allowedTCP = []uint16{443, 8080, 8888}
|
||||||
allowedUDP = []uint16{443, 8080, 8888}
|
allowedUDP = []uint16{443, 8080, 8888}
|
||||||
|
case providers.Wevpn:
|
||||||
|
allowedTCP = []uint16{53, 1195, 1199, 2018}
|
||||||
|
allowedUDP = []uint16{80, 1194, 1198}
|
||||||
case providers.Windscribe:
|
case providers.Windscribe:
|
||||||
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
||||||
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
||||||
@@ -132,6 +126,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate EncPreset
|
// Validate EncPreset
|
||||||
if vpnProvider == providers.PrivateInternetAccess {
|
if vpnProvider == providers.PrivateInternetAccess {
|
||||||
validEncryptionPresets := []string{
|
validEncryptionPresets := []string{
|
||||||
|
presets.None,
|
||||||
presets.Normal,
|
presets.Normal,
|
||||||
presets.Strong,
|
presets.Strong,
|
||||||
}
|
}
|
||||||
@@ -147,7 +142,6 @@ func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
|
|||||||
return OpenVPNSelection{
|
return OpenVPNSelection{
|
||||||
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
||||||
Protocol: o.Protocol,
|
Protocol: o.Protocol,
|
||||||
EndpointIP: o.EndpointIP,
|
|
||||||
CustomPort: gosettings.CopyPointer(o.CustomPort),
|
CustomPort: gosettings.CopyPointer(o.CustomPort),
|
||||||
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
||||||
}
|
}
|
||||||
@@ -157,14 +151,12 @@ func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
|
|||||||
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
||||||
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
|
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
|
||||||
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
|
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
|
||||||
o.EndpointIP = gosettings.OverrideWithValidator(o.EndpointIP, other.EndpointIP)
|
|
||||||
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
||||||
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
||||||
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
|
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
|
||||||
o.EndpointIP = gosettings.DefaultValidator(o.EndpointIP, netip.IPv4Unspecified())
|
|
||||||
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
|
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
|
||||||
|
|
||||||
var defaultEncPreset string
|
var defaultEncPreset string
|
||||||
@@ -182,10 +174,6 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
node = gotree.New("OpenVPN server selection settings:")
|
node = gotree.New("OpenVPN server selection settings:")
|
||||||
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
|
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
|
||||||
|
|
||||||
if !o.EndpointIP.IsUnspecified() {
|
|
||||||
node.Appendf("Endpoint IP address: %s", o.EndpointIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *o.CustomPort != 0 {
|
if *o.CustomPort != 0 {
|
||||||
node.Appendf("Custom port: %d", *o.CustomPort)
|
node.Appendf("Custom port: %d", *o.CustomPort)
|
||||||
}
|
}
|
||||||
@@ -206,12 +194,6 @@ func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
|
|||||||
|
|
||||||
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
||||||
|
|
||||||
o.EndpointIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
|
||||||
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
|
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
|
||||||
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
|
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
@@ -27,9 +24,6 @@ type ControlServer struct {
|
|||||||
// It cannot be empty in the internal state and defaults to
|
// It cannot be empty in the internal state and defaults to
|
||||||
// /gluetun/auth/config.toml.
|
// /gluetun/auth/config.toml.
|
||||||
AuthFilePath string
|
AuthFilePath string
|
||||||
// AuthDefaultRole is a JSON encoded object defining the default role
|
|
||||||
// that applies to all routes without a previously user-defined role assigned to.
|
|
||||||
AuthDefaultRole string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ControlServer) validate() (err error) {
|
func (c ControlServer) validate() (err error) {
|
||||||
@@ -50,30 +44,14 @@ func (c ControlServer) validate() (err error) {
|
|||||||
ErrControlServerPrivilegedPort, port, uid)
|
ErrControlServerPrivilegedPort, port, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
|
|
||||||
jsonDecoder.DisallowUnknownFields()
|
|
||||||
var role auth.Role
|
|
||||||
err = jsonDecoder.Decode(&role)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if role.Auth != "" {
|
|
||||||
err = role.Validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("default authentication role is not valid: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControlServer) copy() (copied ControlServer) {
|
func (c *ControlServer) copy() (copied ControlServer) {
|
||||||
return ControlServer{
|
return ControlServer{
|
||||||
Address: gosettings.CopyPointer(c.Address),
|
Address: gosettings.CopyPointer(c.Address),
|
||||||
Log: gosettings.CopyPointer(c.Log),
|
Log: gosettings.CopyPointer(c.Log),
|
||||||
AuthFilePath: c.AuthFilePath,
|
AuthFilePath: c.AuthFilePath,
|
||||||
AuthDefaultRole: c.AuthDefaultRole,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,21 +62,12 @@ func (c *ControlServer) overrideWith(other ControlServer) {
|
|||||||
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
||||||
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
||||||
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
|
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
|
||||||
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControlServer) setDefaults() {
|
func (c *ControlServer) setDefaults() {
|
||||||
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
||||||
c.Log = gosettings.DefaultPointer(c.Log, true)
|
c.Log = gosettings.DefaultPointer(c.Log, true)
|
||||||
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
|
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
|
||||||
c.AuthDefaultRole = gosettings.DefaultComparable(c.AuthDefaultRole, "{}")
|
|
||||||
if c.AuthDefaultRole != "{}" {
|
|
||||||
var role auth.Role
|
|
||||||
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
|
||||||
role.Name = "default"
|
|
||||||
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
|
|
||||||
c.AuthDefaultRole = string(roleBytes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ControlServer) String() string {
|
func (c ControlServer) String() string {
|
||||||
@@ -110,11 +79,6 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Listening address: %s", *c.Address)
|
node.Appendf("Listening address: %s", *c.Address)
|
||||||
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
||||||
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
||||||
if c.AuthDefaultRole != "{}" {
|
|
||||||
var role auth.Role
|
|
||||||
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
|
||||||
node.AppendNode(role.ToLinesNode())
|
|
||||||
}
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +91,6 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
|
|||||||
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
||||||
|
|
||||||
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
|
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
|
||||||
c.AuthDefaultRole = r.String("HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE", reader.ForceLowercase(false))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package settings
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
@@ -16,11 +17,17 @@ import (
|
|||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerSelection struct {
|
type ServerSelection struct { //nolint:maligned
|
||||||
// VPN is the VPN type which can be 'openvpn'
|
// VPN is the VPN type which can be 'openvpn'
|
||||||
// or 'wireguard'. It cannot be the empty string
|
// or 'wireguard'. It cannot be the empty string
|
||||||
// in the internal state.
|
// in the internal state.
|
||||||
VPN string `json:"vpn"`
|
VPN string `json:"vpn"`
|
||||||
|
// TargetIP is the server endpoint IP address to use.
|
||||||
|
// It will override any IP address from the picked
|
||||||
|
// built-in server. It cannot be the empty value in the internal
|
||||||
|
// state, and can be set to the unspecified address to indicate
|
||||||
|
// there is not target IP address to use.
|
||||||
|
TargetIP netip.Addr `json:"target_ip"`
|
||||||
// Countries is the list of countries to filter VPN servers with.
|
// Countries is the list of countries to filter VPN servers with.
|
||||||
Countries []string `json:"countries"`
|
Countries []string `json:"countries"`
|
||||||
// Categories is the list of categories to filter VPN servers with.
|
// Categories is the list of categories to filter VPN servers with.
|
||||||
@@ -292,6 +299,7 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
|
|||||||
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||||
return ServerSelection{
|
return ServerSelection{
|
||||||
VPN: ss.VPN,
|
VPN: ss.VPN,
|
||||||
|
TargetIP: ss.TargetIP,
|
||||||
Countries: gosettings.CopySlice(ss.Countries),
|
Countries: gosettings.CopySlice(ss.Countries),
|
||||||
Categories: gosettings.CopySlice(ss.Categories),
|
Categories: gosettings.CopySlice(ss.Categories),
|
||||||
Regions: gosettings.CopySlice(ss.Regions),
|
Regions: gosettings.CopySlice(ss.Regions),
|
||||||
@@ -315,6 +323,7 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
|
|||||||
|
|
||||||
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||||
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
|
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
|
||||||
|
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
|
||||||
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
|
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
|
||||||
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
|
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
|
||||||
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
|
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
|
||||||
@@ -337,6 +346,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
|||||||
|
|
||||||
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
|
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
|
||||||
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
|
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
|
||||||
|
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
|
||||||
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
|
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
|
||||||
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
||||||
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
||||||
@@ -344,8 +354,11 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled
|
|||||||
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
||||||
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
||||||
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
||||||
defaultPortForwardOnly := portForwardingEnabled &&
|
defaultPortForwardOnly := false
|
||||||
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
|
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
|
||||||
|
providers.PrivateInternetAccess, providers.Protonvpn) {
|
||||||
|
defaultPortForwardOnly = true
|
||||||
|
}
|
||||||
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
||||||
ss.OpenVPN.setDefaults(vpnProvider)
|
ss.OpenVPN.setDefaults(vpnProvider)
|
||||||
ss.Wireguard.setDefaults()
|
ss.Wireguard.setDefaults()
|
||||||
@@ -358,6 +371,9 @@ func (ss ServerSelection) String() string {
|
|||||||
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Server selection settings:")
|
node = gotree.New("Server selection settings:")
|
||||||
node.Appendf("VPN type: %s", ss.VPN)
|
node.Appendf("VPN type: %s", ss.VPN)
|
||||||
|
if !ss.TargetIP.IsUnspecified() {
|
||||||
|
node.Appendf("Target IP address: %s", ss.TargetIP)
|
||||||
|
}
|
||||||
|
|
||||||
if len(ss.Countries) > 0 {
|
if len(ss.Countries) > 0 {
|
||||||
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
||||||
@@ -448,6 +464,12 @@ func (ss *ServerSelection) read(r *reader.Reader,
|
|||||||
) (err error) {
|
) (err error) {
|
||||||
ss.VPN = vpnType
|
ss.VPN = vpnType
|
||||||
|
|
||||||
|
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||||
|
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
countriesRetroKeys := []string{"COUNTRY"}
|
countriesRetroKeys := []string{"COUNTRY"}
|
||||||
if vpnProvider == providers.Cyberghost {
|
if vpnProvider == providers.Cyberghost {
|
||||||
countriesRetroKeys = append(countriesRetroKeys, "REGION")
|
countriesRetroKeys = append(countriesRetroKeys, "REGION")
|
||||||
|
|||||||
@@ -177,10 +177,10 @@ func (s Settings) Warnings() (warnings []string) {
|
|||||||
// TODO remove in v4
|
// TODO remove in v4
|
||||||
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
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()+
|
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
||||||
" so the local forwarding DNS server will not be used."+
|
" 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 DNS server."+
|
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
|
||||||
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
|
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||||
" corresponding to the first DNS provider chosen is used.")
|
" corresponding to the first DoT provider chosen is used.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return warnings
|
return warnings
|
||||||
|
|||||||
@@ -40,31 +40,30 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
├── DNS settings:
|
├── DNS settings:
|
||||||
| ├── Keep existing nameserver(s): no
|
| ├── Keep existing nameserver(s): no
|
||||||
| ├── DNS server address to use: 127.0.0.1
|
| ├── DNS server address to use: 127.0.0.1
|
||||||
| ├── DNS forwarder server enabled: yes
|
| └── DNS over TLS settings:
|
||||||
| ├── Upstream resolver type: dot
|
| ├── Enabled: yes
|
||||||
| ├── Upstream resolvers:
|
| ├── Update period: every 24h0m0s
|
||||||
| | └── Cloudflare
|
| ├── Upstream resolvers:
|
||||||
| ├── Caching: yes
|
| | └── Cloudflare
|
||||||
| ├── IPv6: no
|
| ├── Caching: yes
|
||||||
| ├── Update period: every 24h0m0s
|
| ├── IPv6: no
|
||||||
| └── DNS filtering settings:
|
| └── DNS filtering settings:
|
||||||
| ├── Block malicious: yes
|
| ├── Block malicious: yes
|
||||||
| ├── Block ads: no
|
| ├── Block ads: no
|
||||||
| └── Block surveillance: yes
|
| └── Block surveillance: yes
|
||||||
├── Firewall settings:
|
├── Firewall settings:
|
||||||
| └── Enabled: yes
|
| └── Enabled: yes
|
||||||
├── Log settings:
|
├── Log settings:
|
||||||
| └── Log level: INFO
|
| └── Log level: INFO
|
||||||
├── Health settings:
|
├── Health settings:
|
||||||
| ├── Server listening address: 127.0.0.1:9999
|
| ├── Server listening address: 127.0.0.1:9999
|
||||||
| ├── Target addresses:
|
| ├── Target address: cloudflare.com:443
|
||||||
| | ├── cloudflare.com:443
|
| ├── Duration to wait after success: 5s
|
||||||
| | └── github.com:443
|
| ├── Read header timeout: 100ms
|
||||||
| ├── Small health check type: ICMP echo request
|
| ├── Read timeout: 500ms
|
||||||
| | └── ICMP target IPs:
|
| └── VPN wait durations:
|
||||||
| | ├── 1.1.1.1
|
| ├── Initial duration: 6s
|
||||||
| | └── 8.8.8.8
|
| └── Additional duration: 5s
|
||||||
| └── Restart VPN on healthcheck failure: yes
|
|
||||||
├── Shadowsocks server settings:
|
├── Shadowsocks server settings:
|
||||||
| └── Enabled: no
|
| └── Enabled: no
|
||||||
├── HTTP proxy settings:
|
├── HTTP proxy settings:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Shadowsocks struct {
|
|||||||
// It defaults to false, and cannot be nil in the internal state.
|
// It defaults to false, and cannot be nil in the internal state.
|
||||||
Enabled *bool
|
Enabled *bool
|
||||||
// Settings are settings for the TCP+UDP server.
|
// Settings are settings for the TCP+UDP server.
|
||||||
Settings tcpudp.Settings
|
tcpudp.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Shadowsocks) validate() (err error) {
|
func (s Shadowsocks) validate() (err error) {
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ type Updater struct {
|
|||||||
// updater. It cannot be nil in the internal state.
|
// updater. It cannot be nil in the internal state.
|
||||||
// TODO change to value and add Enabled field.
|
// TODO change to value and add Enabled field.
|
||||||
Period *time.Duration
|
Period *time.Duration
|
||||||
|
// DNSAddress is the DNS server address to use
|
||||||
|
// to resolve VPN server hostnames to IP addresses.
|
||||||
|
// It cannot be the empty string in the internal state.
|
||||||
|
DNSAddress string
|
||||||
// MinRatio is the minimum ratio of servers to
|
// MinRatio is the minimum ratio of servers to
|
||||||
// find per provider, compared to the total current
|
// find per provider, compared to the total current
|
||||||
// number of servers. It defaults to 0.8.
|
// number of servers. It defaults to 0.8.
|
||||||
@@ -72,6 +76,7 @@ func (u Updater) Validate() (err error) {
|
|||||||
func (u *Updater) copy() (copied Updater) {
|
func (u *Updater) copy() (copied Updater) {
|
||||||
return Updater{
|
return Updater{
|
||||||
Period: gosettings.CopyPointer(u.Period),
|
Period: gosettings.CopyPointer(u.Period),
|
||||||
|
DNSAddress: u.DNSAddress,
|
||||||
MinRatio: u.MinRatio,
|
MinRatio: u.MinRatio,
|
||||||
Providers: gosettings.CopySlice(u.Providers),
|
Providers: gosettings.CopySlice(u.Providers),
|
||||||
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
|
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
|
||||||
@@ -84,6 +89,7 @@ func (u *Updater) copy() (copied Updater) {
|
|||||||
// settings.
|
// settings.
|
||||||
func (u *Updater) overrideWith(other Updater) {
|
func (u *Updater) overrideWith(other Updater) {
|
||||||
u.Period = gosettings.OverrideWithPointer(u.Period, other.Period)
|
u.Period = gosettings.OverrideWithPointer(u.Period, other.Period)
|
||||||
|
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
||||||
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||||
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
|
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
|
||||||
@@ -92,6 +98,7 @@ func (u *Updater) overrideWith(other Updater) {
|
|||||||
|
|
||||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||||
u.Period = gosettings.DefaultPointer(u.Period, 0)
|
u.Period = gosettings.DefaultPointer(u.Period, 0)
|
||||||
|
u.DNSAddress = gosettings.DefaultComparable(u.DNSAddress, "1.1.1.1:53")
|
||||||
|
|
||||||
if u.MinRatio == 0 {
|
if u.MinRatio == 0 {
|
||||||
const defaultMinRatio = 0.8
|
const defaultMinRatio = 0.8
|
||||||
@@ -118,6 +125,7 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
|||||||
|
|
||||||
node = gotree.New("Server data updater settings:")
|
node = gotree.New("Server data updater settings:")
|
||||||
node.Appendf("Update period: %s", *u.Period)
|
node.Appendf("Update period: %s", *u.Period)
|
||||||
|
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||||
if slices.Contains(u.Providers, providers.Protonvpn) {
|
if slices.Contains(u.Providers, providers.Protonvpn) {
|
||||||
@@ -134,6 +142,11 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u.DNSAddress, err = readUpdaterDNSAddress()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
|
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -153,3 +166,12 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readUpdaterDNSAddress() (address string, err error) {
|
||||||
|
// TODO this is currently using Cloudflare in
|
||||||
|
// plaintext to not be blocked by DNS over TLS by default.
|
||||||
|
// If a plaintext address is set in the DNS settings, this one will be used.
|
||||||
|
// use custom future encrypted DNS written in Go without blocking
|
||||||
|
// as it's too much trouble to start another parallel unbound instance for now.
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -251,12 +251,10 @@ func (w *Wireguard) read(r *reader.Reader) (err error) {
|
|||||||
// WARNING: do not initialize w.Addresses to an empty slice
|
// WARNING: do not initialize w.Addresses to an empty slice
|
||||||
// or the defaults for nordvpn will not work.
|
// or the defaults for nordvpn will not work.
|
||||||
for _, addressString := range addressStrings {
|
for _, addressString := range addressStrings {
|
||||||
addressString = strings.TrimSpace(addressString)
|
if !strings.ContainsRune(addressString, '/') {
|
||||||
if addressString == "" {
|
|
||||||
continue
|
|
||||||
} else if !strings.ContainsRune(addressString, '/') {
|
|
||||||
addressString += "/32"
|
addressString += "/32"
|
||||||
}
|
}
|
||||||
|
addressString = strings.TrimSpace(addressString)
|
||||||
address, err := netip.ParsePrefix(addressString)
|
address, err := netip.ParsePrefix(addressString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parsing address: %w", err)
|
return fmt.Errorf("parsing address: %w", err)
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import (
|
|||||||
|
|
||||||
type WireguardSelection struct {
|
type WireguardSelection struct {
|
||||||
// EndpointIP is the server endpoint IP address.
|
// EndpointIP is the server endpoint IP address.
|
||||||
// It is notably required with the custom provider.
|
// It is only used with VPN providers generating Wireguard
|
||||||
// Otherwise it overrides any IP address from the picked
|
// configurations specific to each server and user.
|
||||||
// built-in server connection. To indicate it should
|
// To indicate it should not be used, it should be set
|
||||||
// not be used, it should be set to [netip.IPv4Unspecified].
|
// to netip.IPv4Unspecified(). It can never be the zero value
|
||||||
// It can never be the zero value in the internal state.
|
// in the internal state.
|
||||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||||
// EndpointPort is a the server port to use for the VPN server.
|
// EndpointPort is a the server port to use for the VPN server.
|
||||||
// It is optional for VPN providers IVPN, Mullvad, Surfshark
|
// It is optional for VPN providers IVPN, Mullvad, Surfshark
|
||||||
@@ -155,8 +155,7 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
|
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
|
||||||
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
|
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w - note this MUST be an IP address, "+
|
return err
|
||||||
"see https://github.com/qdm12/gluetun/issues/788", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
|
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package files
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
@@ -82,12 +82,12 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
|
|||||||
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
|
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
|
||||||
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
|
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
|
||||||
if endpoint != nil {
|
if endpoint != nil {
|
||||||
host, port, err := net.SplitHostPort(*endpoint)
|
parts := strings.Split(*endpoint, ":")
|
||||||
if err == nil {
|
endpointIP = &parts[0]
|
||||||
endpointIP = &host
|
const partsWithPort = 2
|
||||||
endpointPort = &port
|
if len(parts) >= partsWithPort {
|
||||||
} else {
|
endpointPort = new(string)
|
||||||
endpointIP = endpoint
|
*endpointPort = strings.Join(parts[1:], ":")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,12 +179,6 @@ Endpoint = 1.2.3.4:51820`,
|
|||||||
endpointIP: ptrTo("1.2.3.4"),
|
endpointIP: ptrTo("1.2.3.4"),
|
||||||
endpointPort: ptrTo("51820"),
|
endpointPort: ptrTo("51820"),
|
||||||
},
|
},
|
||||||
"ipv6_endpoint": {
|
|
||||||
iniData: `[Peer]
|
|
||||||
Endpoint = [2a02:bbbb:aaaa:8075::10]:51820`,
|
|
||||||
endpointIP: ptrTo("2a02:bbbb:aaaa:8075::10"),
|
|
||||||
endpointPort: ptrTo("51820"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for testName, testCase := range testCases {
|
for testName, testCase := range testCases {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const (
|
|||||||
VPNSecure = "vpnsecure"
|
VPNSecure = "vpnsecure"
|
||||||
VPNUnlimited = "vpn unlimited"
|
VPNUnlimited = "vpn unlimited"
|
||||||
Vyprvpn = "vyprvpn"
|
Vyprvpn = "vyprvpn"
|
||||||
|
Wevpn = "wevpn"
|
||||||
Windscribe = "windscribe"
|
Windscribe = "windscribe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ func All() []string {
|
|||||||
VPNSecure,
|
VPNSecure,
|
||||||
VPNUnlimited,
|
VPNUnlimited,
|
||||||
Vyprvpn,
|
Vyprvpn,
|
||||||
|
Wevpn,
|
||||||
Windscribe,
|
Windscribe,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-33
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||||
@@ -17,23 +16,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Loop struct {
|
type Loop struct {
|
||||||
statusManager *loopstate.State
|
statusManager *loopstate.State
|
||||||
state *state.State
|
state *state.State
|
||||||
server *server.Server
|
server *server.Server
|
||||||
filter *mapfilter.Filter
|
filter *mapfilter.Filter
|
||||||
localResolvers []netip.Addr
|
resolvConf string
|
||||||
resolvConf string
|
client *http.Client
|
||||||
client *http.Client
|
logger Logger
|
||||||
logger Logger
|
userTrigger bool
|
||||||
userTrigger bool
|
start <-chan struct{}
|
||||||
start <-chan struct{}
|
running chan<- models.LoopStatus
|
||||||
running chan<- models.LoopStatus
|
stop <-chan struct{}
|
||||||
stop <-chan struct{}
|
stopped chan<- struct{}
|
||||||
stopped chan<- struct{}
|
updateTicker <-chan struct{}
|
||||||
updateTicker <-chan struct{}
|
backoffTime time.Duration
|
||||||
backoffTime time.Duration
|
timeNow func() time.Time
|
||||||
timeNow func() time.Time
|
timeSince func(time.Time) time.Duration
|
||||||
timeSince func(time.Time) time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBackoffTime = 10 * time.Second
|
const defaultBackoffTime = 10 * time.Second
|
||||||
@@ -50,9 +48,7 @@ func NewLoop(settings settings.DNS,
|
|||||||
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
||||||
state := state.New(statusManager, settings, updateTicker)
|
state := state.New(statusManager, settings, updateTicker)
|
||||||
|
|
||||||
filter, err := mapfilter.New(mapfilter.Settings{
|
filter, err := mapfilter.New(mapfilter.Settings{})
|
||||||
Logger: buildFilterLogger(logger),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating map filter: %w", err)
|
return nil, fmt.Errorf("creating map filter: %w", err)
|
||||||
}
|
}
|
||||||
@@ -104,15 +100,3 @@ func (l *Loop) signalOrSetStatus(status models.LoopStatus) {
|
|||||||
l.statusManager.SetStatus(status)
|
l.statusManager.SetStatus(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type filterLogger struct {
|
|
||||||
logger Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *filterLogger) Log(msg string) {
|
|
||||||
l.logger.Debug(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFilterLogger(logger Logger) *filterLogger {
|
|
||||||
return &filterLogger{logger: logger}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ import (
|
|||||||
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
|
|
||||||
targetIP := settings.GetFirstPlaintextIPv4()
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
if fallback {
|
if fallback {
|
||||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||||
@@ -19,15 +27,14 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dialTimeout = 3 * time.Second
|
const dialTimeout = 3 * time.Second
|
||||||
const defaultDNSPort = 53
|
|
||||||
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
||||||
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
|
IP: targetIP,
|
||||||
Timeout: dialTimeout,
|
Timeout: dialTimeout,
|
||||||
}
|
}
|
||||||
nameserver.UseDNSInternally(settingsInternalDNS)
|
nameserver.UseDNSInternally(settingsInternalDNS)
|
||||||
|
|
||||||
settingsSystemWide := nameserver.SettingsSystemDNS{
|
settingsSystemWide := nameserver.SettingsSystemDNS{
|
||||||
IPs: []netip.Addr{targetIP},
|
IP: targetIP,
|
||||||
ResolvPath: l.resolvConf,
|
ResolvPath: l.resolvConf,
|
||||||
}
|
}
|
||||||
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
||||||
|
|||||||
+12
-16
@@ -2,21 +2,14 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
var err error
|
|
||||||
l.localResolvers, err = nameserver.GetPrivateDNSServers()
|
|
||||||
if err != nil {
|
|
||||||
l.logger.Error("getting private DNS servers: " + err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if *l.GetSettings().KeepNameserver {
|
if *l.GetSettings().KeepNameserver {
|
||||||
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
|
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
|
||||||
"this will likely leak DNS traffic outside the VPN " +
|
"this will likely leak DNS traffic outside the VPN " +
|
||||||
@@ -33,12 +26,12 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ctx.Err() == nil {
|
for ctx.Err() == nil {
|
||||||
// Upper scope variables for the DNS forwarder server only
|
// Upper scope variables for the DNS over TLS server only
|
||||||
// Their values are to be used if DOT=off
|
// Their values are to be used if DOT=off
|
||||||
var runError <-chan error
|
var runError <-chan error
|
||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
for !*settings.KeepNameserver && *settings.ServerEnabled {
|
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
||||||
var err error
|
var err error
|
||||||
runError, err = l.setupServer(ctx)
|
runError, err = l.setupServer(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -52,13 +45,18 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, errUpdateBlockLists) {
|
||||||
|
const fallback = true
|
||||||
|
l.useUnencryptedDNS(fallback)
|
||||||
|
}
|
||||||
l.logAndWait(ctx, err)
|
l.logAndWait(ctx, err)
|
||||||
settings = l.GetSettings()
|
settings = l.GetSettings()
|
||||||
}
|
}
|
||||||
l.signalOrSetStatus(constants.Running)
|
l.signalOrSetStatus(constants.Running)
|
||||||
|
|
||||||
settings = l.GetSettings()
|
settings = l.GetSettings()
|
||||||
if !*settings.KeepNameserver && !*settings.ServerEnabled {
|
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||||
const fallback = false
|
const fallback = false
|
||||||
l.useUnencryptedDNS(fallback)
|
l.useUnencryptedDNS(fallback)
|
||||||
}
|
}
|
||||||
@@ -76,8 +74,7 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
settings := l.GetSettings()
|
if !*l.GetSettings().KeepNameserver {
|
||||||
if !*settings.KeepNameserver && *settings.ServerEnabled {
|
|
||||||
l.stopServer()
|
l.stopServer()
|
||||||
// TODO revert OS and Go nameserver when exiting
|
// TODO revert OS and Go nameserver when exiting
|
||||||
}
|
}
|
||||||
@@ -85,8 +82,7 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
|||||||
case <-l.stop:
|
case <-l.stop:
|
||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
l.logger.Info("stopping")
|
l.logger.Info("stopping")
|
||||||
settings := l.GetSettings()
|
if !*l.GetSettings().KeepNameserver {
|
||||||
if !*settings.KeepNameserver && *settings.ServerEnabled {
|
|
||||||
const fallback = false
|
const fallback = false
|
||||||
l.useUnencryptedDNS(fallback)
|
l.useUnencryptedDNS(fallback)
|
||||||
l.stopServer()
|
l.stopServer()
|
||||||
@@ -109,6 +105,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
|||||||
func (l *Loop) stopServer() {
|
func (l *Loop) stopServer() {
|
||||||
stopErr := l.server.Stop()
|
stopErr := l.server.Stop()
|
||||||
if stopErr != nil {
|
if stopErr != nil {
|
||||||
l.logger.Error("stopping server: " + stopErr.Error())
|
l.logger.Error("stopping DoT server: " + stopErr.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-64
@@ -3,16 +3,12 @@ package dns
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/doh"
|
|
||||||
"github.com/qdm12/dns/v2/pkg/dot"
|
"github.com/qdm12/dns/v2/pkg/dot"
|
||||||
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
|
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
|
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
|
||||||
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
|
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/localdns"
|
|
||||||
"github.com/qdm12/dns/v2/pkg/plain"
|
|
||||||
"github.com/qdm12/dns/v2/pkg/provider"
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
"github.com/qdm12/dns/v2/pkg/server"
|
"github.com/qdm12/dns/v2/pkg/server"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
@@ -26,63 +22,33 @@ func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
|
|||||||
return l.state.SetSettings(ctx, settings)
|
return l.state.SetSettings(ctx, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildServerSettings(settings settings.DNS,
|
func buildDoTSettings(settings settings.DNS,
|
||||||
filter *mapfilter.Filter, localResolvers []netip.Addr,
|
filter *mapfilter.Filter, logger Logger) (
|
||||||
logger Logger) (
|
|
||||||
serverSettings server.Settings, err error,
|
serverSettings server.Settings, err error,
|
||||||
) {
|
) {
|
||||||
serverSettings.Logger = logger
|
serverSettings.Logger = logger
|
||||||
|
|
||||||
|
var dotSettings dot.Settings
|
||||||
providersData := provider.NewProviders()
|
providersData := provider.NewProviders()
|
||||||
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
|
dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.DoT.Providers))
|
||||||
for i := range settings.Providers {
|
for i := range settings.DoT.Providers {
|
||||||
var err error
|
var err error
|
||||||
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
|
dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.DoT.Providers[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // this should already had been checked
|
panic(err) // this should already had been checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dotSettings.IPVersion = "ipv4"
|
||||||
ipVersion := "ipv4"
|
if *settings.DoT.IPv6 {
|
||||||
if *settings.IPv6 {
|
dotSettings.IPVersion = "ipv6"
|
||||||
ipVersion = "ipv6"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var dialer server.Dialer
|
serverSettings.Dialer, err = dot.New(dotSettings)
|
||||||
switch settings.UpstreamType {
|
if err != nil {
|
||||||
case "dot":
|
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
||||||
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.Caching {
|
if *settings.DoT.Caching {
|
||||||
lruCache, err := lru.New(lru.Settings{})
|
lruCache, err := lru.New(lru.Settings{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
|
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
|
||||||
@@ -104,22 +70,5 @@ func buildServerSettings(settings settings.DNS,
|
|||||||
}
|
}
|
||||||
serverSettings.Middlewares = append(serverSettings.Middlewares, filterMiddleware)
|
serverSettings.Middlewares = append(serverSettings.Middlewares, filterMiddleware)
|
||||||
|
|
||||||
localResolversAddrPorts := make([]netip.AddrPort, len(localResolvers))
|
|
||||||
const defaultDNSPort = 53
|
|
||||||
for i, addr := range localResolvers {
|
|
||||||
localResolversAddrPorts[i] = netip.AddrPortFrom(addr, defaultDNSPort)
|
|
||||||
}
|
|
||||||
localDNSMiddleware, err := localdns.New(localdns.Settings{
|
|
||||||
Resolvers: localResolversAddrPorts, // auto-detected at container start only
|
|
||||||
Logger: logger,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return server.Settings{}, fmt.Errorf("creating local DNS middleware: %w", err)
|
|
||||||
}
|
|
||||||
// Place after cache middleware, since we want to avoid caching for local
|
|
||||||
// hostnames that may change regularly.
|
|
||||||
// Place after filter middleware to avoid conflicts with the rebinding protection.
|
|
||||||
serverSettings.Middlewares = append(serverSettings.Middlewares, localDNSMiddleware)
|
|
||||||
|
|
||||||
return serverSettings, nil
|
return serverSettings, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-21
@@ -2,37 +2,32 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/check"
|
"github.com/qdm12/dns/v2/pkg/check"
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
|
|
||||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||||
"github.com/qdm12/dns/v2/pkg/server"
|
"github.com/qdm12/dns/v2/pkg/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errUpdateBlockLists = errors.New("cannot update filter block lists")
|
||||||
|
|
||||||
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) {
|
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) {
|
||||||
|
err = l.updateFiles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", errUpdateBlockLists, err)
|
||||||
|
}
|
||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
var updateSettings update.Settings
|
|
||||||
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
|
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger)
|
||||||
err = l.filter.Update(updateSettings)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("updating filter for rebinding protection: %w", err)
|
return nil, fmt.Errorf("building DoT settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.updateFiles(ctx, settings)
|
server, err := server.New(dotSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.logger.Warn("downloading block lists failed, skipping: " + err.Error())
|
return nil, fmt.Errorf("creating DoT server: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("building server settings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server, err := server.New(serverSettings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("creating server: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runError, err = server.Start(ctx)
|
runError, err = server.Start(ctx)
|
||||||
@@ -42,12 +37,11 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
|||||||
l.server = server
|
l.server = server
|
||||||
|
|
||||||
// use internal DNS server
|
// use internal DNS server
|
||||||
const defaultDNSPort = 53
|
|
||||||
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
|
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
|
||||||
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
|
IP: settings.ServerAddress,
|
||||||
})
|
})
|
||||||
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
||||||
IPs: []netip.Addr{settings.ServerAddress},
|
IP: settings.ServerAddress,
|
||||||
ResolvPath: l.resolvConf,
|
ResolvPath: l.resolvConf,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
|||||||
|
|
||||||
// Check for only update period change
|
// Check for only update period change
|
||||||
tempSettings := s.settings.Copy()
|
tempSettings := s.settings.Copy()
|
||||||
*tempSettings.UpdatePeriod = *settings.UpdatePeriod
|
*tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod
|
||||||
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
||||||
|
|
||||||
s.settings = settings
|
s.settings = settings
|
||||||
@@ -40,7 +40,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
|||||||
|
|
||||||
// Restart
|
// Restart
|
||||||
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
||||||
if *settings.ServerEnabled {
|
if *settings.DoT.Enabled {
|
||||||
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||||
}
|
}
|
||||||
return outcome
|
return outcome
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
|||||||
timer.Stop()
|
timer.Stop()
|
||||||
timerIsStopped := true
|
timerIsStopped := true
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
if period := *settings.UpdatePeriod; period > 0 {
|
if period := *settings.DoT.UpdatePeriod; period > 0 {
|
||||||
timer.Reset(period)
|
timer.Reset(period)
|
||||||
timerIsStopped = false
|
timerIsStopped = false
|
||||||
}
|
}
|
||||||
@@ -30,14 +30,11 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
|||||||
lastTick = l.timeNow()
|
lastTick = l.timeNow()
|
||||||
|
|
||||||
status := l.GetStatus()
|
status := l.GetStatus()
|
||||||
settings := l.GetSettings()
|
|
||||||
if status == constants.Running {
|
if status == constants.Running {
|
||||||
if err := l.updateFiles(ctx, settings); err != nil {
|
if err := l.updateFiles(ctx); err != nil {
|
||||||
l.statusManager.SetStatus(constants.Crashed)
|
l.statusManager.SetStatus(constants.Crashed)
|
||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
l.logger.Warn("skipping DNS server restart due to failed files update")
|
l.logger.Warn("skipping DNS server restart due to failed files update")
|
||||||
settings := l.GetSettings()
|
|
||||||
timer.Reset(*settings.UpdatePeriod)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,14 +42,15 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
|||||||
_, _ = l.statusManager.ApplyStatus(ctx, constants.Stopped)
|
_, _ = l.statusManager.ApplyStatus(ctx, constants.Stopped)
|
||||||
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
||||||
|
|
||||||
timer.Reset(*settings.UpdatePeriod)
|
settings := l.GetSettings()
|
||||||
|
timer.Reset(*settings.DoT.UpdatePeriod)
|
||||||
case <-l.updateTicker:
|
case <-l.updateTicker:
|
||||||
if !timer.Stop() {
|
if !timer.Stop() {
|
||||||
<-timer.C
|
<-timer.C
|
||||||
}
|
}
|
||||||
timerIsStopped = true
|
timerIsStopped = true
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
newUpdatePeriod := *settings.UpdatePeriod
|
newUpdatePeriod := *settings.DoT.UpdatePeriod
|
||||||
if newUpdatePeriod == 0 {
|
if newUpdatePeriod == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import (
|
|||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/blockbuilder"
|
"github.com/qdm12/dns/v2/pkg/blockbuilder"
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
|
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l *Loop) updateFiles(ctx context.Context, settings settings.DNS) (err error) {
|
func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
||||||
|
settings := l.GetSettings()
|
||||||
|
|
||||||
l.logger.Info("downloading hostnames and IP block lists")
|
l.logger.Info("downloading hostnames and IP block lists")
|
||||||
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
|
blacklistSettings := settings.DoT.Blacklist.ToBlockBuilderSettings(l.client)
|
||||||
|
|
||||||
blockBuilder, err := blockbuilder.New(blacklistSettings)
|
blockBuilder, err := blockbuilder.New(blacklistSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func isDeleteMatchInstruction(instruction string) bool {
|
|||||||
fields := strings.Fields(instruction)
|
fields := strings.Fields(instruction)
|
||||||
for i, field := range fields {
|
for i, field := range fields {
|
||||||
switch {
|
switch {
|
||||||
case field != "-D" && field != "--delete":
|
case field != "-D" && field != "--delete": //nolint:goconst
|
||||||
continue
|
continue
|
||||||
case i == len(fields)-1: // malformed: missing chain name
|
case i == len(fields)-1: // malformed: missing chain name
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/routing"
|
"github.com/qdm12/gluetun/internal/routing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct { //nolint:maligned
|
||||||
runner CmdRunner
|
runner CmdRunner
|
||||||
logger Logger
|
logger Logger
|
||||||
iptablesMutex sync.Mutex
|
iptablesMutex sync.Mutex
|
||||||
defaultRoutes []routing.DefaultRoute
|
ip6tablesMutex sync.Mutex
|
||||||
localNetworks []routing.LocalNetwork
|
defaultRoutes []routing.DefaultRoute
|
||||||
|
localNetworks []routing.LocalNetwork
|
||||||
|
|
||||||
// Fixed state
|
// Fixed state
|
||||||
ipTables string
|
ipTables string
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ func (c *Config) runIP6tablesInstruction(ctx context.Context, instruction string
|
|||||||
if c.ip6Tables == "" {
|
if c.ip6Tables == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
c.iptablesMutex.Lock() // only one ip6tables command at once
|
c.ip6tablesMutex.Lock() // only one ip6tables command at once
|
||||||
defer c.iptablesMutex.Unlock()
|
defer c.ip6tablesMutex.Unlock()
|
||||||
|
|
||||||
if isDeleteMatchInstruction(instruction) {
|
if isDeleteMatchInstruction(instruction) {
|
||||||
return deleteIPTablesRule(ctx, c.ip6Tables, instruction,
|
return deleteIPTablesRule(ctx, c.ip6Tables, instruction,
|
||||||
@@ -58,7 +58,7 @@ var ErrPolicyNotValid = errors.New("policy is not valid")
|
|||||||
|
|
||||||
func (c *Config) setIPv6AllPolicies(ctx context.Context, policy string) error {
|
func (c *Config) setIPv6AllPolicies(ctx context.Context, policy string) error {
|
||||||
switch policy {
|
switch policy {
|
||||||
case "ACCEPT", "DROP":
|
case "ACCEPT", "DROP": //nolint:goconst
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: %s", ErrPolicyNotValid, policy)
|
return fmt.Errorf("%w: %s", ErrPolicyNotValid, policy)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,9 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
|
|||||||
defaultInterface string, connection models.Connection, remove bool,
|
defaultInterface string, connection models.Connection, remove bool,
|
||||||
) error {
|
) error {
|
||||||
protocol := connection.Protocol
|
protocol := connection.Protocol
|
||||||
|
if protocol == "tcp-client" {
|
||||||
|
protocol = "tcp" //nolint:goconst
|
||||||
|
}
|
||||||
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
|
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
|
||||||
appendOrDelete(remove), connection.IP, defaultInterface, protocol,
|
appendOrDelete(remove), connection.IP, defaultInterface, protocol,
|
||||||
protocol, connection.Port)
|
protocol, connection.Port)
|
||||||
@@ -215,9 +218,6 @@ func (c *Config) redirectPort(ctx context.Context, intf string,
|
|||||||
interfaceFlag = ""
|
interfaceFlag = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
c.iptablesMutex.Lock()
|
|
||||||
defer c.iptablesMutex.Unlock()
|
|
||||||
|
|
||||||
err = c.runIptablesInstructions(ctx, []string{
|
err = c.runIptablesInstructions(ctx, []string{
|
||||||
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
|
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
|
||||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||||
|
|||||||
@@ -323,12 +323,12 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
|
|||||||
|
|
||||||
func parseProtocol(s string) (protocol string, err error) {
|
func parseProtocol(s string) (protocol string, err error) {
|
||||||
switch s {
|
switch s {
|
||||||
case "0", "all":
|
case "0":
|
||||||
case "1", "icmp":
|
case "1":
|
||||||
protocol = "icmp"
|
protocol = "icmp"
|
||||||
case "6", "tcp":
|
case "6":
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
case "17", "udp":
|
case "17":
|
||||||
protocol = "udp"
|
protocol = "udp"
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)
|
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ 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
|
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
|
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
|
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{
|
table: chain{
|
||||||
name: "INPUT",
|
name: "INPUT",
|
||||||
@@ -112,17 +111,6 @@ num pkts bytes target prot opt in out source destinati
|
|||||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
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"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,356 +0,0 @@
|
|||||||
package healthcheck
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/healthcheck/dns"
|
|
||||||
"github.com/qdm12/gluetun/internal/healthcheck/icmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Checker struct {
|
|
||||||
tlsDialAddrs []string
|
|
||||||
dialer *net.Dialer
|
|
||||||
echoer *icmp.Echoer
|
|
||||||
dnsClient *dns.Client
|
|
||||||
logger Logger
|
|
||||||
icmpTargetIPs []netip.Addr
|
|
||||||
smallCheckType string
|
|
||||||
startupOnFail bool
|
|
||||||
|
|
||||||
icmpNotPermitted *bool
|
|
||||||
|
|
||||||
// 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 following:
|
|
||||||
// - TCP+TLS dial addresses
|
|
||||||
// - ICMP echo IP addresses to target
|
|
||||||
// - the desired small check type (dns or icmp)
|
|
||||||
// - whether to startup the periodic checks if the startup check fails.
|
|
||||||
// This function MUST be called before calling [Checker.Start].
|
|
||||||
func (c *Checker) SetConfig(tlsDialAddrs []string, icmpTargets []netip.Addr,
|
|
||||||
smallCheckType string, startupOnFail bool,
|
|
||||||
) {
|
|
||||||
c.tlsDialAddrs = tlsDialAddrs
|
|
||||||
c.icmpTargetIPs = icmpTargets
|
|
||||||
c.smallCheckType = smallCheckType
|
|
||||||
c.startupOnFail = startupOnFail
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the [Checker] which behaves differently according to its
|
|
||||||
// internal field startupOnFail, which is set by calling [Checker.SetConfig].
|
|
||||||
//
|
|
||||||
// By default, startupOnFail should be false and the behavior is as follows:
|
|
||||||
// A blocking 6s-timed TCP+TLS check is performed first. If it fails,
|
|
||||||
// an error is returned and the [Checker] is not started.
|
|
||||||
// On success, it starts the periodic checks in a separate goroutine, returning
|
|
||||||
// the runError error channel and a nil error.
|
|
||||||
//
|
|
||||||
// If startupOnFail is true, the behavior is as follows:
|
|
||||||
// A blocking 6s-timed TCP+TLS check is performed first. If it fails,
|
|
||||||
// the error is sent to the runError channel, but no error is returned
|
|
||||||
// and the [Checker] continues to start the periodic checks in a separate goroutine, returning
|
|
||||||
// the runError error channel and a nil error.
|
|
||||||
//
|
|
||||||
// The periodic checks consist in:
|
|
||||||
// - a "small" ICMP echo check every minute
|
|
||||||
// - a "full" TCP+TLS check every 5 minutes
|
|
||||||
//
|
|
||||||
// The [Checker] has to be ultimately stopped by calling [Checker.Stop].
|
|
||||||
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
|
|
||||||
if len(c.tlsDialAddrs) == 0 || len(c.icmpTargetIPs) == 0 || c.smallCheckType == "" {
|
|
||||||
panic("call Checker.SetConfig with non empty values before Checker.Start")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.icmpNotPermitted != nil && *c.icmpNotPermitted {
|
|
||||||
// restore forced check type to dns if icmp was found to be not permitted
|
|
||||||
c.smallCheckType = smallCheckDNS
|
|
||||||
}
|
|
||||||
c.echoer.Reset()
|
|
||||||
|
|
||||||
// runErrorCh MUST be buffered in the case startupOnFail is true, and
|
|
||||||
// a startup error was encountered, to avoid blocking the startup
|
|
||||||
// goroutine when sending the error, especially since the caller may
|
|
||||||
// not be ready to receive from the channel yet.
|
|
||||||
runErrorCh := make(chan error, 1)
|
|
||||||
runError = runErrorCh
|
|
||||||
err = c.startupCheck(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("startup check: %w", err)
|
|
||||||
if !c.startupOnFail {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
runErrorCh <- err
|
|
||||||
}
|
|
||||||
|
|
||||||
ready := make(chan struct{})
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
c.stop = cancel
|
|
||||||
done := make(chan struct{})
|
|
||||||
c.done = done
|
|
||||||
const smallCheckPeriod = time.Minute
|
|
||||||
smallCheckTimer := time.NewTimer(smallCheckPeriod)
|
|
||||||
const fullCheckPeriod = 5 * time.Minute
|
|
||||||
fullCheckTimer := time.NewTimer(fullCheckPeriod)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
continue
|
|
||||||
case runErrorCh <- err:
|
|
||||||
}
|
|
||||||
smallCheckTimer.Reset(smallCheckPeriod)
|
|
||||||
case <-fullCheckTimer.C:
|
|
||||||
err := c.fullPeriodicCheck(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("full periodic check: %w", err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
continue
|
|
||||||
case runErrorCh <- err:
|
|
||||||
}
|
|
||||||
fullCheckTimer.Reset(fullCheckPeriod)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
<-ready
|
|
||||||
return runError, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) Stop() error {
|
|
||||||
c.stop()
|
|
||||||
<-c.done
|
|
||||||
c.tlsDialAddrs = nil
|
|
||||||
c.icmpTargetIPs = nil
|
|
||||||
c.smallCheckType = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
|
|
||||||
icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs))
|
|
||||||
copy(icmpTargetIPs, c.icmpTargetIPs)
|
|
||||||
tryTimeouts := []time.Duration{
|
|
||||||
5 * time.Second,
|
|
||||||
5 * time.Second,
|
|
||||||
5 * time.Second,
|
|
||||||
10 * time.Second,
|
|
||||||
10 * time.Second,
|
|
||||||
10 * time.Second,
|
|
||||||
15 * time.Second,
|
|
||||||
15 * time.Second,
|
|
||||||
15 * time.Second,
|
|
||||||
30 * time.Second,
|
|
||||||
}
|
|
||||||
check := func(ctx context.Context, try int) error {
|
|
||||||
if c.smallCheckType == smallCheckDNS {
|
|
||||||
return c.dnsClient.Check(ctx)
|
|
||||||
}
|
|
||||||
ip := icmpTargetIPs[try%len(icmpTargetIPs)]
|
|
||||||
err := c.echoer.Echo(ctx, ip)
|
|
||||||
if c.icmpNotPermitted == nil && errors.Is(err, icmp.ErrNotPermitted) {
|
|
||||||
c.icmpNotPermitted = new(bool)
|
|
||||||
*c.icmpNotPermitted = true
|
|
||||||
c.smallCheckType = smallCheckDNS
|
|
||||||
c.logger.Infof("%s; permanently falling back to %s checks",
|
|
||||||
err, smallCheckTypeToString(c.smallCheckType))
|
|
||||||
return c.dnsClient.Check(ctx)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return withRetries(ctx, tryTimeouts, c.logger, smallCheckTypeToString(c.smallCheckType), check)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) fullPeriodicCheck(ctx context.Context) error {
|
|
||||||
// 20s timeout in case the connection is under stress
|
|
||||||
// See https://github.com/qdm12/gluetun/issues/2270
|
|
||||||
tryTimeouts := []time.Duration{10 * time.Second, 15 * time.Second, 30 * time.Second}
|
|
||||||
check := func(ctx context.Context, try int) error {
|
|
||||||
tlsDialAddr := c.tlsDialAddrs[try%len(c.tlsDialAddrs)]
|
|
||||||
return tcpTLSCheck(ctx, c.dialer, tlsDialAddr)
|
|
||||||
}
|
|
||||||
return withRetries(ctx, tryTimeouts, 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, tryTimeouts []time.Duration,
|
|
||||||
logger Logger, checkName string, check func(ctx context.Context, try int) error,
|
|
||||||
) error {
|
|
||||||
maxTries := len(tryTimeouts)
|
|
||||||
type errData struct {
|
|
||||||
err error
|
|
||||||
durationMS int64
|
|
||||||
}
|
|
||||||
errs := make([]errData, maxTries)
|
|
||||||
for i, timeout := range tryTimeouts {
|
|
||||||
start := time.Now()
|
|
||||||
checkCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
err := check(checkCtx, i)
|
|
||||||
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, i+1, maxTries, err)
|
|
||||||
errs[i].err = err
|
|
||||||
errs[i].durationMS = time.Since(start).Round(time.Millisecond).Milliseconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
errStrings := make([]string, len(errs))
|
|
||||||
for i, err := range errs {
|
|
||||||
errStrings[i] = fmt.Sprintf("attempt %d (%dms): %s", i+1, err.durationMS, err.err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%w:\n\t%s", ErrAllCheckTriesFailed, strings.Join(errStrings, "\n\t"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) startupCheck(ctx context.Context) error {
|
|
||||||
// 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. However, since the addresses to dial
|
|
||||||
// may be multiple, we run the check in parallel. If any succeeds, the check passes.
|
|
||||||
// This is to prevent false negatives at startup, if one of the addresses is down
|
|
||||||
// for external reasons.
|
|
||||||
const timeout = 6 * time.Second
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
errCh := make(chan error)
|
|
||||||
|
|
||||||
for _, address := range c.tlsDialAddrs {
|
|
||||||
go func(addr string) {
|
|
||||||
err := tcpTLSCheck(ctx, c.dialer, addr)
|
|
||||||
errCh <- err
|
|
||||||
}(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make([]error, 0, len(c.tlsDialAddrs))
|
|
||||||
success := false
|
|
||||||
for range c.tlsDialAddrs {
|
|
||||||
err := <-errCh
|
|
||||||
if err == nil {
|
|
||||||
success = true
|
|
||||||
cancel()
|
|
||||||
continue
|
|
||||||
} else if success {
|
|
||||||
continue // ignore canceled errors after success
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Debugf("startup check parallel attempt failed: %s", err)
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
if success {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
errStrings := make([]string, len(errs))
|
|
||||||
for i, err := range errs {
|
|
||||||
errStrings[i] = fmt.Sprintf("parallel attempt %d/%d failed: %s", i+1, len(errs), err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%w: %s", ErrAllCheckTriesFailed, strings.Join(errStrings, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
smallCheckDNS = "dns"
|
|
||||||
smallCheckICMP = "icmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func smallCheckTypeToString(smallCheckType string) string {
|
|
||||||
switch smallCheckType {
|
|
||||||
case smallCheckICMP:
|
|
||||||
return "ICMP echo"
|
|
||||||
case smallCheckDNS:
|
|
||||||
return "plain DNS over UDP"
|
|
||||||
default:
|
|
||||||
panic("unknown small check type: " + smallCheckType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrHTTPStatusNotOK = errors.New("HTTP response status is not OK")
|
var ErrHTTPStatusNotOK = errors.New("HTTP response status is not OK")
|
||||||
@@ -22,9 +21,6 @@ func NewClient(httpClient *http.Client) *Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Check(ctx context.Context, url string) error {
|
func (c *Client) Check(ctx context.Context, url string) error {
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
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].String()
|
|
||||||
resolver := &net.Resolver{
|
|
||||||
PreferGo: true,
|
|
||||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
dialer := net.Dialer{}
|
|
||||||
return dialer.DialContext(ctx, "udp", dnsAddr)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ips, err := resolver.LookupIP(ctx, "ip", "github.com")
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
|
||||||
return fmt.Errorf("with DNS server %s: %w", dnsAddr, err)
|
|
||||||
case len(ips) == 0:
|
|
||||||
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
|
||||||
return fmt.Errorf("with DNS server %s: %w", dnsAddr, ErrLookupNoIPs)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,15 +9,13 @@ import (
|
|||||||
type handler struct {
|
type handler struct {
|
||||||
healthErr error
|
healthErr error
|
||||||
healthErrMu sync.RWMutex
|
healthErrMu sync.RWMutex
|
||||||
logger Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var errHealthcheckNotRunYet = errors.New("healthcheck did not run yet")
|
var errHealthcheckNotRunYet = errors.New("healthcheck did not run yet")
|
||||||
|
|
||||||
func newHandler(logger Logger) *handler {
|
func newHandler() *handler {
|
||||||
return &handler{
|
return &handler{
|
||||||
healthErr: errHealthcheckNotRunYet,
|
healthErr: errHealthcheckNotRunYet,
|
||||||
logger: logger,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -7,41 +7,40 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Checker_fullcheck(t *testing.T) {
|
func Test_Server_healthCheck(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("canceled real dialer", func(t *testing.T) {
|
t.Run("canceled real dialer", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dialer := &net.Dialer{}
|
dialer := &net.Dialer{}
|
||||||
addresses := []string{"badaddress:9876", "cloudflare.com:443", "google.com:443"}
|
const address = "cloudflare.com:443"
|
||||||
|
|
||||||
checker := &Checker{
|
server := &Server{
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
tlsDialAddrs: addresses,
|
config: settings.Health{
|
||||||
|
TargetAddress: address,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
err := checker.fullPeriodicCheck(canceledCtx)
|
err := server.healthCheck(canceledCtx)
|
||||||
|
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.EqualError(t, err, "TCP+TLS dial: context canceled")
|
assert.Contains(t, err.Error(), "operation was canceled")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("dial localhost:0", func(t *testing.T) {
|
t.Run("dial localhost:0", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const timeout = 100 * time.Millisecond
|
listener, err := net.Listen("tcp4", "localhost:0")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
listenConfig := &net.ListenConfig{}
|
|
||||||
listener, err := listenConfig.Listen(ctx, "tcp4", "localhost:0")
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
err = listener.Close()
|
err = listener.Close()
|
||||||
@@ -51,12 +50,18 @@ func Test_Checker_fullcheck(t *testing.T) {
|
|||||||
listeningAddress := listener.Addr()
|
listeningAddress := listener.Addr()
|
||||||
|
|
||||||
dialer := &net.Dialer{}
|
dialer := &net.Dialer{}
|
||||||
checker := &Checker{
|
server := &Server{
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
tlsDialAddrs: []string{listeningAddress.String()},
|
config: settings.Health{
|
||||||
|
TargetAddress: listeningAddress.String(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = checker.fullPeriodicCheck(ctx)
|
const timeout = 100 * time.Millisecond
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = server.healthCheck(ctx)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
package icmp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
cryptorand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand/v2"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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
|
|
||||||
seqStart time.Time
|
|
||||||
id int
|
|
||||||
seq int
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset resets the [Echoer] icmp echo parameters:
|
|
||||||
// - ID is assigned a new random value
|
|
||||||
// - sequence is reset to 1
|
|
||||||
// - sequence start time is set to now
|
|
||||||
// It is used when the sequence is complete or when the VPN reconnects.
|
|
||||||
func (e *Echoer) Reset() {
|
|
||||||
const uint16Bytes = 2
|
|
||||||
idBytes := make([]byte, uint16Bytes)
|
|
||||||
_, _ = e.randomSource.Read(idBytes)
|
|
||||||
e.id = int(binary.BigEndian.Uint16(idBytes))
|
|
||||||
e.seq = 1
|
|
||||||
e.seqStart = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTimedOut = errors.New("timed out waiting for ICMP echo reply")
|
|
||||||
ErrNotPermitted = errors.New("not permitted")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (e *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 maxSeq = 1<<16 - 1
|
|
||||||
const refreshIDInterval = 5 * time.Minute
|
|
||||||
if e.seq > maxSeq && time.Since(e.seqStart) >= refreshIDInterval {
|
|
||||||
e.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
message := buildMessageToSend(ipVersion, e.id, e.seq, e.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 to %s: %w", ip, err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
e.seq++
|
|
||||||
}()
|
|
||||||
|
|
||||||
receivedData, err := receiveEchoReply(conn, e.id, e.seq, e.buffer, ipVersion, e.logger)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, net.ErrClosed) && ctx.Err() != nil {
|
|
||||||
return fmt.Errorf("%w from %s", ErrTimedOut, ip)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("receiving ICMP echo reply from %s: %w", ip, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sentData := message.Body.(*icmp.Echo).Data //nolint:forcetypeassert
|
|
||||||
if !bytes.Equal(receivedData, sentData) {
|
|
||||||
return fmt.Errorf("%w: sent %x to %s and received %x", ErrICMPEchoDataMismatch, sentData, ip, receivedData)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMessageToSend(ipVersion string, id, seq int, randomSource io.Reader) (message *icmp.Message) {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
const size = 32
|
|
||||||
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: seq,
|
|
||||||
Data: messageBodyData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
func receiveEchoReply(conn net.PacketConn, id, seq 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:
|
|
||||||
switch {
|
|
||||||
case id != body.ID:
|
|
||||||
logger.Warnf("ignoring ICMP echo reply mismatching expected id %d "+
|
|
||||||
"(id: %d, seq: %d, type: %d, code: %d, length: %d, return address %s)",
|
|
||||||
id, body.Seq, body.ID, message.Type, message.Code, len(packetBytes), returnAddr)
|
|
||||||
continue // not the ID we are looking for
|
|
||||||
case seq != body.Seq:
|
|
||||||
logger.Warnf("ignoring ICMP echo reply mismatching expected sequence number %d "+
|
|
||||||
"(id: %d, seq: %d, type: %d, code: %d, length: %d, return address %s)",
|
|
||||||
seq, body.ID, body.Seq, message.Type, message.Code, len(packetBytes), returnAddr)
|
|
||||||
continue // not the seq 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 and seq %d)",
|
|
||||||
message.Code, returnAddr, id, seq)
|
|
||||||
// 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 and seq %d)",
|
|
||||||
message.Code, returnAddr, id, seq)
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: %T (type %d, code %d, return address %s, expected id %d and seq %d)",
|
|
||||||
ErrICMPBodyUnsupported, body, message.Type, message.Code, returnAddr, id, seq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package icmp
|
|
||||||
|
|
||||||
type Logger interface {
|
|
||||||
Debugf(format string, args ...any)
|
|
||||||
Warnf(format string, args ...any)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package healthcheck
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Debug(s string)
|
||||||
|
Info(s string)
|
||||||
|
Error(s string)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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,13 +10,14 @@ import (
|
|||||||
func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
const readHeaderTimeout = 100 * time.Millisecond
|
loopDone := make(chan struct{})
|
||||||
const readTimeout = 500 * time.Millisecond
|
go s.runHealthcheckLoop(ctx, loopDone)
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
Addr: s.config.ServerAddress,
|
Addr: s.config.ServerAddress,
|
||||||
Handler: s.handler,
|
Handler: s.handler,
|
||||||
ReadHeaderTimeout: readHeaderTimeout,
|
ReadHeaderTimeout: s.config.ReadHeaderTimeout,
|
||||||
ReadTimeout: readTimeout,
|
ReadTimeout: s.config.ReadTimeout,
|
||||||
}
|
}
|
||||||
serverDone := make(chan struct{})
|
serverDone := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
@@ -36,5 +37,6 @@ func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
s.logger.Error(err.Error())
|
s.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<-loopDone
|
||||||
<-serverDone
|
<-serverDone
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package healthcheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
@@ -10,21 +11,30 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
logger Logger
|
logger Logger
|
||||||
handler *handler
|
handler *handler
|
||||||
|
dialer *net.Dialer
|
||||||
config settings.Health
|
config settings.Health
|
||||||
|
vpn vpnHealth
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(config settings.Health, logger Logger) *Server {
|
func NewServer(config settings.Health,
|
||||||
|
logger Logger, vpnLoop StatusApplier,
|
||||||
|
) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
handler: newHandler(logger),
|
handler: newHandler(),
|
||||||
config: config,
|
dialer: &net.Dialer{
|
||||||
|
Resolver: &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: config,
|
||||||
|
vpn: vpnHealth{
|
||||||
|
loop: vpnLoop,
|
||||||
|
healthyWait: *config.VPN.Initial,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) SetError(err error) {
|
|
||||||
s.handler.setErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusApplier interface {
|
type StatusApplier interface {
|
||||||
ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
||||||
outcome string, err error)
|
outcome string, err error)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ func (h *handler) isAuthorized(responseWriter http.ResponseWriter, request *http
|
|||||||
}
|
}
|
||||||
basicAuth := request.Header.Get("Proxy-Authorization")
|
basicAuth := request.Header.Get("Proxy-Authorization")
|
||||||
if basicAuth == "" {
|
if basicAuth == "" {
|
||||||
|
h.logger.Info("Proxy-Authorization header not found from " + request.RemoteAddr)
|
||||||
responseWriter.Header().Set("Proxy-Authenticate", `Basic realm="Access to Gluetun over HTTP"`)
|
responseWriter.Header().Set("Proxy-Authenticate", `Basic realm="Access to Gluetun over HTTP"`)
|
||||||
responseWriter.WriteHeader(http.StatusProxyAuthRequired)
|
responseWriter.WriteHeader(http.StatusProxyAuthRequired)
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ func (s *Server) Run(ctx context.Context, ready chan<- struct{}, done chan<- str
|
|||||||
|
|
||||||
crashed := make(chan struct{})
|
crashed := make(chan struct{})
|
||||||
shutdownDone := make(chan struct{})
|
shutdownDone := make(chan struct{})
|
||||||
listenCtx, listenCancel := context.WithCancel(ctx)
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(shutdownDone)
|
defer close(shutdownDone)
|
||||||
defer listenCancel()
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
case <-crashed:
|
case <-crashed:
|
||||||
@@ -39,8 +37,7 @@ func (s *Server) Run(ctx context.Context, ready chan<- struct{}, done chan<- str
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
listenConfig := &net.ListenConfig{}
|
listener, err := net.Listen("tcp", s.address)
|
||||||
listener, err := listenConfig.Listen(listenCtx, "tcp", s.address)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(s.addressSet)
|
close(s.addressSet)
|
||||||
close(crashed) // stop shutdown goroutine
|
close(crashed) // stop shutdown goroutine
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package mod
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errBuiltinModuleNotFound = errors.New("builtin module not found")
|
|
||||||
|
|
||||||
func checkModulesBuiltin(modulesPath, moduleName string) error {
|
|
||||||
f, err := os.Open(filepath.Join(modulesPath, "modules.builtin"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
moduleName = strings.TrimSuffix(moduleName, ".ko")
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(f)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
line = strings.TrimSuffix(line, ".ko")
|
|
||||||
if strings.HasSuffix(line, "/"+moduleName) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%w: %s", errBuiltinModuleNotFound, moduleName)
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
package mod
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"compress/gzip"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errModuleNameUnknown = errors.New("unknown module name")
|
|
||||||
errKernelFeatureIsModule = errors.New("kernel feature is a module, not built-in")
|
|
||||||
errKernelFeatureNotSet = errors.New("kernel feature not set")
|
|
||||||
errKernelFeatureNotFound = errors.New("kernel feature not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// checkProcConfig checks /proc/config.gz for a the kernel feature corresponding
|
|
||||||
// to the given module name. If the kernel feature is found and set to "y", it returns nil.
|
|
||||||
// If the kernel feature is found and set to "m", it returns an error indicating that the kernel
|
|
||||||
// feature is a module, not built-in.
|
|
||||||
// If the kernel feature is found and not set, it returns an error indicating that the kernel
|
|
||||||
// feature is not set. If the kernel feature is not found, it returns an error indicating that the kernel
|
|
||||||
// feature is not found.
|
|
||||||
func checkProcConfig(moduleName string) error {
|
|
||||||
f, err := os.Open("/proc/config.gz")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
gz, err := gzip.NewReader(f)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating gzip reader: %w", err)
|
|
||||||
}
|
|
||||||
defer gz.Close()
|
|
||||||
|
|
||||||
// If any group of kernel features is satisfied, then the module is considered supported.
|
|
||||||
kernelFeatureGroups, ok := moduleNameToKernelFeatureGroups(moduleName)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("%w: %s", errModuleNameUnknown, moduleName)
|
|
||||||
}
|
|
||||||
groups := make([]map[string]bool, len(kernelFeatureGroups))
|
|
||||||
for i, group := range kernelFeatureGroups {
|
|
||||||
featureToOK := make(map[string]bool)
|
|
||||||
for _, feature := range group {
|
|
||||||
featureToOK[feature] = false
|
|
||||||
}
|
|
||||||
groups[i] = featureToOK
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(gz)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
for _, featureToOK := range groups {
|
|
||||||
for name, ok := range featureToOK {
|
|
||||||
switch {
|
|
||||||
case ok:
|
|
||||||
case strings.HasPrefix(line, name+"=m"):
|
|
||||||
return fmt.Errorf("%w: %s", errKernelFeatureIsModule, name)
|
|
||||||
case strings.HasPrefix(line, name+"=y"):
|
|
||||||
featureToOK[name] = true
|
|
||||||
if allFeaturesOK(featureToOK) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case strings.HasPrefix(line, "# "+name+" is not set"):
|
|
||||||
return fmt.Errorf("%w: %s", errKernelFeatureNotSet, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%w: for module name %s", errKernelFeatureNotFound, moduleName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func moduleNameToKernelFeatureGroups(moduleName string) (featureGroups [][]string, ok bool) {
|
|
||||||
moduleMap := map[string][][]string{
|
|
||||||
"nf_tables": {{"CONFIG_NF_TABLES"}},
|
|
||||||
|
|
||||||
// Netfilter Matches
|
|
||||||
"xt_conntrack": {{"CONFIG_NETFILTER_XT_MATCH_CONNTRACK"}},
|
|
||||||
"xt_connmark": {
|
|
||||||
{"CONFIG_NETFILTER_XT_CONNMARK"},
|
|
||||||
{"CONFIG_NETFILTER_XT_MATCH_CONNMARK", "CONFIG_NETFILTER_XT_TARGET_CONNMARK"},
|
|
||||||
},
|
|
||||||
"xt_mark": {
|
|
||||||
{"CONFIG_NETFILTER_XT_MARK"},
|
|
||||||
{"CONFIG_NETFILTER_XT_MATCH_MARK", "CONFIG_NETFILTER_XT_TARGET_MARK"},
|
|
||||||
},
|
|
||||||
"nf_conntrack_netlink": {{"CONFIG_NF_CT_NETLINK"}},
|
|
||||||
"nf_reject_ipv4": {{"CONFIG_NF_REJECT_IPV4"}},
|
|
||||||
|
|
||||||
// Common Netfilter Targets
|
|
||||||
"xt_log": {{"CONFIG_NETFILTER_XT_TARGET_LOG"}},
|
|
||||||
"xt_reject": {
|
|
||||||
{"CONFIG_IP_NF_TARGET_REJECT", "CONFIG_NF_REJECT_IPV4"},
|
|
||||||
{"CONFIG_NETFILTER_XT_TARGET_REJECT", "CONFIG_NF_REJECT_IPV4"},
|
|
||||||
},
|
|
||||||
"xt_masquerade": {{"CONFIG_NETFILTER_XT_TARGET_MASQUERADE"}},
|
|
||||||
|
|
||||||
// Additional Netfilter Matches
|
|
||||||
"xt_addrtype": {{"CONFIG_NETFILTER_XT_MATCH_ADDRTYPE"}},
|
|
||||||
"xt_comment": {{"CONFIG_NETFILTER_XT_MATCH_COMMENT"}},
|
|
||||||
"xt_multiport": {{"CONFIG_NETFILTER_XT_MATCH_MULTIPORT"}},
|
|
||||||
"xt_state": {{"CONFIG_NETFILTER_XT_MATCH_STATE"}},
|
|
||||||
"xt_tcpudp": {{"CONFIG_NETFILTER_XT_MATCH_TCPUDP"}},
|
|
||||||
|
|
||||||
// Tunneling and Virtualization
|
|
||||||
"tun": {{"CONFIG_TUN"}},
|
|
||||||
"bridge": {{"CONFIG_BRIDGE"}},
|
|
||||||
"veth": {{"CONFIG_VETH"}},
|
|
||||||
"vxlan": {{"CONFIG_VXLAN"}},
|
|
||||||
"wireguard": {{"CONFIG_WIREGUARD"}},
|
|
||||||
|
|
||||||
// Filesystems
|
|
||||||
"overlay": {{"CONFIG_OVERLAY_FS"}},
|
|
||||||
"fuse": {{"CONFIG_FUSE_FS"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
featureGroups, ok = moduleMap[strings.ToLower(moduleName)]
|
|
||||||
return featureGroups, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func allFeaturesOK(featureToOK map[string]bool) bool {
|
|
||||||
for _, ok := range featureToOK {
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
+30
-34
@@ -28,7 +28,36 @@ type moduleInfo struct {
|
|||||||
|
|
||||||
var ErrModulesDirectoryNotFound = errors.New("modules directory not found")
|
var ErrModulesDirectoryNotFound = errors.New("modules directory not found")
|
||||||
|
|
||||||
func getModulesInfo(modulesPath string) (modulesInfo map[string]moduleInfo, err error) {
|
func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) {
|
||||||
|
var utsName unix.Utsname
|
||||||
|
err = unix.Uname(&utsName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting unix uname release: %w", err)
|
||||||
|
}
|
||||||
|
release := unix.ByteSliceToString(utsName.Release[:])
|
||||||
|
release = strings.TrimSpace(release)
|
||||||
|
|
||||||
|
modulePaths := []string{
|
||||||
|
filepath.Join("/lib/modules", release),
|
||||||
|
filepath.Join("/usr/lib/modules", release),
|
||||||
|
}
|
||||||
|
|
||||||
|
var modulesPath string
|
||||||
|
var found bool
|
||||||
|
for _, modulesPath = range modulePaths {
|
||||||
|
info, err := os.Stat(modulesPath)
|
||||||
|
if err == nil && info.IsDir() {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("%w: %s are not valid existing directories"+
|
||||||
|
"; have you bind mounted the /lib/modules directory?",
|
||||||
|
ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
dependencyFilepath := filepath.Join(modulesPath, "modules.dep")
|
dependencyFilepath := filepath.Join(modulesPath, "modules.dep")
|
||||||
dependencyFile, err := os.Open(dependencyFilepath)
|
dependencyFile, err := os.Open(dependencyFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,39 +109,6 @@ func getModulesInfo(modulesPath string) (modulesInfo map[string]moduleInfo, err
|
|||||||
return modulesInfo, nil
|
return modulesInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getModulesPath() (string, error) {
|
|
||||||
release, err := getReleaseName()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("getting release name: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
modulePaths := []string{
|
|
||||||
filepath.Join("/lib/modules", release),
|
|
||||||
filepath.Join("/usr/lib/modules", release),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, modulesPath := range modulePaths {
|
|
||||||
info, err := os.Stat(modulesPath)
|
|
||||||
if err == nil && info.IsDir() {
|
|
||||||
return modulesPath, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("%w: %s are not valid existing directories"+
|
|
||||||
"; have you bind mounted the /lib/modules directory?",
|
|
||||||
ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getReleaseName() (release string, err error) {
|
|
||||||
var utsName unix.Utsname
|
|
||||||
err = unix.Uname(&utsName)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("getting unix uname release: %w", err)
|
|
||||||
}
|
|
||||||
release = unix.ByteSliceToString(utsName.Release[:])
|
|
||||||
release = strings.TrimSpace(release)
|
|
||||||
return release, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBuiltinModules(modulesDirPath string, modulesInfo map[string]moduleInfo) error {
|
func getBuiltinModules(modulesDirPath string, modulesInfo map[string]moduleInfo) error {
|
||||||
file, err := os.Open(filepath.Join(modulesDirPath, "modules.builtin"))
|
file, err := os.Open(filepath.Join(modulesDirPath, "modules.builtin"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ func initModule(path string) (err error) {
|
|||||||
const flags = 0
|
const flags = 0
|
||||||
err = unix.FinitModule(int(file.Fd()), moduleParams, flags)
|
err = unix.FinitModule(int(file.Fd()), moduleParams, flags)
|
||||||
switch {
|
switch {
|
||||||
case err == nil, err == unix.EEXIST: //nolint:err113
|
case err == nil, err == unix.EEXIST: //nolint:goerr113
|
||||||
return nil
|
return nil
|
||||||
case err != unix.ENOSYS: //nolint:err113
|
case err != unix.ENOSYS: //nolint:goerr113
|
||||||
if strings.HasSuffix(err.Error(), "operation not permitted") {
|
if strings.HasSuffix(err.Error(), "operation not permitted") {
|
||||||
err = fmt.Errorf("%w; did you set the SYS_MODULE capability to your container?", err)
|
err = fmt.Errorf("%w; did you set the SYS_MODULE capability to your container?", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-39
@@ -1,49 +1,12 @@
|
|||||||
package mod
|
package mod
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Probe is a expanded version of modprobe, in which it checks if the Kernel
|
// Probe loads the given kernel module and its dependencies.
|
||||||
// built-in features contain the given module name.
|
|
||||||
// It first tries to locate the modules directory in [getModulesPath].
|
|
||||||
// If it fails (like on WSL), it then only checks for the kernel feature
|
|
||||||
// in /proc/config.gz with [checkProcConfig].
|
|
||||||
// Otherwise, it first checks if the modules directory modules.builtin
|
|
||||||
// file contains the given module name in [checkModulesBuiltin].
|
|
||||||
// If the module is not found, it then runs the classic [modProbe] behavior,
|
|
||||||
// trying to load the module in the kernel.
|
|
||||||
// If this fails, it does one final try running [checkProcConfig].
|
|
||||||
func Probe(moduleName string) error {
|
func Probe(moduleName string) error {
|
||||||
modulesPath, err := getModulesPath()
|
modulesInfo, err := getModulesInfo()
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrModulesDirectoryNotFound) {
|
|
||||||
err = checkProcConfig(moduleName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("checking /proc/config.gz: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("getting modules path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkModulesBuiltin(modulesPath, moduleName)
|
|
||||||
if err != nil {
|
|
||||||
err = modProbe(modulesPath, moduleName)
|
|
||||||
if err != nil {
|
|
||||||
err = checkProcConfig(moduleName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("checking /proc/config.gz: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// modProbe is the classic modprobe behavior.
|
|
||||||
func modProbe(modulesPath, moduleName string) error {
|
|
||||||
modulesInfo, err := getModulesInfo(modulesPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting modules information: %w", err)
|
return fmt.Errorf("getting modules information: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ func getMarkdownHeaders(vpnProvider string) (headers []string, err error) {
|
|||||||
return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader}, nil
|
return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader}, nil
|
||||||
case providers.Vyprvpn:
|
case providers.Vyprvpn:
|
||||||
return []string{regionHeader, hostnameHeader, tcpHeader, udpHeader}, nil
|
return []string{regionHeader, hostnameHeader, tcpHeader, udpHeader}, nil
|
||||||
|
case providers.Wevpn:
|
||||||
|
return []string{cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil
|
||||||
case providers.Windscribe:
|
case providers.Windscribe:
|
||||||
return []string{regionHeader, cityHeader, hostnameHeader, vpnHeader}, nil
|
return []string{regionHeader, cityHeader, hostnameHeader, vpnHeader}, nil
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PublicIP struct {
|
type PublicIP struct {
|
||||||
IP netip.Addr `json:"public_ip"`
|
IP netip.Addr `json:"public_ip,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
Region string `json:"region,omitempty"`
|
||||||
Country string `json:"country,omitempty"`
|
Country string `json:"country,omitempty"`
|
||||||
City string `json:"city,omitempty"`
|
City string `json:"city,omitempty"`
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const (
|
|||||||
func FamilyToString(family int) string {
|
func FamilyToString(family int) string {
|
||||||
switch family {
|
switch family {
|
||||||
case FamilyAll:
|
case FamilyAll:
|
||||||
return "all"
|
return "all" //nolint:goconst
|
||||||
case FamilyV4:
|
case FamilyV4:
|
||||||
return "v4"
|
return "v4"
|
||||||
case FamilyV6:
|
case FamilyV6:
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
package netlink
|
package netlink
|
||||||
|
|
||||||
import (
|
import "net/netip"
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/qdm12/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeNetipPrefix(n byte) netip.Prefix {
|
func makeNetipPrefix(n byte) netip.Prefix {
|
||||||
const bits = 24
|
const bits = 24
|
||||||
return netip.PrefixFrom(netip.AddrFrom4([4]byte{n, n, n, 0}), bits)
|
return netip.PrefixFrom(netip.AddrFrom4([4]byte{n, n, n, 0}), bits)
|
||||||
}
|
}
|
||||||
|
|
||||||
type noopLogger struct{}
|
|
||||||
|
|
||||||
func (l *noopLogger) Debug(_ string) {}
|
|
||||||
func (l *noopLogger) Debugf(_ string, _ ...any) {}
|
|
||||||
func (l *noopLogger) Patch(_ ...log.Option) {}
|
|
||||||
|
|||||||
@@ -3,35 +3,52 @@
|
|||||||
package netlink
|
package netlink
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/mod"
|
"github.com/qdm12/gluetun/internal/mod"
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (n *NetLink) IsWireguardSupported() bool {
|
func (n *NetLink) IsWireguardSupported() (ok bool, err error) {
|
||||||
// Check for Wireguard family without loading the wireguard module.
|
// Check for Wireguard family without loading the wireguard module.
|
||||||
// Some kernels have the wireguard module built-in, and don't have a
|
// Some kernels have the wireguard module built-in, and don't have a
|
||||||
// modules directory, such as WSL2 kernels.
|
// modules directory, such as WSL2 kernels.
|
||||||
ok := hasWireguardFamily()
|
ok, err = hasWireguardFamily()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("checking for wireguard family: %w", err)
|
||||||
|
}
|
||||||
if ok {
|
if ok {
|
||||||
return true
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try loading the wireguard module, since some systems do not load
|
// Try loading the wireguard module, since some systems do not load
|
||||||
// it after a boot. If this fails, wireguard is assumed to not be supported.
|
// it after a boot. If this fails, wireguard is assumed to not be supported.
|
||||||
n.debugLogger.Debugf("wireguard family not found, trying to load wireguard kernel module")
|
n.debugLogger.Debugf("wireguard family not found, trying to load wireguard kernel module")
|
||||||
err := mod.Probe("wireguard")
|
err = mod.Probe("wireguard")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.debugLogger.Debugf("failed loading wireguard kernel module: %s", err)
|
n.debugLogger.Debugf("failed loading wireguard kernel module: %s", err)
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
n.debugLogger.Debugf("wireguard kernel module loaded successfully")
|
n.debugLogger.Debugf("wireguard kernel module loaded successfully")
|
||||||
|
|
||||||
// Re-check if the Wireguard family is now available, after loading
|
// Re-check if the Wireguard family is now available, after loading
|
||||||
// the wireguard kernel module.
|
// the wireguard kernel module.
|
||||||
return hasWireguardFamily()
|
ok, err = hasWireguardFamily()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("checking for wireguard family: %w", err)
|
||||||
|
}
|
||||||
|
return ok, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasWireguardFamily() bool {
|
func hasWireguardFamily() (ok bool, err error) {
|
||||||
_, err := netlink.GenlFamilyGet("wireguard")
|
families, err := netlink.GenlFamilyList()
|
||||||
return err == nil
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("listing gen 1 families: %w", err)
|
||||||
|
}
|
||||||
|
for _, family := range families {
|
||||||
|
if family.Name == "wireguard" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ package netlink
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_NetLink_IsWireguardSupported(t *testing.T) {
|
func Test_NetLink_IsWireguardSupported(t *testing.T) {
|
||||||
t.Parallel()
|
t.Skip() // TODO unskip once the data race problem with netlink.GenlFamilyList() is fixed
|
||||||
|
|
||||||
netLink := &NetLink{
|
t.Parallel()
|
||||||
debugLogger: &noopLogger{},
|
netLink := &NetLink{}
|
||||||
}
|
ok, err := netLink.IsWireguardSupported()
|
||||||
ok := netLink.IsWireguardSupported()
|
require.NoError(t, err)
|
||||||
if ok { // cannot assert since this depends on kernel
|
if ok { // cannot assert since this depends on kernel
|
||||||
t.Log("wireguard is supported")
|
t.Log("wireguard is supported")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ func extractDataFromLines(lines []string) (
|
|||||||
func extractDataFromLine(line string) (
|
func extractDataFromLine(line string) (
|
||||||
ip netip.Addr, port uint16, protocol string, err error,
|
ip netip.Addr, port uint16, protocol string, err error,
|
||||||
) {
|
) {
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, "proto "):
|
case strings.HasPrefix(line, "proto "):
|
||||||
protocol, err = extractProto(line)
|
protocol, err = extractProto(line)
|
||||||
@@ -83,7 +81,10 @@ func extractDataFromLine(line string) (
|
|||||||
return ip, 0, "", nil
|
return ip, 0, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var errProtoLineFieldsCount = errors.New("proto line has not 2 fields as expected")
|
var (
|
||||||
|
errProtoLineFieldsCount = errors.New("proto line has not 2 fields as expected")
|
||||||
|
errProtocolNotSupported = errors.New("network protocol not supported")
|
||||||
|
)
|
||||||
|
|
||||||
func extractProto(line string) (protocol string, err error) {
|
func extractProto(line string) (protocol string, err error) {
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
@@ -91,25 +92,13 @@ func extractProto(line string) (protocol string, err error) {
|
|||||||
return "", fmt.Errorf("%w: %s", errProtoLineFieldsCount, line)
|
return "", fmt.Errorf("%w: %s", errProtoLineFieldsCount, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseProto(fields[1])
|
switch fields[1] {
|
||||||
}
|
case "tcp", "tcp4", "tcp6", "tcp-client", "udp", "udp4", "udp6":
|
||||||
|
|
||||||
var errProtocolNotSupported = errors.New("network protocol not supported")
|
|
||||||
|
|
||||||
func parseProto(field string) (protocol string, err error) {
|
|
||||||
switch field {
|
|
||||||
case "tcp", "tcp4", "tcp6", "tcp-client":
|
|
||||||
// tcp4, tcp6 can be assimilated as tcp since the IP version is
|
|
||||||
// determined by the remote IP address version.
|
|
||||||
// tcp-client is a synonym of tcp for OpenVPN 2.5+ acting in client mode.
|
|
||||||
return constants.TCP, nil
|
|
||||||
case "udp", "udp4", "udp6":
|
|
||||||
// udp4, udp6 can be assimilated as udp since the IP version is
|
|
||||||
// determined by the remote IP address version.
|
|
||||||
return constants.UDP, nil
|
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("%w: %s", errProtocolNotSupported, field)
|
return "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fields[1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -147,9 +136,11 @@ func extractRemote(line string) (ip netip.Addr, port uint16,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if n > 3 { //nolint:mnd
|
if n > 3 { //nolint:mnd
|
||||||
protocol, err = parseProto(fields[3])
|
switch fields[3] {
|
||||||
if err != nil {
|
case "tcp", "udp":
|
||||||
return netip.Addr{}, 0, "", fmt.Errorf("parsing protocol from remote line: %w", err)
|
protocol = fields[3]
|
||||||
|
default:
|
||||||
|
return netip.Addr{}, 0, "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[3])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,14 +62,6 @@ func Test_extractDataFromLines(t *testing.T) {
|
|||||||
Protocol: constants.UDP,
|
Protocol: constants.UDP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"leading_whitespace": {
|
|
||||||
lines: []string{" proto tcp", "\tremote 1.2.3.4 443 tcp"},
|
|
||||||
connection: models.Connection{
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
|
|
||||||
Port: 443,
|
|
||||||
Protocol: constants.TCP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
for name, testCase := range testCases {
|
||||||
@@ -113,7 +105,7 @@ func Test_extractDataFromLine(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"tcp-client": {
|
"tcp-client": {
|
||||||
line: "proto tcp-client",
|
line: "proto tcp-client",
|
||||||
protocol: constants.TCP,
|
protocol: "tcp-client",
|
||||||
},
|
},
|
||||||
"extract remote error": {
|
"extract remote error": {
|
||||||
line: "remote bad",
|
line: "remote bad",
|
||||||
@@ -247,7 +239,7 @@ func Test_extractRemote(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"invalid protocol": {
|
"invalid protocol": {
|
||||||
line: "remote 1.2.3.4 8000 bad",
|
line: "remote 1.2.3.4 8000 bad",
|
||||||
err: errors.New("parsing protocol from remote line: network protocol not supported: bad"),
|
err: errors.New("network protocol not supported: bad"),
|
||||||
},
|
},
|
||||||
"IP host and port and protocol": {
|
"IP host and port and protocol": {
|
||||||
line: "remote 1.2.3.4 8000 udp",
|
line: "remote 1.2.3.4 8000 udp",
|
||||||
|
|||||||
@@ -29,16 +29,19 @@ func (r *Runner) Run(ctx context.Context, errCh chan<- error, ready chan<- struc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||||
streamDone := make(chan struct{})
|
streamDone := make(chan struct{})
|
||||||
go streamLines(streamDone, r.logger,
|
go streamLines(streamCtx, streamDone, r.logger,
|
||||||
stdoutLines, stderrLines, ready)
|
stdoutLines, stderrLines, ready)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
<-waitError
|
<-waitError
|
||||||
|
streamCancel()
|
||||||
<-streamDone
|
<-streamDone
|
||||||
errCh <- ctx.Err()
|
errCh <- ctx.Err()
|
||||||
case err := <-waitError:
|
case err := <-waitError:
|
||||||
|
streamCancel()
|
||||||
<-streamDone
|
<-streamDone
|
||||||
errCh <- err
|
errCh <- err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,26 @@
|
|||||||
package openvpn
|
package openvpn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func streamLines(done chan<- struct{},
|
func streamLines(ctx context.Context, done chan<- struct{},
|
||||||
logger Logger, stdout, stderr <-chan string,
|
logger Logger, stdout, stderr <-chan string,
|
||||||
tunnelReady chan<- struct{},
|
tunnelReady chan<- struct{},
|
||||||
) {
|
) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
|
var line string
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var line string
|
|
||||||
var ok bool
|
|
||||||
errLine := false
|
errLine := false
|
||||||
select {
|
select {
|
||||||
case line, ok = <-stdout:
|
case <-ctx.Done():
|
||||||
if ok {
|
return
|
||||||
break
|
case line = <-stdout:
|
||||||
}
|
case line = <-stderr:
|
||||||
if stderr == nil {
|
errLine = true
|
||||||
return
|
|
||||||
}
|
|
||||||
stdout = nil
|
|
||||||
case line, ok = <-stderr:
|
|
||||||
if ok {
|
|
||||||
errLine = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if stdout == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stderr = nil
|
|
||||||
}
|
}
|
||||||
line, level := processLogLine(line)
|
line, level := processLogLine(line)
|
||||||
if line == "" {
|
if line == "" {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ type Logger interface {
|
|||||||
Info(s string)
|
Info(s string)
|
||||||
Warn(s string)
|
Warn(s string)
|
||||||
Error(s string)
|
Error(s string)
|
||||||
Errorf(format string, args ...any)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cmder interface {
|
type Cmder interface {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/portforward/service"
|
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||||
@@ -86,8 +85,6 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
defer close(runDone)
|
defer close(runDone)
|
||||||
|
|
||||||
var serviceRunError <-chan error
|
var serviceRunError <-chan error
|
||||||
var retryAfter <-chan time.Time
|
|
||||||
const retryDelay = 5 * time.Second
|
|
||||||
for {
|
for {
|
||||||
updateReceived := false
|
updateReceived := false
|
||||||
select {
|
select {
|
||||||
@@ -106,12 +103,10 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
l.settingsMutex.Unlock()
|
l.settingsMutex.Unlock()
|
||||||
case err := <-serviceRunError:
|
case err := <-serviceRunError:
|
||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
case <-retryAfter:
|
|
||||||
// Retry starting the service after a delay
|
|
||||||
retryAfter = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.service != nil {
|
firstRun := serviceRunError == nil
|
||||||
|
if !firstRun {
|
||||||
err := l.service.Stop()
|
err := l.service.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
||||||
@@ -135,10 +130,6 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
err = fmt.Errorf("starting port forwarding service: %w", err)
|
err = fmt.Errorf("starting port forwarding service: %w", err)
|
||||||
}
|
}
|
||||||
updateResult <- err
|
updateResult <- err
|
||||||
} else if err != nil {
|
|
||||||
// Log the error and schedule a retry
|
|
||||||
l.logger.Errorf("starting port forwarding service: %s - retrying in %s", err, retryDelay)
|
|
||||||
retryAfter = time.After(retryDelay)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user