Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7450424849 | ||
|
|
16bc27bf9f | ||
|
|
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",
|
||||||
|
|||||||
37
.github/ISSUE_TEMPLATE/provider.md
vendored
37
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -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.
|
||||||
|
|||||||
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -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/)
|
|
||||||
48
.github/workflows/ci.yml
vendored
48
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: "1"
|
DOCKER_BUILDKIT: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- 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 == 'qdm12/gluetun' &&
|
|
||||||
(
|
|
||||||
github.event_name == 'push' ||
|
|
||||||
github.event_name == 'release' ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
|
||||||
)
|
|
||||||
needs: [verify]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: secrets
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- run: docker build -t qmcgaw/gluetun .
|
|
||||||
|
|
||||||
- name: Setup Go for CI utility
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: ci/go.mod
|
|
||||||
|
|
||||||
- name: Build utility
|
|
||||||
run: go build -C ./ci -o runner ./cmd/main.go
|
|
||||||
|
|
||||||
- name: Run Gluetun container with Mullvad configuration
|
|
||||||
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{ secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
|
|
||||||
|
|
||||||
- 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,15 +73,15 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- 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: |
|
||||||
@@ -121,14 +91,14 @@ jobs:
|
|||||||
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
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- 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
|
||||||
|
|||||||
2
.github/workflows/closed-issue.yml
vendored
2
.github/workflows/closed-issue.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
3
.github/workflows/configs/mlc-config.json
vendored
3
.github/workflows/configs/mlc-config.json
vendored
@@ -8,7 +8,6 @@
|
|||||||
"retryOn429": false,
|
"retryOn429": false,
|
||||||
"fallbackRetryDelay": "30s",
|
"fallbackRetryDelay": "30s",
|
||||||
"aliveStatusCodes": [
|
"aliveStatusCodes": [
|
||||||
200,
|
200
|
||||||
429
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
2
.github/workflows/labels.yml
vendored
2
.github/workflows/labels.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- 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
|
||||||
|
|||||||
6
.github/workflows/markdown.yml
vendored
6
.github/workflows/markdown.yml
vendored
@@ -18,12 +18,12 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- 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:
|
||||||
|
|||||||
2
.github/workflows/opened-issue.yml
vendored
2
.github/workflows/opened-issue.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
@@ -1,73 +1,30 @@
|
|||||||
version: "2"
|
linters-settings:
|
||||||
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gci
|
|
||||||
- gofumpt
|
|
||||||
- goimports
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
|
|
||||||
linters:
|
|
||||||
settings:
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
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:
|
issues:
|
||||||
generated: lax
|
exclude-rules:
|
||||||
presets:
|
- path: _test\.go
|
||||||
- comments
|
linters:
|
||||||
- common-false-positives
|
- dupl
|
||||||
- legacy
|
- err113
|
||||||
- std-error-handling
|
|
||||||
rules:
|
|
||||||
- linters:
|
|
||||||
- containedctx
|
- containedctx
|
||||||
- dupl
|
|
||||||
- err113
|
|
||||||
- maintidx
|
- maintidx
|
||||||
path: _test\.go
|
- path: "internal\\/server\\/.+\\.go"
|
||||||
- linters:
|
linters:
|
||||||
- dupl
|
- dupl
|
||||||
path: internal\/server\/.+\.go
|
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
|
||||||
- linters:
|
linters:
|
||||||
- ireturn
|
- ireturn
|
||||||
text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
|
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
|
||||||
- linters:
|
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
||||||
|
linters:
|
||||||
- ireturn
|
- ireturn
|
||||||
path: internal\/openvpn\/pkcs8\/descbc\.go
|
- source: "^\\/\\/ https\\:\\/\\/.+$"
|
||||||
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
|
linters:
|
||||||
- linters:
|
|
||||||
- revive
|
|
||||||
path: internal\/provider\/(common|utils)\/.+\.go
|
|
||||||
text: "var-naming: avoid (bad|meaningless) package names"
|
|
||||||
- linters:
|
|
||||||
- lll
|
- lll
|
||||||
source: "^// https://.+$"
|
|
||||||
- linters:
|
|
||||||
- err113
|
|
||||||
- mnd
|
|
||||||
path: ci\/.+\.go
|
|
||||||
|
|
||||||
paths:
|
linters:
|
||||||
- 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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
3
.markdownlint.json
Normal file
3
.markdownlint.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"MD013": false
|
||||||
|
}
|
||||||
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Update a VPN provider servers data",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"program": "cmd/gluetun/main.go",
|
||||||
|
"args": [
|
||||||
|
"update",
|
||||||
|
"${input:updateMode}",
|
||||||
|
"-providers",
|
||||||
|
"${input:provider}"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "provider",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Please enter a provider (or comma separated list of providers)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "updateMode",
|
||||||
|
"type": "pickString",
|
||||||
|
"description": "Update mode to use",
|
||||||
|
"options": [
|
||||||
|
"-maintainer",
|
||||||
|
"-enduser"
|
||||||
|
],
|
||||||
|
"default": "-maintainer"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
51
.vscode/tasks.json
vendored
51
.vscode/tasks.json
vendored
@@ -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",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
48
Dockerfile
48
Dockerfile
@@ -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,7 +56,7 @@ 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**, **WeVPN**, **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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/ci/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
fmt.Println("Usage: " + os.Args[0] + " <command>")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "mullvad":
|
|
||||||
err = internal.MullvadTest(ctx)
|
|
||||||
case "protonvpn":
|
|
||||||
err = internal.ProtonVPNTest(ctx)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unknown command: %s", os.Args[1])
|
|
||||||
}
|
|
||||||
stop()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("❌", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("✅ Test completed successfully.")
|
|
||||||
}
|
|
||||||
36
ci/go.mod
36
ci/go.mod
@@ -1,36 +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/felixge/httpsnoop v1.0.4 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
|
||||||
github.com/moby/term v0.5.2 // indirect
|
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
|
||||||
golang.org/x/time v0.14.0 // indirect
|
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
|
||||||
)
|
|
||||||
97
ci/go.sum
97
ci/go.sum
@@ -1,97 +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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
|
||||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MullvadTest(ctx context.Context) error {
|
|
||||||
expectedSecrets := []string{
|
|
||||||
"Wireguard private key",
|
|
||||||
"Wireguard address",
|
|
||||||
}
|
|
||||||
secrets, err := readSecrets(ctx, expectedSecrets)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ProtonVPNTest(ctx context.Context) error {
|
|
||||||
expectedSecrets := []string{
|
|
||||||
"Wireguard private key",
|
|
||||||
}
|
|
||||||
secrets, err := readSecrets(ctx, expectedSecrets)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func readSecrets(ctx context.Context, expectedSecrets []string) (lines []string, err error) {
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
lines = make([]string, 0, len(expectedSecrets))
|
|
||||||
|
|
||||||
for i := range expectedSecrets {
|
|
||||||
fmt.Println("🤫 reading", expectedSecrets[i], "from Stdin...")
|
|
||||||
if !scanner.Scan() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
|
||||||
fmt.Println("🤫 "+expectedSecrets[i], "secret read successfully")
|
|
||||||
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) 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopContainer(client *client.Client, containerID string) {
|
|
||||||
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
|
|
||||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
|
||||||
defer stopCancel()
|
|
||||||
|
|
||||||
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("failed to stop container:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
|
|
||||||
|
|
||||||
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
|
|
||||||
beforeStartTime time.Time,
|
|
||||||
) error {
|
|
||||||
logOptions := container.LogsOptions{
|
|
||||||
ShowStdout: true,
|
|
||||||
Follow: true,
|
|
||||||
Since: beforeStartTime.Format(time.RFC3339Nano),
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting container logs: %w", err)
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
var linesSeen []string
|
|
||||||
scanner := bufio.NewScanner(reader)
|
|
||||||
for ctx.Err() == nil {
|
|
||||||
if scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if len(line) > 8 { // remove Docker log prefix
|
|
||||||
line = line[8:]
|
|
||||||
}
|
|
||||||
linesSeen = append(linesSeen, line)
|
|
||||||
if successRegexp.MatchString(line) {
|
|
||||||
fmt.Println("✅ Success line logged")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := scanner.Err()
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
logSeenLines(linesSeen)
|
|
||||||
return fmt.Errorf("reading log stream: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The scanner is either done or cannot read because of EOF
|
|
||||||
fmt.Println("The log scanner stopped")
|
|
||||||
logSeenLines(linesSeen)
|
|
||||||
|
|
||||||
// Check if the container is still running
|
|
||||||
inspect, err := client.ContainerInspect(ctx, containerID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("inspecting container: %w", err)
|
|
||||||
}
|
|
||||||
if !inspect.State.Running {
|
|
||||||
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func logSeenLines(lines []string) {
|
|
||||||
fmt.Println("Logs seen so far:")
|
|
||||||
for _, line := range lines {
|
|
||||||
fmt.Println(" " + line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -164,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
|
||||||
@@ -416,13 +414,6 @@ 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)
|
|
||||||
|
|
||||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||||
|
|
||||||
unzipper := unzip.New(httpClient)
|
unzipper := unzip.New(httpClient)
|
||||||
@@ -434,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))
|
||||||
@@ -469,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 {
|
||||||
@@ -483,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),
|
||||||
@@ -602,34 +602,3 @@ type RunStarter interface {
|
|||||||
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 = ` @@@
|
|
||||||
@@@@
|
|
||||||
@@@@@@
|
|
||||||
@@@@.@@ @@@@@@@@@@
|
|
||||||
@@@@.@@@ @@@@@@@@==@@@@
|
|
||||||
@@@.@..@@ @@@@@@@=@..==@@@@
|
|
||||||
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
|
|
||||||
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
|
|
||||||
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
|
|
||||||
@@@.--@..@@@@.-@@@@@@@==============@@@@
|
|
||||||
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
|
|
||||||
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
|
|
||||||
@@@@.@--@@@@@@@@@@================@@@@@@@
|
|
||||||
@@@..--@@*@@@@@@================@@@@+*@@
|
|
||||||
@@@.---@@.@@@@=================@@@@--@@
|
|
||||||
@@@-.---@@@@@@================@@@@*--@@@
|
|
||||||
@@@.:-#@@@@@@===============*@@@@.---@@
|
|
||||||
@@@.-------.@@@============@@@@@@.--@@@
|
|
||||||
@@@..--------:@@@=========@@@@@@@@.--@@@
|
|
||||||
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
|
|
||||||
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
|
|
||||||
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
|
|
||||||
@@@@@@=============@@@@@ @@@...------------.*@@@
|
|
||||||
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
|
|
||||||
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
|
|
||||||
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
|
|
||||||
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
|
|
||||||
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
|
|
||||||
@@@@@@@@ @@@@ @@@@
|
|
||||||
`
|
|
||||||
|
|||||||
37
go.mod
37
go.mod
@@ -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.20251123213823-54e987293e88
|
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.44.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
|
||||||
|
|||||||
72
go.sum
72
go.sum
@@ -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.20251123213823-54e987293e88 h1:GJ5FALvJ3UmHjVaNYebrfV5zF5You4dq8HfRWZy2loM=
|
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88/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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
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=
|
||||||
|
|||||||
@@ -88,11 +88,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ func readObsolete(r *reader.Reader) (warnings []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 {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("validating DoT settings: %w", err)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Blacklist.validate()
|
|
||||||
if err != nil {
|
|
||||||
return 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() {
|
||||||
@@ -38,7 +35,6 @@ var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,6
|
|||||||
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,12 +50,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +62,6 @@ func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
170
internal/configuration/settings/dot.go
Normal file
170
internal/configuration/settings/dot.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DoT contains settings to configure the DoT server.
|
||||||
|
type DoT struct {
|
||||||
|
// Enabled is true if the DoT server should be running
|
||||||
|
// and used. It defaults to true, and cannot be nil
|
||||||
|
// in the internal state.
|
||||||
|
Enabled *bool
|
||||||
|
// UpdatePeriod is the period to update DNS block lists.
|
||||||
|
// It can be set to 0 to disable the update.
|
||||||
|
// It defaults to 24h and cannot be nil in
|
||||||
|
// the internal state.
|
||||||
|
UpdatePeriod *time.Duration
|
||||||
|
// Providers is a list of DNS over TLS providers
|
||||||
|
Providers []string `json:"providers"`
|
||||||
|
// Caching is true if the DoT server should cache
|
||||||
|
// DNS responses.
|
||||||
|
Caching *bool `json:"caching"`
|
||||||
|
// IPv6 is true if the DoT server should connect over IPv6.
|
||||||
|
IPv6 *bool `json:"ipv6"`
|
||||||
|
// Blacklist contains settings to configure the filter
|
||||||
|
// block lists.
|
||||||
|
Blacklist DNSBlacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
|
||||||
|
|
||||||
|
func (d DoT) validate() (err error) {
|
||||||
|
const minUpdatePeriod = 30 * time.Second
|
||||||
|
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||||
|
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||||
|
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
for _, providerName := range d.Providers {
|
||||||
|
_, err := providers.Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DoT) copy() (copied DoT) {
|
||||||
|
return DoT{
|
||||||
|
Enabled: gosettings.CopyPointer(d.Enabled),
|
||||||
|
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||||
|
Providers: gosettings.CopySlice(d.Providers),
|
||||||
|
Caching: gosettings.CopyPointer(d.Caching),
|
||||||
|
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||||
|
Blacklist: d.Blacklist.copy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWith overrides fields of the receiver
|
||||||
|
// settings object with any field set in the other
|
||||||
|
// settings.
|
||||||
|
func (d *DoT) overrideWith(other DoT) {
|
||||||
|
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
||||||
|
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||||
|
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||||
|
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||||
|
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||||
|
d.Blacklist.overrideWith(other.Blacklist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DoT) setDefaults() {
|
||||||
|
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
||||||
|
const defaultUpdatePeriod = 24 * time.Hour
|
||||||
|
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||||
|
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||||
|
provider.Cloudflare().Name,
|
||||||
|
})
|
||||||
|
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||||
|
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||||
|
d.Blacklist.setDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
provider, err := providers.Get(d.Providers[0])
|
||||||
|
if err != nil {
|
||||||
|
// Settings should be validated before calling this function,
|
||||||
|
// so an error happening here is a programming error.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.DoT.IPv4[0].Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DoT) String() string {
|
||||||
|
return d.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DoT) toLinesNode() (node *gotree.Node) {
|
||||||
|
node = gotree.New("DNS over TLS settings:")
|
||||||
|
|
||||||
|
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
|
||||||
|
if !*d.Enabled {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
update := "disabled" //nolint:goconst
|
||||||
|
if *d.UpdatePeriod > 0 {
|
||||||
|
update = "every " + d.UpdatePeriod.String()
|
||||||
|
}
|
||||||
|
node.Appendf("Update period: %s", update)
|
||||||
|
|
||||||
|
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||||
|
for _, provider := range d.Providers {
|
||||||
|
upstreamResolvers.Append(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||||
|
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||||
|
|
||||||
|
node.AppendNode(d.Blacklist.toLinesNode())
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DoT) read(reader *reader.Reader) (err error) {
|
||||||
|
d.Enabled, err = reader.BoolPtr("DOT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Providers = reader.CSV("DOT_PROVIDERS")
|
||||||
|
|
||||||
|
d.Caching, err = reader.BoolPtr("DOT_CACHING")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.read(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -71,10 +53,11 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
76
internal/configuration/settings/healthywait.go
Normal file
76
internal/configuration/settings/healthywait.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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,21 +44,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +52,6 @@ func (c *ControlServer) copy() (copied 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")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ 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.
|
||||||
@@ -344,8 +344,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()
|
||||||
|
|||||||
@@ -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,13 +40,13 @@ 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
|
||||||
|
| ├── Update period: every 24h0m0s
|
||||||
| ├── Upstream resolvers:
|
| ├── Upstream resolvers:
|
||||||
| | └── Cloudflare
|
| | └── Cloudflare
|
||||||
| ├── Caching: yes
|
| ├── Caching: yes
|
||||||
| ├── IPv6: no
|
| ├── IPv6: no
|
||||||
| ├── Update period: every 24h0m0s
|
|
||||||
| └── DNS filtering settings:
|
| └── DNS filtering settings:
|
||||||
| ├── Block malicious: yes
|
| ├── Block malicious: yes
|
||||||
| ├── Block ads: no
|
| ├── Block ads: no
|
||||||
@@ -57,14 +57,13 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
| └── 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) {
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -21,7 +20,6 @@ type Loop struct {
|
|||||||
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
|
||||||
@@ -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.Info(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)
|
||||||
|
|||||||
@@ -4,20 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"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 " +
|
||||||
@@ -34,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 {
|
||||||
@@ -64,7 +56,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
@@ -113,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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
case "dot":
|
|
||||||
dialerSettings := dot.Settings{
|
|
||||||
UpstreamResolvers: upstreamResolvers,
|
|
||||||
IPVersion: ipVersion,
|
|
||||||
}
|
|
||||||
dialer, err = dot.New(dialerSettings)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
||||||
}
|
}
|
||||||
case "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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"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/nameserver"
|
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||||
@@ -21,14 +20,14 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
|||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
|
|
||||||
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
|
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("building server settings: %w", err)
|
return nil, fmt.Errorf("building DoT settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := server.New(serverSettings)
|
server, err := server.New(dotSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating server: %w", err)
|
return nil, fmt.Errorf("creating DoT server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
runError, err = server.Start(ctx)
|
runError, err = server.Start(ctx)
|
||||||
@@ -38,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
|
||||||
}
|
}
|
||||||
@@ -43,14 +43,14 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
|||||||
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
timer.Reset(*settings.UpdatePeriod)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
|||||||
settings := l.GetSettings()
|
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 {
|
||||||
@@ -37,7 +37,6 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
|||||||
IPPrefixes: result.BlockedIPPrefixes,
|
IPPrefixes: result.BlockedIPPrefixes,
|
||||||
}
|
}
|
||||||
updateSettings.BlockHostnames(result.BlockedHostnames)
|
updateSettings.BlockHostnames(result.BlockedHostnames)
|
||||||
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
|
|
||||||
err = l.filter.Update(updateSettings)
|
err = l.filter.Update(updateSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("updating filter: %w", err)
|
return fmt.Errorf("updating filter: %w", err)
|
||||||
|
|||||||
@@ -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,7 +9,7 @@ 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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
|
|||||||
) error {
|
) error {
|
||||||
protocol := connection.Protocol
|
protocol := connection.Protocol
|
||||||
if protocol == "tcp-client" {
|
if protocol == "tcp-client" {
|
||||||
protocol = "tcp"
|
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,
|
||||||
|
|||||||
@@ -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,326 +0,0 @@
|
|||||||
package healthcheck
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/healthcheck/dns"
|
|
||||||
"github.com/qdm12/gluetun/internal/healthcheck/icmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Checker struct {
|
|
||||||
tlsDialAddrs []string
|
|
||||||
dialer *net.Dialer
|
|
||||||
echoer *icmp.Echoer
|
|
||||||
dnsClient *dns.Client
|
|
||||||
logger Logger
|
|
||||||
icmpTargetIPs []netip.Addr
|
|
||||||
smallCheckType string
|
|
||||||
configMutex sync.Mutex
|
|
||||||
|
|
||||||
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 TCP+TLS dial addresses, the ICMP echo IP address
|
|
||||||
// to target and the desired small check type (dns or icmp).
|
|
||||||
// This function MUST be called before calling [Checker.Start].
|
|
||||||
func (c *Checker) SetConfig(tlsDialAddrs []string, icmpTargets []netip.Addr,
|
|
||||||
smallCheckType string,
|
|
||||||
) {
|
|
||||||
c.configMutex.Lock()
|
|
||||||
defer c.configMutex.Unlock()
|
|
||||||
c.tlsDialAddrs = tlsDialAddrs
|
|
||||||
c.icmpTargetIPs = icmpTargets
|
|
||||||
c.smallCheckType = smallCheckType
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the checker by first running a blocking 6s-timed TCP+TLS check,
|
|
||||||
// and, on success, starts the periodic checks in a separate goroutine:
|
|
||||||
// - a "small" ICMP echo check every minute
|
|
||||||
// - a "full" TCP+TLS check every 5 minutes
|
|
||||||
// It returns a channel `runError` that receives an error (nil or not) when a periodic check is performed.
|
|
||||||
// It returns an error if the initial TCP+TLS check fails.
|
|
||||||
// The Checker has to be ultimately stopped by calling [Checker.Stop].
|
|
||||||
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
|
|
||||||
if len(c.tlsDialAddrs) == 0 || len(c.icmpTargetIPs) == 0 || c.smallCheckType == "" {
|
|
||||||
panic("call Checker.SetConfig with non empty values before Checker.Start")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.icmpNotPermitted {
|
|
||||||
// restore forced check type to dns if icmp was found to be not permitted
|
|
||||||
c.smallCheckType = smallCheckDNS
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.startupCheck(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("startup check: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ready := make(chan struct{})
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
c.stop = cancel
|
|
||||||
done := make(chan struct{})
|
|
||||||
c.done = done
|
|
||||||
const smallCheckPeriod = time.Minute
|
|
||||||
smallCheckTimer := time.NewTimer(smallCheckPeriod)
|
|
||||||
const fullCheckPeriod = 5 * time.Minute
|
|
||||||
fullCheckTimer := time.NewTimer(fullCheckPeriod)
|
|
||||||
runErrorCh := make(chan error)
|
|
||||||
runError = runErrorCh
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
close(ready)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
fullCheckTimer.Stop()
|
|
||||||
smallCheckTimer.Stop()
|
|
||||||
return
|
|
||||||
case <-smallCheckTimer.C:
|
|
||||||
err := c.smallPeriodicCheck(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("small periodic check: %w", err)
|
|
||||||
}
|
|
||||||
runErrorCh <- err
|
|
||||||
smallCheckTimer.Reset(smallCheckPeriod)
|
|
||||||
case <-fullCheckTimer.C:
|
|
||||||
err := c.fullPeriodicCheck(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("full periodic check: %w", err)
|
|
||||||
}
|
|
||||||
runErrorCh <- err
|
|
||||||
fullCheckTimer.Reset(fullCheckPeriod)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
<-ready
|
|
||||||
return runError, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) Stop() error {
|
|
||||||
c.stop()
|
|
||||||
<-c.done
|
|
||||||
c.tlsDialAddrs = nil
|
|
||||||
c.icmpTargetIPs = nil
|
|
||||||
c.smallCheckType = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
|
|
||||||
c.configMutex.Lock()
|
|
||||||
icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs))
|
|
||||||
copy(icmpTargetIPs, c.icmpTargetIPs)
|
|
||||||
c.configMutex.Unlock()
|
|
||||||
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 errors.Is(err, icmp.ErrNotPermitted) {
|
|
||||||
c.icmpNotPermitted = true
|
|
||||||
c.smallCheckType = smallCheckDNS
|
|
||||||
c.logger.Infof("%s; permanently falling back to %s checks",
|
|
||||||
smallCheckTypeToString(c.smallCheckType), err)
|
|
||||||
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: %s", ErrAllCheckTriesFailed, strings.Join(errStrings, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
122
internal/healthcheck/health.go
Normal file
122
internal/healthcheck/health.go
Normal file
@@ -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,193 +0,0 @@
|
|||||||
package icmp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
cryptorand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand/v2"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/icmp"
|
|
||||||
"golang.org/x/net/ipv4"
|
|
||||||
"golang.org/x/net/ipv6"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrICMPBodyUnsupported = errors.New("ICMP body type is not supported")
|
|
||||||
ErrICMPEchoDataMismatch = errors.New("ICMP data mismatch")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Echoer struct {
|
|
||||||
buffer []byte
|
|
||||||
randomSource io.Reader
|
|
||||||
logger Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEchoer(logger Logger) *Echoer {
|
|
||||||
const maxICMPEchoSize = 1500
|
|
||||||
buffer := make([]byte, maxICMPEchoSize)
|
|
||||||
var seed [32]byte
|
|
||||||
_, _ = cryptorand.Read(seed[:])
|
|
||||||
randomSource := rand.NewChaCha8(seed)
|
|
||||||
return &Echoer{
|
|
||||||
buffer: buffer,
|
|
||||||
randomSource: randomSource,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTimedOut = errors.New("timed out waiting for ICMP echo reply")
|
|
||||||
ErrNotPermitted = errors.New("not permitted")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (i *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) {
|
|
||||||
var ipVersion string
|
|
||||||
var conn net.PacketConn
|
|
||||||
if ip.Is4() {
|
|
||||||
ipVersion = "v4"
|
|
||||||
conn, err = listenICMPv4(ctx)
|
|
||||||
} else {
|
|
||||||
ipVersion = "v6"
|
|
||||||
conn, err = listenICMPv6(ctx)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if strings.HasSuffix(err.Error(), "socket: operation not permitted") {
|
|
||||||
err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrNotPermitted)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("listening for ICMP packets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
const echoDataSize = 32
|
|
||||||
id, message := buildMessageToSend(ipVersion, echoDataSize, i.randomSource)
|
|
||||||
|
|
||||||
encodedMessage, err := message.Marshal(nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("encoding ICMP message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice()})
|
|
||||||
if err != nil {
|
|
||||||
if strings.HasSuffix(err.Error(), "sendto: operation not permitted") {
|
|
||||||
err = fmt.Errorf("%w", ErrNotPermitted)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("writing ICMP message to %s: %w", ip, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
receivedData, err := receiveEchoReply(conn, id, i.buffer, ipVersion, i.logger)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, net.ErrClosed) && ctx.Err() != nil {
|
|
||||||
return fmt.Errorf("%w 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, size uint, randomSource io.Reader) (id int, message *icmp.Message) {
|
|
||||||
const uint16Bytes = 2
|
|
||||||
idBytes := make([]byte, uint16Bytes)
|
|
||||||
_, _ = randomSource.Read(idBytes)
|
|
||||||
id = int(binary.BigEndian.Uint16(idBytes))
|
|
||||||
|
|
||||||
var icmpType icmp.Type
|
|
||||||
switch ipVersion {
|
|
||||||
case "v4":
|
|
||||||
icmpType = ipv4.ICMPTypeEcho
|
|
||||||
case "v6":
|
|
||||||
icmpType = ipv6.ICMPTypeEchoRequest
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("IP version %q not supported", ipVersion))
|
|
||||||
}
|
|
||||||
messageBodyData := make([]byte, size)
|
|
||||||
_, _ = randomSource.Read(messageBodyData)
|
|
||||||
|
|
||||||
// See https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types
|
|
||||||
message = &icmp.Message{
|
|
||||||
Type: icmpType, // echo request
|
|
||||||
Code: 0, // no code
|
|
||||||
Checksum: 0, // calculated at encoding (ipv4) or sending (ipv6)
|
|
||||||
Body: &icmp.Echo{
|
|
||||||
ID: id,
|
|
||||||
Seq: 0, // only one packet
|
|
||||||
Data: messageBodyData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return id, message
|
|
||||||
}
|
|
||||||
|
|
||||||
func receiveEchoReply(conn net.PacketConn, id int, buffer []byte, ipVersion string, logger Logger,
|
|
||||||
) (data []byte, err error) {
|
|
||||||
var icmpProtocol int
|
|
||||||
const (
|
|
||||||
icmpv4Protocol = 1
|
|
||||||
icmpv6Protocol = 58
|
|
||||||
)
|
|
||||||
switch ipVersion {
|
|
||||||
case "v4":
|
|
||||||
icmpProtocol = icmpv4Protocol
|
|
||||||
case "v6":
|
|
||||||
icmpProtocol = icmpv6Protocol
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown IP version: %s", ipVersion))
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Note we need to read the whole packet in one call to ReadFrom, so the buffer
|
|
||||||
// must be large enough to read the entire reply packet. See:
|
|
||||||
// https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J
|
|
||||||
bytesRead, returnAddr, err := conn.ReadFrom(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("reading from ICMP connection: %w", err)
|
|
||||||
}
|
|
||||||
packetBytes := buffer[:bytesRead]
|
|
||||||
|
|
||||||
// Parse the ICMP message
|
|
||||||
message, err := icmp.ParseMessage(icmpProtocol, packetBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch body := message.Body.(type) {
|
|
||||||
case *icmp.Echo:
|
|
||||||
if id != body.ID {
|
|
||||||
logger.Warnf("ignoring ICMP echo reply mismatching expected id %d "+
|
|
||||||
"(id: %d, type: %d, code: %d, length: %d, return address %s)",
|
|
||||||
id, body.ID, message.Type, message.Code, len(packetBytes), returnAddr)
|
|
||||||
continue // not the ID we are looking for
|
|
||||||
}
|
|
||||||
return body.Data, nil
|
|
||||||
case *icmp.DstUnreach:
|
|
||||||
logger.Debugf("ignoring ICMP destination unreachable message (type: 3, code: %d, return address %s, expected-id %d)",
|
|
||||||
message.Code, returnAddr, id)
|
|
||||||
// See https://github.com/qdm12/gluetun/pull/2923#issuecomment-3377532249
|
|
||||||
// on why we ignore this message. If it is actually unreachable, the timeout on waiting for
|
|
||||||
// the echo reply will do instead of returning an error error.
|
|
||||||
continue
|
|
||||||
case *icmp.TimeExceeded:
|
|
||||||
logger.Debugf("ignoring ICMP time exceeded message (type: 11, code: %d, return address %s, expected-id %d)",
|
|
||||||
message.Code, returnAddr, id)
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: %T (type %d, code %d, return address %s, expected-id %d)",
|
|
||||||
ErrICMPBodyUnsupported, body, message.Type, message.Code, returnAddr, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
7
internal/healthcheck/logger.go
Normal file
7
internal/healthcheck/logger.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package healthcheck
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Debug(s string)
|
||||||
|
Info(s string)
|
||||||
|
Error(s string)
|
||||||
|
}
|
||||||
25
internal/healthcheck/openvpn.go
Normal file
25
internal/healthcheck/openvpn.go
Normal file
@@ -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(),
|
||||||
|
dialer: &net.Dialer{
|
||||||
|
Resolver: &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
config: config,
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
||||||
commandTemplate string, ports []uint16, vpnInterface string,
|
commandTemplate string, ports []uint16,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
portStrings := make([]string, len(ports))
|
portStrings := make([]string, len(ports))
|
||||||
for i, port := range ports {
|
for i, port := range ports {
|
||||||
@@ -18,8 +18,6 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
|||||||
}
|
}
|
||||||
portsString := strings.Join(portStrings, ",")
|
portsString := strings.Join(portStrings, ",")
|
||||||
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
||||||
commandString = strings.ReplaceAll(commandString, "{{PORT}}", portStrings[0])
|
|
||||||
commandString = strings.ReplaceAll(commandString, "{{VPN_INTERFACE}}", vpnInterface)
|
|
||||||
args, err := command.Split(commandString)
|
args, err := command.Split(commandString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parsing command: %w", err)
|
return fmt.Errorf("parsing command: %w", err)
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ func Test_Service_runCommand(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
cmder := command.New()
|
cmder := command.New()
|
||||||
const commandTemplate = `/bin/sh -c "echo {{PORTS}}-{{PORT}}-{{VPN_INTERFACE}}"`
|
const commandTemplate = `/bin/sh -c "echo {{PORTS}}"`
|
||||||
ports := []uint16{1234, 5678}
|
ports := []uint16{1234, 5678}
|
||||||
const vpnInterface = "tun0"
|
|
||||||
logger := NewMockLogger(ctrl)
|
logger := NewMockLogger(ctrl)
|
||||||
logger.EXPECT().Info("1234,5678-1234-tun0")
|
logger.EXPECT().Info("1234,5678")
|
||||||
|
|
||||||
err := runCommand(ctx, cmder, logger, commandTemplate, ports, vpnInterface)
|
err := runCommand(ctx, cmder, logger, commandTemplate, ports)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ func (s *Service) writePortForwardedFile(ports []uint16) (err error) {
|
|||||||
fileData := []byte(strings.Join(portStrings, "\n"))
|
fileData := []byte(strings.Join(portStrings, "\n"))
|
||||||
|
|
||||||
filepath := s.settings.Filepath
|
filepath := s.settings.Filepath
|
||||||
if len(ports) == 0 {
|
|
||||||
s.logger.Info("clearing port file " + filepath)
|
|
||||||
} else {
|
|
||||||
s.logger.Info("writing port file " + filepath)
|
s.logger.Info("writing port file " + filepath)
|
||||||
}
|
|
||||||
const perms = os.FileMode(0o644)
|
const perms = os.FileMode(0o644)
|
||||||
err = os.WriteFile(filepath, fileData, perms)
|
err = os.WriteFile(filepath, fileData, perms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
|
|||||||
s.portMutex.Unlock()
|
s.portMutex.Unlock()
|
||||||
|
|
||||||
if s.settings.UpCommand != "" {
|
if s.settings.UpCommand != "" {
|
||||||
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports, s.settings.Interface)
|
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("running up command: %w", err)
|
err = fmt.Errorf("running up command: %w", err)
|
||||||
s.logger.Error(err.Error())
|
s.logger.Error(err.Error())
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (s *Service) cleanup() (err error) {
|
|||||||
const downTimeout = 60 * time.Second
|
const downTimeout = 60 * time.Second
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), downTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), downTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err = runCommand(ctx, s.cmder, s.logger, s.settings.DownCommand, s.ports, s.settings.Interface)
|
err = runCommand(ctx, s.cmder, s.logger, s.settings.DownCommand, s.ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("running down command: %w", err)
|
err = fmt.Errorf("running down command: %w", err)
|
||||||
s.logger.Error(err.Error())
|
s.logger.Error(err.Error())
|
||||||
@@ -59,6 +59,8 @@ func (s *Service) cleanup() (err error) {
|
|||||||
|
|
||||||
s.ports = nil
|
s.ports = nil
|
||||||
|
|
||||||
|
filepath := s.settings.Filepath
|
||||||
|
s.logger.Info("clearing port file " + filepath)
|
||||||
err = s.writePortForwardedFile(nil)
|
err = s.writePortForwardedFile(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("clearing port file: %w", err)
|
return fmt.Errorf("clearing port file: %w", err)
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ func bindPort(ctx context.Context, client *http.Client, apiIPAddress netip.Addr,
|
|||||||
// replaceInErr is used to remove sensitive information from errors.
|
// replaceInErr is used to remove sensitive information from errors.
|
||||||
func replaceInErr(err error, substitutions map[string]string) error {
|
func replaceInErr(err error, substitutions map[string]string) error {
|
||||||
s := replaceInString(err.Error(), substitutions)
|
s := replaceInString(err.Error(), substitutions)
|
||||||
return errors.New(s) //nolint:err113
|
return errors.New(s) //nolint:goerr113
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceInString is used to remove sensitive information.
|
// replaceInString is used to remove sensitive information.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func FetchMultiInfo(ctx context.Context, fetcher InfoFetcher, ips []netip.Addr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
results = make([]models.PublicIP, len(ips))
|
results = make([]models.PublicIP, len(ips))
|
||||||
for range ips {
|
for range len(ips) {
|
||||||
aResult := <-resultsCh
|
aResult := <-resultsCh
|
||||||
if aResult.err != nil {
|
if aResult.err != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type dnsHandler struct {
|
|||||||
func (h *dnsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *dnsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
r.RequestURI = strings.TrimPrefix(r.RequestURI, "/dns")
|
r.RequestURI = strings.TrimPrefix(r.RequestURI, "/dns")
|
||||||
switch r.RequestURI {
|
switch r.RequestURI {
|
||||||
case "/status":
|
case "/status": //nolint:goconst
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
h.getStatus(w)
|
h.getStatus(w)
|
||||||
|
|||||||
@@ -25,14 +25,13 @@ func newHandler(ctx context.Context, logger Logger, logging bool,
|
|||||||
handler := &handler{}
|
handler := &handler{}
|
||||||
|
|
||||||
vpn := newVPNHandler(ctx, vpnLooper, storage, ipv6Supported, logger)
|
vpn := newVPNHandler(ctx, vpnLooper, storage, ipv6Supported, logger)
|
||||||
openvpn := newOpenvpnHandler(ctx, vpnLooper, logger)
|
openvpn := newOpenvpnHandler(ctx, vpnLooper, pfGetter, logger)
|
||||||
dns := newDNSHandler(ctx, dnsLooper, logger)
|
dns := newDNSHandler(ctx, dnsLooper, logger)
|
||||||
updater := newUpdaterHandler(ctx, updaterLooper, logger)
|
updater := newUpdaterHandler(ctx, updaterLooper, logger)
|
||||||
publicip := newPublicIPHandler(publicIPLooper, logger)
|
publicip := newPublicIPHandler(publicIPLooper, logger)
|
||||||
portForward := newPortForwardHandler(ctx, pfGetter, logger)
|
|
||||||
|
|
||||||
handler.v0 = newHandlerV0(ctx, logger, vpnLooper, dnsLooper, updaterLooper)
|
handler.v0 = newHandlerV0(ctx, logger, vpnLooper, dnsLooper, updaterLooper)
|
||||||
handler.v1 = newHandlerV1(logger, buildInfo, vpn, openvpn, dns, updater, publicip, portForward)
|
handler.v1 = newHandlerV1(logger, buildInfo, vpn, openvpn, dns, updater, publicip)
|
||||||
|
|
||||||
authMiddleware, err := auth.New(authSettings, logger)
|
authMiddleware, err := auth.New(authSettings, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (h *handlerV0) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.logger.Warn(err.Error())
|
h.logger.Warn(err.Error())
|
||||||
}
|
}
|
||||||
case "/openvpn/portforwarded":
|
case "/openvpn/portforwarded":
|
||||||
http.Redirect(w, r, "/v1/portforward", http.StatusPermanentRedirect)
|
http.Redirect(w, r, "/v1/openvpn/portforwarded", http.StatusPermanentRedirect)
|
||||||
case "/openvpn/settings":
|
case "/openvpn/settings":
|
||||||
http.Redirect(w, r, "/v1/openvpn/settings", http.StatusPermanentRedirect)
|
http.Redirect(w, r, "/v1/openvpn/settings", http.StatusPermanentRedirect)
|
||||||
case "/updater/restart":
|
case "/updater/restart":
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func newHandlerV1(w warner, buildInfo models.BuildInformation,
|
func newHandlerV1(w warner, buildInfo models.BuildInformation,
|
||||||
vpn, openvpn, dns, updater, publicip, portForward http.Handler,
|
vpn, openvpn, dns, updater, publicip http.Handler,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return &handlerV1{
|
return &handlerV1{
|
||||||
warner: w,
|
warner: w,
|
||||||
@@ -20,7 +20,6 @@ func newHandlerV1(w warner, buildInfo models.BuildInformation,
|
|||||||
dns: dns,
|
dns: dns,
|
||||||
updater: updater,
|
updater: updater,
|
||||||
publicip: publicip,
|
publicip: publicip,
|
||||||
portForward: portForward,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +31,6 @@ type handlerV1 struct {
|
|||||||
dns http.Handler
|
dns http.Handler
|
||||||
updater http.Handler
|
updater http.Handler
|
||||||
publicip http.Handler
|
publicip http.Handler
|
||||||
portForward http.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handlerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *handlerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -49,8 +47,6 @@ func (h *handlerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.updater.ServeHTTP(w, r)
|
h.updater.ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.RequestURI, "/publicip"):
|
case strings.HasPrefix(r.RequestURI, "/publicip"):
|
||||||
h.publicip.ServeHTTP(w, r)
|
h.publicip.ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.RequestURI, "/portforward"):
|
|
||||||
h.portForward.ServeHTTP(w, r)
|
|
||||||
default:
|
default:
|
||||||
errString := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI)
|
errString := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI)
|
||||||
http.Error(w, errString, http.StatusBadRequest)
|
http.Error(w, errString, http.StatusBadRequest)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ type infoWarner interface {
|
|||||||
|
|
||||||
type infoer interface {
|
type infoer interface {
|
||||||
Info(s string)
|
Info(s string)
|
||||||
Infof(format string, args ...any)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type warner interface {
|
type warner interface {
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import (
|
|||||||
func Read(filepath string) (settings Settings, err error) {
|
func Read(filepath string) (settings Settings, err error) {
|
||||||
file, err := os.Open(filepath)
|
file, err := os.Open(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return Settings{}, nil
|
||||||
|
}
|
||||||
return settings, fmt.Errorf("opening file: %w", err)
|
return settings, fmt.Errorf("opening file: %w", err)
|
||||||
}
|
}
|
||||||
decoder := toml.NewDecoder(file)
|
decoder := toml.NewDecoder(file)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ func New(settings Settings, debugLogger DebugLogger) (
|
|||||||
routeToRoles: routeToRoles,
|
routeToRoles: routeToRoles,
|
||||||
unprotectedRoutes: map[string]struct{}{
|
unprotectedRoutes: map[string]struct{}{
|
||||||
http.MethodGet + " /openvpn/actions/restart": {},
|
http.MethodGet + " /openvpn/actions/restart": {},
|
||||||
http.MethodGet + " /openvpn/portforwarded": {},
|
|
||||||
http.MethodGet + " /unbound/actions/restart": {},
|
http.MethodGet + " /unbound/actions/restart": {},
|
||||||
http.MethodGet + " /updater/restart": {},
|
http.MethodGet + " /updater/restart": {},
|
||||||
http.MethodGet + " /v1/version": {},
|
http.MethodGet + " /v1/version": {},
|
||||||
@@ -37,7 +36,6 @@ func New(settings Settings, debugLogger DebugLogger) (
|
|||||||
http.MethodGet + " /v1/updater/status": {},
|
http.MethodGet + " /v1/updater/status": {},
|
||||||
http.MethodPut + " /v1/updater/status": {},
|
http.MethodPut + " /v1/updater/status": {},
|
||||||
http.MethodGet + " /v1/publicip/ip": {},
|
http.MethodGet + " /v1/publicip/ip": {},
|
||||||
http.MethodGet + " /v1/portforward": {},
|
|
||||||
},
|
},
|
||||||
logger: debugLogger,
|
logger: debugLogger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/validate"
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
@@ -19,50 +15,6 @@ type Settings struct {
|
|||||||
Roles []Role
|
Roles []Role
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultRole sets a default role to apply to all routes without a
|
|
||||||
// previously user-defined role assigned to. Note the role argument
|
|
||||||
// routes are ignored. This should be called BEFORE calling [Settings.SetDefaults].
|
|
||||||
func (s *Settings) SetDefaultRole(jsonRole string) error {
|
|
||||||
var role Role
|
|
||||||
decoder := json.NewDecoder(bytes.NewBufferString(jsonRole))
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
err := decoder.Decode(&role)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("decoding default role: %w", err)
|
|
||||||
}
|
|
||||||
if role.Auth == "" {
|
|
||||||
return nil // no default role to set
|
|
||||||
}
|
|
||||||
err = role.Validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("validating default role: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticatedRoutes := make(map[string]struct{}, len(validRoutes))
|
|
||||||
for _, role := range s.Roles {
|
|
||||||
for _, route := range role.Routes {
|
|
||||||
authenticatedRoutes[route] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(authenticatedRoutes) == len(validRoutes) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
unauthenticatedRoutes := make([]string, 0, len(validRoutes))
|
|
||||||
for route := range validRoutes {
|
|
||||||
_, authenticated := authenticatedRoutes[route]
|
|
||||||
if !authenticated {
|
|
||||||
unauthenticatedRoutes = append(unauthenticatedRoutes, route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(unauthenticatedRoutes)
|
|
||||||
role.Routes = unauthenticatedRoutes
|
|
||||||
s.Roles = append(s.Roles, role)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Settings) SetDefaults() {
|
func (s *Settings) SetDefaults() {
|
||||||
s.Roles = gosettings.DefaultSlice(s.Roles, []Role{{ // TODO v3.41.0 leave empty
|
s.Roles = gosettings.DefaultSlice(s.Roles, []Role{{ // TODO v3.41.0 leave empty
|
||||||
Name: "public",
|
Name: "public",
|
||||||
@@ -70,7 +22,6 @@ func (s *Settings) SetDefaults() {
|
|||||||
Routes: []string{
|
Routes: []string{
|
||||||
http.MethodGet + " /openvpn/actions/restart",
|
http.MethodGet + " /openvpn/actions/restart",
|
||||||
http.MethodGet + " /unbound/actions/restart",
|
http.MethodGet + " /unbound/actions/restart",
|
||||||
http.MethodGet + " /openvpn/portforwarded",
|
|
||||||
http.MethodGet + " /updater/restart",
|
http.MethodGet + " /updater/restart",
|
||||||
http.MethodGet + " /v1/version",
|
http.MethodGet + " /v1/version",
|
||||||
http.MethodGet + " /v1/vpn/status",
|
http.MethodGet + " /v1/vpn/status",
|
||||||
@@ -83,14 +34,13 @@ func (s *Settings) SetDefaults() {
|
|||||||
http.MethodGet + " /v1/updater/status",
|
http.MethodGet + " /v1/updater/status",
|
||||||
http.MethodPut + " /v1/updater/status",
|
http.MethodPut + " /v1/updater/status",
|
||||||
http.MethodGet + " /v1/publicip/ip",
|
http.MethodGet + " /v1/publicip/ip",
|
||||||
http.MethodGet + " /v1/portforward",
|
|
||||||
},
|
},
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Settings) Validate() (err error) {
|
func (s Settings) Validate() (err error) {
|
||||||
for i, role := range s.Roles {
|
for i, role := range s.Roles {
|
||||||
err = role.Validate()
|
err = role.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("role %s (%d of %d): %w",
|
return fmt.Errorf("role %s (%d of %d): %w",
|
||||||
role.Name, i+1, len(s.Roles), err)
|
role.Name, i+1, len(s.Roles), err)
|
||||||
@@ -111,18 +61,18 @@ const (
|
|||||||
type Role struct {
|
type Role struct {
|
||||||
// Name is the role name and is only used for documentation
|
// Name is the role name and is only used for documentation
|
||||||
// and in the authentication middleware debug logs.
|
// and in the authentication middleware debug logs.
|
||||||
Name string `json:"name"`
|
Name string
|
||||||
// Auth is the authentication method to use, which can be 'none', 'basic' or 'apikey'.
|
// Auth is the authentication method to use, which can be 'none' or 'apikey'.
|
||||||
Auth string `json:"auth"`
|
Auth string
|
||||||
// APIKey is the API key to use when using the 'apikey' authentication.
|
// APIKey is the API key to use when using the 'apikey' authentication.
|
||||||
APIKey string `json:"apikey"`
|
APIKey string
|
||||||
// Username for HTTP Basic authentication method.
|
// Username for HTTP Basic authentication method.
|
||||||
Username string `json:"username"`
|
Username string
|
||||||
// Password for HTTP Basic authentication method.
|
// Password for HTTP Basic authentication method.
|
||||||
Password string `json:"password"`
|
Password string
|
||||||
// Routes is a list of routes that the role can access in the format
|
// Routes is a list of routes that the role can access in the format
|
||||||
// "HTTP_METHOD PATH", for example "GET /v1/vpn/status"
|
// "HTTP_METHOD PATH", for example "GET /v1/vpn/status"
|
||||||
Routes []string `json:"-"`
|
Routes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -133,7 +83,7 @@ var (
|
|||||||
ErrRouteNotSupported = errors.New("route not supported by the control server")
|
ErrRouteNotSupported = errors.New("route not supported by the control server")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Role) Validate() (err error) {
|
func (r Role) validate() (err error) {
|
||||||
err = validate.IsOneOf(r.Auth, AuthNone, AuthAPIKey, AuthBasic)
|
err = validate.IsOneOf(r.Auth, AuthNone, AuthAPIKey, AuthBasic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %s", ErrMethodNotSupported, r.Auth)
|
return fmt.Errorf("%w: %s", ErrMethodNotSupported, r.Auth)
|
||||||
@@ -162,8 +112,6 @@ func (r Role) Validate() (err error) {
|
|||||||
// WARNING: do not mutate programmatically.
|
// WARNING: do not mutate programmatically.
|
||||||
var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
|
var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
|
||||||
http.MethodGet + " /openvpn/actions/restart": {},
|
http.MethodGet + " /openvpn/actions/restart": {},
|
||||||
http.MethodGet + " /openvpn/portforwarded": {},
|
|
||||||
http.MethodGet + " /openvpn/settings": {},
|
|
||||||
http.MethodGet + " /unbound/actions/restart": {},
|
http.MethodGet + " /unbound/actions/restart": {},
|
||||||
http.MethodGet + " /updater/restart": {},
|
http.MethodGet + " /updater/restart": {},
|
||||||
http.MethodGet + " /v1/version": {},
|
http.MethodGet + " /v1/version": {},
|
||||||
@@ -180,22 +128,4 @@ var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
|
|||||||
http.MethodGet + " /v1/updater/status": {},
|
http.MethodGet + " /v1/updater/status": {},
|
||||||
http.MethodPut + " /v1/updater/status": {},
|
http.MethodPut + " /v1/updater/status": {},
|
||||||
http.MethodGet + " /v1/publicip/ip": {},
|
http.MethodGet + " /v1/publicip/ip": {},
|
||||||
http.MethodGet + " /v1/portforward": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Role) ToLinesNode() (node *gotree.Node) {
|
|
||||||
node = gotree.New("Role " + r.Name)
|
|
||||||
node.Appendf("Authentication method: %s", r.Auth)
|
|
||||||
switch r.Auth {
|
|
||||||
case AuthNone:
|
|
||||||
case AuthBasic:
|
|
||||||
node.Appendf("Username: %s", r.Username)
|
|
||||||
node.Appendf("Password: %s", gosettings.ObfuscateKey(r.Password))
|
|
||||||
case AuthAPIKey:
|
|
||||||
node.Appendf("API key: %s", gosettings.ObfuscateKey(r.APIKey))
|
|
||||||
default:
|
|
||||||
panic("missing code for authentication method: " + r.Auth)
|
|
||||||
}
|
|
||||||
node.Appendf("Number of routes covered: %d", len(r.Routes))
|
|
||||||
return node
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newOpenvpnHandler(ctx context.Context, looper VPNLooper, w warner) http.Handler {
|
func newOpenvpnHandler(ctx context.Context, looper VPNLooper,
|
||||||
|
pfGetter PortForwardedGetter, w warner,
|
||||||
|
) http.Handler {
|
||||||
return &openvpnHandler{
|
return &openvpnHandler{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
looper: looper,
|
looper: looper,
|
||||||
|
pf: pfGetter,
|
||||||
warner: w,
|
warner: w,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +24,7 @@ func newOpenvpnHandler(ctx context.Context, looper VPNLooper, w warner) http.Han
|
|||||||
type openvpnHandler struct {
|
type openvpnHandler struct {
|
||||||
ctx context.Context //nolint:containedctx
|
ctx context.Context //nolint:containedctx
|
||||||
looper VPNLooper
|
looper VPNLooper
|
||||||
|
pf PortForwardedGetter
|
||||||
warner warner
|
warner warner
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +47,10 @@ func (h *openvpnHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
default:
|
default:
|
||||||
errMethodNotSupported(w, r.Method)
|
errMethodNotSupported(w, r.Method)
|
||||||
}
|
}
|
||||||
case "/portforwarded": // TODO v4 remove
|
case "/portforwarded":
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
http.Redirect(w, r, "/v1/portforward", http.StatusMovedPermanently)
|
h.getPortForwarded(w)
|
||||||
default:
|
default:
|
||||||
errMethodNotSupported(w, r.Method)
|
errMethodNotSupported(w, r.Method)
|
||||||
}
|
}
|
||||||
@@ -118,3 +122,23 @@ func (h *openvpnHandler) getSettings(w http.ResponseWriter) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *openvpnHandler) getPortForwarded(w http.ResponseWriter) {
|
||||||
|
ports := h.pf.GetPortsForwarded()
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
var data any
|
||||||
|
switch len(ports) {
|
||||||
|
case 0:
|
||||||
|
data = portWrapper{Port: 0} // TODO v4 change to portsWrapper
|
||||||
|
case 1:
|
||||||
|
data = portWrapper{Port: ports[0]} // TODO v4 change to portsWrapper
|
||||||
|
default:
|
||||||
|
data = portsWrapper{Ports: ports}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := encoder.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
h.warner.Warn(err.Error())
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newPortForwardHandler(ctx context.Context,
|
|
||||||
portForward PortForwardedGetter, warner warner,
|
|
||||||
) http.Handler {
|
|
||||||
return &portForwardHandler{
|
|
||||||
ctx: ctx,
|
|
||||||
portForward: portForward,
|
|
||||||
warner: warner,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type portForwardHandler struct {
|
|
||||||
ctx context.Context //nolint:containedctx
|
|
||||||
portForward PortForwardedGetter
|
|
||||||
warner warner
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *portForwardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
h.getPortForwarded(w)
|
|
||||||
default:
|
|
||||||
errMethodNotSupported(w, r.Method)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *portForwardHandler) getPortForwarded(w http.ResponseWriter) {
|
|
||||||
ports := h.portForward.GetPortsForwarded()
|
|
||||||
encoder := json.NewEncoder(w)
|
|
||||||
var data any
|
|
||||||
switch len(ports) {
|
|
||||||
case 0:
|
|
||||||
data = portWrapper{Port: 0} // TODO v4 change to portsWrapper
|
|
||||||
case 1:
|
|
||||||
data = portWrapper{Port: ports[0]} // TODO v4 change to portsWrapper
|
|
||||||
default:
|
|
||||||
data = portsWrapper{Ports: ports}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := encoder.Encode(data)
|
|
||||||
if err != nil {
|
|
||||||
h.warner.Warn(err.Error())
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,29 +2,31 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/httpserver"
|
"github.com/qdm12/gluetun/internal/httpserver"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(ctx context.Context, settings settings.ControlServer, logger Logger,
|
func New(ctx context.Context, address string, logEnabled bool, logger Logger,
|
||||||
buildInfo models.BuildInformation, openvpnLooper VPNLooper,
|
authConfigPath string, buildInfo models.BuildInformation, openvpnLooper VPNLooper,
|
||||||
pfGetter PortForwardedGetter, dnsLooper DNSLoop,
|
pfGetter PortForwardedGetter, dnsLooper DNSLoop,
|
||||||
updaterLooper UpdaterLooper, publicIPLooper PublicIPLoop, storage Storage,
|
updaterLooper UpdaterLooper, publicIPLooper PublicIPLoop, storage Storage,
|
||||||
ipv6Supported bool) (
|
ipv6Supported bool) (
|
||||||
server *httpserver.Server, err error,
|
server *httpserver.Server, err error,
|
||||||
) {
|
) {
|
||||||
authSettings, err := setupAuthMiddleware(settings.AuthFilePath, settings.AuthDefaultRole, logger)
|
authSettings, err := auth.Read(authConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("building authentication middleware settings: %w", err)
|
return nil, fmt.Errorf("reading auth settings: %w", err)
|
||||||
|
}
|
||||||
|
authSettings.SetDefaults()
|
||||||
|
err = authSettings.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("validating auth settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := newHandler(ctx, logger, *settings.Log, authSettings, buildInfo,
|
handler, err := newHandler(ctx, logger, logEnabled, authSettings, buildInfo,
|
||||||
openvpnLooper, pfGetter, dnsLooper, updaterLooper, publicIPLooper,
|
openvpnLooper, pfGetter, dnsLooper, updaterLooper, publicIPLooper,
|
||||||
storage, ipv6Supported)
|
storage, ipv6Supported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -32,7 +34,7 @@ func New(ctx context.Context, settings settings.ControlServer, logger Logger,
|
|||||||
}
|
}
|
||||||
|
|
||||||
httpServerSettings := httpserver.Settings{
|
httpServerSettings := httpserver.Settings{
|
||||||
Address: *settings.Address,
|
Address: address,
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
}
|
}
|
||||||
@@ -44,26 +46,3 @@ func New(ctx context.Context, settings settings.ControlServer, logger Logger,
|
|||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupAuthMiddleware(authPath, jsonDefaultRole string, logger Logger) (
|
|
||||||
authSettings auth.Settings, err error,
|
|
||||||
) {
|
|
||||||
authSettings, err = auth.Read(authPath)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, os.ErrNotExist): // no auth file present
|
|
||||||
case err != nil:
|
|
||||||
return auth.Settings{}, fmt.Errorf("reading auth settings: %w", err)
|
|
||||||
default:
|
|
||||||
logger.Infof("read %d roles from authentication file", len(authSettings.Roles))
|
|
||||||
}
|
|
||||||
err = authSettings.SetDefaultRole(jsonDefaultRole)
|
|
||||||
if err != nil {
|
|
||||||
return auth.Settings{}, fmt.Errorf("setting default role: %w", err)
|
|
||||||
}
|
|
||||||
authSettings.SetDefaults()
|
|
||||||
err = authSettings.Validate()
|
|
||||||
if err != nil {
|
|
||||||
return auth.Settings{}, fmt.Errorf("validating auth settings: %w", err)
|
|
||||||
}
|
|
||||||
return authSettings, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ func (s *Storage) FlushToFile(path string) error {
|
|||||||
// flushToFile flushes the merged servers data to the file
|
// flushToFile flushes the merged servers data to the file
|
||||||
// specified by path, as indented JSON. It is not thread-safe.
|
// specified by path, as indented JSON. It is not thread-safe.
|
||||||
func (s *Storage) flushToFile(path string) error {
|
func (s *Storage) flushToFile(path string) error {
|
||||||
if path == "" {
|
|
||||||
return nil // no file to write to
|
|
||||||
}
|
|
||||||
const permission = 0o644
|
const permission = 0o644
|
||||||
dirPath := filepath.Dir(path)
|
dirPath := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dirPath, permission); err != nil {
|
if err := os.MkdirAll(dirPath, permission); err != nil {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,16 +33,13 @@ func New(logger Logger, filepath string) (storage *Storage, err error) {
|
|||||||
|
|
||||||
storage = &Storage{
|
storage = &Storage{
|
||||||
hardcodedServers: hardcodedServers,
|
hardcodedServers: hardcodedServers,
|
||||||
mergedServers: hardcodedServers,
|
|
||||||
logger: logger,
|
logger: logger,
|
||||||
filepath: filepath,
|
filepath: filepath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if filepath != "" {
|
|
||||||
if err := storage.syncServers(); err != nil {
|
if err := storage.syncServers(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return storage, nil
|
return storage, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func (s *Storage) syncServers() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Eventually write file
|
// Eventually write file
|
||||||
if reflect.DeepEqual(serversOnFile, s.mergedServers) {
|
if s.filepath == "" || reflect.DeepEqual(serversOnFile, s.mergedServers) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,13 +99,3 @@ type CmdStarter interface {
|
|||||||
stdoutLines, stderrLines <-chan string,
|
stdoutLines, stderrLines <-chan string,
|
||||||
waitError <-chan error, startErr error)
|
waitError <-chan error, startErr error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type HealthChecker interface {
|
|
||||||
SetConfig(tlsDialAddrs []string, icmpTargetIPs []netip.Addr, smallCheckType string)
|
|
||||||
Start(ctx context.Context) (runError <-chan error, err error)
|
|
||||||
Stop() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type HealthServer interface {
|
|
||||||
SetError(err error)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ type Loop struct {
|
|||||||
state *state.State
|
state *state.State
|
||||||
providers Providers
|
providers Providers
|
||||||
storage Storage
|
storage Storage
|
||||||
healthSettings settings.Health
|
|
||||||
healthChecker HealthChecker
|
|
||||||
healthServer HealthServer
|
|
||||||
// Fixed parameters
|
// Fixed parameters
|
||||||
buildInfo models.BuildInformation
|
buildInfo models.BuildInformation
|
||||||
versionInfo bool
|
versionInfo bool
|
||||||
@@ -52,8 +49,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint16,
|
func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint16,
|
||||||
providers Providers, storage Storage, healthSettings settings.Health,
|
providers Providers, storage Storage, openvpnConf OpenVPN,
|
||||||
healthChecker HealthChecker, healthServer HealthServer, openvpnConf OpenVPN,
|
|
||||||
netLinker NetLinker, fw Firewall, routing Routing,
|
netLinker NetLinker, fw Firewall, routing Routing,
|
||||||
portForward PortForward, starter CmdStarter,
|
portForward PortForward, starter CmdStarter,
|
||||||
publicip PublicIPLoop, dnsLooper DNSLoop,
|
publicip PublicIPLoop, dnsLooper DNSLoop,
|
||||||
@@ -73,9 +69,6 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1
|
|||||||
state: state,
|
state: state,
|
||||||
providers: providers,
|
providers: providers,
|
||||||
storage: storage,
|
storage: storage,
|
||||||
healthSettings: healthSettings,
|
|
||||||
healthChecker: healthChecker,
|
|
||||||
healthServer: healthServer,
|
|
||||||
buildInfo: buildInfo,
|
buildInfo: buildInfo,
|
||||||
versionInfo: versionInfo,
|
versionInfo: versionInfo,
|
||||||
ipv6Supported: ipv6Supported,
|
ipv6Supported: ipv6Supported,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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/openvpn"
|
"github.com/qdm12/gluetun/internal/openvpn"
|
||||||
"github.com/qdm12/gluetun/internal/provider"
|
"github.com/qdm12/gluetun/internal/provider"
|
||||||
)
|
)
|
||||||
@@ -15,38 +14,39 @@ import (
|
|||||||
func setupOpenVPN(ctx context.Context, fw Firewall,
|
func setupOpenVPN(ctx context.Context, fw Firewall,
|
||||||
openvpnConf OpenVPN, providerConf provider.Provider,
|
openvpnConf OpenVPN, providerConf provider.Provider,
|
||||||
settings settings.VPN, ipv6Supported bool, starter CmdStarter,
|
settings settings.VPN, ipv6Supported bool, starter CmdStarter,
|
||||||
logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, err error,
|
logger openvpn.Logger) (runner *openvpn.Runner, serverName string,
|
||||||
|
canPortForward bool, err error,
|
||||||
) {
|
) {
|
||||||
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
|
connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, models.Connection{}, fmt.Errorf("finding a valid server connection: %w", err)
|
return nil, "", false, fmt.Errorf("finding a valid server connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := providerConf.OpenVPNConfig(connection, settings.OpenVPN, ipv6Supported)
|
lines := providerConf.OpenVPNConfig(connection, settings.OpenVPN, ipv6Supported)
|
||||||
|
|
||||||
if err := openvpnConf.WriteConfig(lines); err != nil {
|
if err := openvpnConf.WriteConfig(lines); err != nil {
|
||||||
return nil, models.Connection{}, fmt.Errorf("writing configuration to file: %w", err)
|
return nil, "", false, fmt.Errorf("writing configuration to file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *settings.OpenVPN.User != "" {
|
if *settings.OpenVPN.User != "" {
|
||||||
err := openvpnConf.WriteAuthFile(*settings.OpenVPN.User, *settings.OpenVPN.Password)
|
err := openvpnConf.WriteAuthFile(*settings.OpenVPN.User, *settings.OpenVPN.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, models.Connection{}, fmt.Errorf("writing auth to file: %w", err)
|
return nil, "", false, fmt.Errorf("writing auth to file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if *settings.OpenVPN.KeyPassphrase != "" {
|
if *settings.OpenVPN.KeyPassphrase != "" {
|
||||||
err := openvpnConf.WriteAskPassFile(*settings.OpenVPN.KeyPassphrase)
|
err := openvpnConf.WriteAskPassFile(*settings.OpenVPN.KeyPassphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, models.Connection{}, fmt.Errorf("writing askpass file: %w", err)
|
return nil, "", false, fmt.Errorf("writing askpass file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fw.SetVPNConnection(ctx, connection, settings.OpenVPN.Interface); err != nil {
|
if err := fw.SetVPNConnection(ctx, connection, settings.OpenVPN.Interface); err != nil {
|
||||||
return nil, models.Connection{}, fmt.Errorf("allowing VPN connection through firewall: %w", err)
|
return nil, "", false, fmt.Errorf("allowing VPN connection through firewall: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
runner = openvpn.NewRunner(settings.OpenVPN, starter, logger)
|
runner = openvpn.NewRunner(settings.OpenVPN, starter, logger)
|
||||||
|
|
||||||
return runner, connection, nil
|
return runner, connection.ServerName, connection.PortForward, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/log"
|
"github.com/qdm12/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,17 +28,17 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
var vpnRunner interface {
|
var vpnRunner interface {
|
||||||
Run(ctx context.Context, waitError chan<- error, tunnelReady chan<- struct{})
|
Run(ctx context.Context, waitError chan<- error, tunnelReady chan<- struct{})
|
||||||
}
|
}
|
||||||
var vpnInterface string
|
var serverName, vpnInterface string
|
||||||
var connection models.Connection
|
var canPortForward bool
|
||||||
var err error
|
var err error
|
||||||
subLogger := l.logger.New(log.SetComponent(settings.Type))
|
subLogger := l.logger.New(log.SetComponent(settings.Type))
|
||||||
if settings.Type == vpn.OpenVPN {
|
if settings.Type == vpn.OpenVPN {
|
||||||
vpnInterface = settings.OpenVPN.Interface
|
vpnInterface = settings.OpenVPN.Interface
|
||||||
vpnRunner, connection, err = setupOpenVPN(ctx, l.fw,
|
vpnRunner, serverName, canPortForward, err = setupOpenVPN(ctx, l.fw,
|
||||||
l.openvpnConf, providerConf, settings, l.ipv6Supported, l.starter, subLogger)
|
l.openvpnConf, providerConf, settings, l.ipv6Supported, l.starter, subLogger)
|
||||||
} else { // Wireguard
|
} else { // Wireguard
|
||||||
vpnInterface = settings.Wireguard.Interface
|
vpnInterface = settings.Wireguard.Interface
|
||||||
vpnRunner, connection, err = setupWireguard(ctx, l.netLinker, l.fw,
|
vpnRunner, serverName, canPortForward, err = setupWireguard(ctx, l.netLinker, l.fw,
|
||||||
providerConf, settings, l.ipv6Supported, subLogger)
|
providerConf, settings, l.ipv6Supported, subLogger)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,23 +46,22 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tunnelUpData := tunnelUpData{
|
tunnelUpData := tunnelUpData{
|
||||||
serverIP: connection.IP,
|
serverName: serverName,
|
||||||
serverName: connection.ServerName,
|
canPortForward: canPortForward,
|
||||||
canPortForward: connection.PortForward,
|
|
||||||
portForwarder: portForwarder,
|
portForwarder: portForwarder,
|
||||||
vpnIntf: vpnInterface,
|
vpnIntf: vpnInterface,
|
||||||
username: settings.Provider.PortForwarding.Username,
|
username: settings.Provider.PortForwarding.Username,
|
||||||
password: settings.Provider.PortForwarding.Password,
|
password: settings.Provider.PortForwarding.Password,
|
||||||
}
|
}
|
||||||
|
|
||||||
vpnCtx, vpnCancel := context.WithCancel(context.Background())
|
openvpnCtx, openvpnCancel := context.WithCancel(context.Background())
|
||||||
waitError := make(chan error)
|
waitError := make(chan error)
|
||||||
tunnelReady := make(chan struct{})
|
tunnelReady := make(chan struct{})
|
||||||
|
|
||||||
go vpnRunner.Run(vpnCtx, waitError, tunnelReady)
|
go vpnRunner.Run(openvpnCtx, waitError, tunnelReady)
|
||||||
|
|
||||||
if err := l.waitForError(ctx, waitError); err != nil {
|
if err := l.waitForError(ctx, waitError); err != nil {
|
||||||
vpnCancel()
|
openvpnCancel()
|
||||||
l.crashed(ctx, err)
|
l.crashed(ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -75,10 +73,10 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
for stayHere {
|
for stayHere {
|
||||||
select {
|
select {
|
||||||
case <-tunnelReady:
|
case <-tunnelReady:
|
||||||
go l.onTunnelUp(vpnCtx, ctx, tunnelUpData)
|
go l.onTunnelUp(openvpnCtx, tunnelUpData)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
l.cleanup()
|
l.cleanup()
|
||||||
vpnCancel()
|
openvpnCancel()
|
||||||
<-waitError
|
<-waitError
|
||||||
close(waitError)
|
close(waitError)
|
||||||
return
|
return
|
||||||
@@ -86,7 +84,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
l.logger.Info("stopping")
|
l.logger.Info("stopping")
|
||||||
l.cleanup()
|
l.cleanup()
|
||||||
vpnCancel()
|
openvpnCancel()
|
||||||
<-waitError
|
<-waitError
|
||||||
// do not close waitError or the waitError
|
// do not close waitError or the waitError
|
||||||
// select case will trigger
|
// select case will trigger
|
||||||
@@ -99,7 +97,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.statusManager.Lock() // prevent SetStatus from running in parallel
|
l.statusManager.Lock() // prevent SetStatus from running in parallel
|
||||||
|
|
||||||
l.cleanup()
|
l.cleanup()
|
||||||
vpnCancel()
|
openvpnCancel()
|
||||||
l.statusManager.SetStatus(constants.Crashed)
|
l.statusManager.SetStatus(constants.Crashed)
|
||||||
l.logAndWait(ctx, err)
|
l.logAndWait(ctx, err)
|
||||||
stayHere = false
|
stayHere = false
|
||||||
@@ -107,6 +105,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.statusManager.Unlock()
|
l.statusManager.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vpnCancel()
|
openvpnCancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package vpn
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/check"
|
"github.com/qdm12/dns/v2/pkg/check"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
@@ -10,8 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type tunnelUpData struct {
|
type tunnelUpData struct {
|
||||||
// Healthcheck
|
|
||||||
serverIP netip.Addr
|
|
||||||
// Port forwarding
|
// Port forwarding
|
||||||
vpnIntf string
|
vpnIntf string
|
||||||
serverName string // used for PIA
|
serverName string // used for PIA
|
||||||
@@ -21,7 +18,7 @@ type tunnelUpData struct {
|
|||||||
portForwarder PortForwarder
|
portForwarder PortForwarder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
|
func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) {
|
||||||
l.client.CloseIdleConnections()
|
l.client.CloseIdleConnections()
|
||||||
|
|
||||||
for _, vpnPort := range l.vpnInputPorts {
|
for _, vpnPort := range l.vpnInputPorts {
|
||||||
@@ -31,27 +28,7 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
icmpTargetIPs := l.healthSettings.ICMPTargetIPs
|
if *l.dnsLooper.GetSettings().DoT.Enabled {
|
||||||
if len(icmpTargetIPs) == 1 && icmpTargetIPs[0].IsUnspecified() {
|
|
||||||
icmpTargetIPs = []netip.Addr{data.serverIP}
|
|
||||||
}
|
|
||||||
l.healthChecker.SetConfig(l.healthSettings.TargetAddresses, icmpTargetIPs,
|
|
||||||
l.healthSettings.SmallCheckType)
|
|
||||||
|
|
||||||
healthErrCh, err := l.healthChecker.Start(ctx)
|
|
||||||
l.healthServer.SetError(err)
|
|
||||||
if err != nil {
|
|
||||||
if *l.healthSettings.RestartVPN {
|
|
||||||
// Note this restart call must be done in a separate goroutine
|
|
||||||
// from the VPN loop goroutine.
|
|
||||||
l.restartVPN(loopCtx, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l.logger.Warnf("(ignored) healthchecker start failed: %s", err)
|
|
||||||
l.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *l.dnsLooper.GetSettings().ServerEnabled {
|
|
||||||
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
|
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
|
||||||
} else {
|
} else {
|
||||||
err := check.WaitForDNS(ctx, check.Settings{})
|
err := check.WaitForDNS(ctx, check.Settings{})
|
||||||
@@ -60,7 +37,7 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.publicip.RunOnce(ctx)
|
err := l.publicip.RunOnce(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.logger.Error("getting public IP address information: " + err.Error())
|
l.logger.Error("getting public IP address information: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -79,41 +56,4 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
l.collectHealthErrors(ctx, loopCtx, healthErrCh)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Loop) collectHealthErrors(ctx, loopCtx context.Context, healthErrCh <-chan error) {
|
|
||||||
var previousHealthErr error
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
_ = l.healthChecker.Stop()
|
|
||||||
return
|
|
||||||
case healthErr := <-healthErrCh:
|
|
||||||
l.healthServer.SetError(healthErr)
|
|
||||||
if healthErr != nil {
|
|
||||||
if *l.healthSettings.RestartVPN {
|
|
||||||
// Note this restart call must be done in a separate goroutine
|
|
||||||
// from the VPN loop goroutine.
|
|
||||||
_ = l.healthChecker.Stop()
|
|
||||||
l.restartVPN(loopCtx, healthErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l.logger.Warnf("(ignored) healthcheck failed: %s", healthErr)
|
|
||||||
l.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md")
|
|
||||||
} else if previousHealthErr != nil {
|
|
||||||
l.logger.Info("healthcheck passed successfully after previous failure(s)")
|
|
||||||
}
|
|
||||||
previousHealthErr = healthErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Loop) restartVPN(ctx context.Context, healthErr error) {
|
|
||||||
l.logger.Warnf("restarting VPN because it failed to pass the healthcheck: %s", healthErr)
|
|
||||||
l.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md")
|
|
||||||
l.logger.Info("DO NOT OPEN AN ISSUE UNLESS YOU HAVE READ AND TRIED EVERY POSSIBLE SOLUTION")
|
|
||||||
_, _ = l.ApplyStatus(ctx, constants.Stopped)
|
|
||||||
_, _ = l.ApplyStatus(ctx, constants.Running)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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/provider"
|
"github.com/qdm12/gluetun/internal/provider"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||||
"github.com/qdm12/gluetun/internal/wireguard"
|
"github.com/qdm12/gluetun/internal/wireguard"
|
||||||
@@ -17,11 +16,11 @@ import (
|
|||||||
func setupWireguard(ctx context.Context, netlinker NetLinker,
|
func setupWireguard(ctx context.Context, netlinker NetLinker,
|
||||||
fw Firewall, providerConf provider.Provider,
|
fw Firewall, providerConf provider.Provider,
|
||||||
settings settings.VPN, ipv6Supported bool, logger wireguard.Logger) (
|
settings settings.VPN, ipv6Supported bool, logger wireguard.Logger) (
|
||||||
wireguarder *wireguard.Wireguard, connection models.Connection, err error,
|
wireguarder *wireguard.Wireguard, serverName string, canPortForward bool, err error,
|
||||||
) {
|
) {
|
||||||
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
|
connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, models.Connection{}, fmt.Errorf("finding a VPN server: %w", err)
|
return nil, "", false, fmt.Errorf("finding a VPN server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wireguardSettings := utils.BuildWireguardSettings(connection, settings.Wireguard, ipv6Supported)
|
wireguardSettings := utils.BuildWireguardSettings(connection, settings.Wireguard, ipv6Supported)
|
||||||
@@ -32,13 +31,13 @@ func setupWireguard(ctx context.Context, netlinker NetLinker,
|
|||||||
|
|
||||||
wireguarder, err = wireguard.New(wireguardSettings, netlinker, logger)
|
wireguarder, err = wireguard.New(wireguardSettings, netlinker, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, models.Connection{}, fmt.Errorf("creating Wireguard: %w", err)
|
return nil, "", false, fmt.Errorf("creating Wireguard: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = fw.SetVPNConnection(ctx, connection, settings.Wireguard.Interface)
|
err = fw.SetVPNConnection(ctx, connection, settings.Wireguard.Interface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, models.Connection{}, fmt.Errorf("setting firewall: %w", err)
|
return nil, "", false, fmt.Errorf("setting firewall: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return wireguarder, connection, nil
|
return wireguarder, connection.ServerName, connection.PortForward, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user