Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f636a038c | ||
|
|
052317f95b | ||
|
|
5f355fabbe |
@@ -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/)
|
|
||||||
45
.github/workflows/ci.yml
vendored
45
.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:
|
||||||
@@ -59,40 +59,13 @@ jobs:
|
|||||||
- name: Run tests in test container
|
- name: Run tests in test container
|
||||||
run: |
|
run: |
|
||||||
touch coverage.txt
|
touch coverage.txt
|
||||||
docker run --rm --device /dev/net/tun \
|
docker run --rm \
|
||||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||||
test-container
|
test-container
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
codeql:
|
codeql:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -100,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: |
|
||||||
@@ -125,7 +98,7 @@ jobs:
|
|||||||
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@v20
|
- uses: DavidAnson/markdownlint-cli2-action@v16
|
||||||
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,70 +1,27 @@
|
|||||||
version: "2"
|
linters-settings:
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
|
||||||
formatters:
|
issues:
|
||||||
enable:
|
exclude-rules:
|
||||||
- gci
|
- path: _test\.go
|
||||||
- gofumpt
|
linters:
|
||||||
- goimports
|
- dupl
|
||||||
exclusions:
|
- err113
|
||||||
generated: lax
|
- containedctx
|
||||||
paths:
|
- maintidx
|
||||||
- third_party$
|
- path: "internal\\/server\\/.+\\.go"
|
||||||
- builtin$
|
linters:
|
||||||
- examples$
|
- dupl
|
||||||
|
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
|
||||||
|
linters:
|
||||||
|
- ireturn
|
||||||
|
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
|
||||||
|
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
||||||
|
linters:
|
||||||
|
- ireturn
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
settings:
|
|
||||||
misspell:
|
|
||||||
locale: US
|
|
||||||
goconst:
|
|
||||||
ignore-string-values:
|
|
||||||
# commonly used settings strings
|
|
||||||
- "^disabled$"
|
|
||||||
# Firewall and routing strings
|
|
||||||
- "^(ACCEPT|DROP)$"
|
|
||||||
- "^--delete$"
|
|
||||||
- "^all$"
|
|
||||||
- "^(tcp|udp)$"
|
|
||||||
# Server route strings
|
|
||||||
- "^/status$"
|
|
||||||
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
presets:
|
|
||||||
- comments
|
|
||||||
- common-false-positives
|
|
||||||
- legacy
|
|
||||||
- std-error-handling
|
|
||||||
rules:
|
|
||||||
- linters:
|
|
||||||
- containedctx
|
|
||||||
- dupl
|
|
||||||
- err113
|
|
||||||
- maintidx
|
|
||||||
path: _test\.go
|
|
||||||
- linters:
|
|
||||||
- dupl
|
|
||||||
path: internal\/server\/.+\.go
|
|
||||||
- linters:
|
|
||||||
- ireturn
|
|
||||||
text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
|
|
||||||
- linters:
|
|
||||||
- ireturn
|
|
||||||
path: internal\/openvpn\/pkcs8\/descbc\.go
|
|
||||||
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
|
|
||||||
- linters:
|
|
||||||
- revive
|
|
||||||
path: internal\/provider\/(common|utils)\/.+\.go
|
|
||||||
text: "var-naming: avoid (bad|meaningless) package names"
|
|
||||||
- linters:
|
|
||||||
- err113
|
|
||||||
- mnd
|
|
||||||
path: ci\/.+\.go
|
|
||||||
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
enable:
|
enable:
|
||||||
# - cyclop
|
# - cyclop
|
||||||
# - errorlint
|
# - errorlint
|
||||||
@@ -72,6 +29,7 @@ linters:
|
|||||||
- asciicheck
|
- asciicheck
|
||||||
- bidichk
|
- bidichk
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
- canonicalheader
|
||||||
- containedctx
|
- containedctx
|
||||||
- copyloopvar
|
- copyloopvar
|
||||||
- decorder
|
- decorder
|
||||||
@@ -85,6 +43,7 @@ linters:
|
|||||||
- exhaustive
|
- exhaustive
|
||||||
- fatcontext
|
- fatcontext
|
||||||
- forcetypeassert
|
- forcetypeassert
|
||||||
|
- gci
|
||||||
- gocheckcompilerdirectives
|
- gocheckcompilerdirectives
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
@@ -93,7 +52,9 @@ linters:
|
|||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godot
|
- godot
|
||||||
|
- gofumpt
|
||||||
- goheader
|
- goheader
|
||||||
|
- goimports
|
||||||
- gomoddirectives
|
- gomoddirectives
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
@@ -126,6 +87,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",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
43
Dockerfile
43
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 && \
|
||||||
@@ -106,7 +106,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
||||||
WIREGUARD_ADDRESSES= \
|
WIREGUARD_ADDRESSES= \
|
||||||
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
||||||
WIREGUARD_MTU=1320 \
|
WIREGUARD_MTU=1400 \
|
||||||
WIREGUARD_IMPLEMENTATION=auto \
|
WIREGUARD_IMPLEMENTATION=auto \
|
||||||
# VPN server filtering
|
# VPN server filtering
|
||||||
SERVER_REGIONS= \
|
SERVER_REGIONS= \
|
||||||
@@ -125,8 +125,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||||
VPN_PORT_FORWARDING_USERNAME= \
|
VPN_PORT_FORWARDING_USERNAME= \
|
||||||
VPN_PORT_FORWARDING_PASSWORD= \
|
VPN_PORT_FORWARDING_PASSWORD= \
|
||||||
VPN_PORT_FORWARDING_UP_COMMAND= \
|
|
||||||
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
|
||||||
# # Cyberghost only:
|
# # Cyberghost only:
|
||||||
OPENVPN_CERT= \
|
OPENVPN_CERT= \
|
||||||
OPENVPN_KEY= \
|
OPENVPN_KEY= \
|
||||||
@@ -164,20 +162,19 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# Health
|
# Health
|
||||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||||
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||||
HEALTH_ICMP_TARGET_IP=1.1.1.1 \
|
HEALTH_SUCCESS_WAIT_DURATION=5s \
|
||||||
HEALTH_RESTART_VPN=on \
|
HEALTH_VPN_DURATION_INITIAL=6s \
|
||||||
# DNS
|
HEALTH_VPN_DURATION_ADDITION=5s \
|
||||||
DNS_SERVER=on \
|
# DNS over TLS
|
||||||
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
DOT=on \
|
||||||
DNS_UPSTREAM_RESOLVERS=cloudflare \
|
DOT_PROVIDERS=cloudflare \
|
||||||
DNS_BLOCK_IPS= \
|
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_IP_PREFIXES=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
DOT_CACHING=on \
|
||||||
DNS_CACHING=on \
|
DOT_IPV6=off \
|
||||||
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_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 \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Gluetun VPN client
|
# Gluetun VPN client
|
||||||
|
|
||||||
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -26,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)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -55,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
|
||||||
@@ -87,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,33 +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)
|
|
||||||
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,193 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/api/types/network"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MullvadTest(ctx context.Context) error {
|
|
||||||
secrets, err := readSecrets(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading secrets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeout = 15 * time.Second
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating Docker client: %w", err)
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
config := &container.Config{
|
|
||||||
Image: "qmcgaw/gluetun",
|
|
||||||
StopTimeout: ptrTo(3),
|
|
||||||
Env: []string{
|
|
||||||
"VPN_SERVICE_PROVIDER=mullvad",
|
|
||||||
"VPN_TYPE=wireguard",
|
|
||||||
"LOG_LEVEL=debug",
|
|
||||||
"SERVER_COUNTRIES=USA",
|
|
||||||
"WIREGUARD_PRIVATE_KEY=" + secrets.mullvadWireguardPrivateKey,
|
|
||||||
"WIREGUARD_ADDRESSES=" + secrets.mullvadWireguardAddress,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
hostConfig := &container.HostConfig{
|
|
||||||
AutoRemove: true,
|
|
||||||
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
|
|
||||||
}
|
|
||||||
networkConfig := (*network.NetworkingConfig)(nil)
|
|
||||||
platform := (*v1.Platform)(nil)
|
|
||||||
const containerName = "" // auto-generated name
|
|
||||||
|
|
||||||
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating container: %w", err)
|
|
||||||
}
|
|
||||||
for _, warning := range response.Warnings {
|
|
||||||
fmt.Println("Warning during container creation:", warning)
|
|
||||||
}
|
|
||||||
containerID := response.ID
|
|
||||||
defer stopContainer(client, containerID)
|
|
||||||
|
|
||||||
beforeStartTime := time.Now()
|
|
||||||
|
|
||||||
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return waitForLogLine(ctx, client, containerID, beforeStartTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptrTo[T any](v T) *T { return &v }
|
|
||||||
|
|
||||||
type secrets struct {
|
|
||||||
mullvadWireguardPrivateKey string
|
|
||||||
mullvadWireguardAddress string
|
|
||||||
}
|
|
||||||
|
|
||||||
func readSecrets(ctx context.Context) (secrets, error) {
|
|
||||||
expectedSecrets := [...]string{
|
|
||||||
"Mullvad Wireguard private key",
|
|
||||||
"Mullvad Wireguard address",
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
lines := make([]string, 0, len(expectedSecrets))
|
|
||||||
|
|
||||||
for i := range expectedSecrets {
|
|
||||||
fmt.Println("🤫 reading", expectedSecrets[i], "from Stdin...")
|
|
||||||
if !scanner.Scan() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return secrets{}, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return secrets{}, fmt.Errorf("reading secrets from stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(lines) < len(expectedSecrets) {
|
|
||||||
return secrets{}, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
|
|
||||||
len(expectedSecrets), len(lines))
|
|
||||||
}
|
|
||||||
for i, line := range lines {
|
|
||||||
if line == "" {
|
|
||||||
return secrets{}, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return secrets{
|
|
||||||
mullvadWireguardPrivateKey: lines[0],
|
|
||||||
mullvadWireguardAddress: lines[1],
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopContainer(client *client.Client, containerID string) {
|
|
||||||
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
|
|
||||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
|
||||||
defer stopCancel()
|
|
||||||
|
|
||||||
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("failed to stop container:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
|
|
||||||
|
|
||||||
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
|
|
||||||
beforeStartTime time.Time,
|
|
||||||
) error {
|
|
||||||
logOptions := container.LogsOptions{
|
|
||||||
ShowStdout: true,
|
|
||||||
Follow: true,
|
|
||||||
Since: beforeStartTime.Format(time.RFC3339Nano),
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting container logs: %w", err)
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
var linesSeen []string
|
|
||||||
scanner := bufio.NewScanner(reader)
|
|
||||||
for ctx.Err() == nil {
|
|
||||||
if scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if len(line) > 8 { // remove Docker log prefix
|
|
||||||
line = line[8:]
|
|
||||||
}
|
|
||||||
linesSeen = append(linesSeen, line)
|
|
||||||
if successRegexp.MatchString(line) {
|
|
||||||
fmt.Println("✅ Success line logged")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := scanner.Err()
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
logSeenLines(linesSeen)
|
|
||||||
return fmt.Errorf("reading log stream: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The scanner is either done or cannot read because of EOF
|
|
||||||
fmt.Println("The log scanner stopped")
|
|
||||||
logSeenLines(linesSeen)
|
|
||||||
|
|
||||||
// Check if the container is still running
|
|
||||||
inspect, err := client.ContainerInspect(ctx, containerID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("inspecting container: %w", err)
|
|
||||||
}
|
|
||||||
if !inspect.State.Running {
|
|
||||||
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func logSeenLines(lines []string) {
|
|
||||||
fmt.Println("Logs seen so far:")
|
|
||||||
for _, line := range lines {
|
|
||||||
fmt.Println(" " + line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -98,13 +98,11 @@ func main() {
|
|||||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for OS signal or run error
|
|
||||||
var err error
|
var err error
|
||||||
select {
|
select {
|
||||||
case receivedSignal := <-signalCh:
|
case signal := <-signalCh:
|
||||||
signal.Stop(signalCh)
|
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
|
logger.Warn("Caught OS signal " + signal.String() + ", shutting down")
|
||||||
cancel()
|
cancel()
|
||||||
case err = <-errorCh:
|
case err = <-errorCh:
|
||||||
close(errorCh)
|
close(errorCh)
|
||||||
@@ -115,14 +113,15 @@ func main() {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown timed sequence, and force exit on second OS signal
|
|
||||||
const shutdownGracePeriod = 5 * time.Second
|
const shutdownGracePeriod = 5 * time.Second
|
||||||
timer := time.NewTimer(shutdownGracePeriod)
|
timer := time.NewTimer(shutdownGracePeriod)
|
||||||
select {
|
select {
|
||||||
case shutdownErr := <-errorCh:
|
case shutdownErr := <-errorCh:
|
||||||
timer.Stop()
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
if shutdownErr != nil {
|
if shutdownErr != nil {
|
||||||
logger.Warnf("Shutdown failed: %s", shutdownErr)
|
logger.Warnf("Shutdown not completed gracefully: %s", shutdownErr)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +133,9 @@ func main() {
|
|||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
logger.Warn("Shutdown timed out")
|
logger.Warn("Shutdown timed out")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
case signal := <-signalCh:
|
||||||
|
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +382,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
|
|
||||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||||
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
|
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
|
||||||
portForwardRunError, err := portForwardLooper.Start(ctx)
|
portForwardRunError, err := portForwardLooper.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||||
@@ -414,13 +416,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)
|
||||||
@@ -431,8 +426,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))
|
||||||
@@ -483,6 +478,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),
|
||||||
@@ -581,7 +582,6 @@ type Linker interface {
|
|||||||
LinkDel(link netlink.Link) (err error)
|
LinkDel(link netlink.Link) (err error)
|
||||||
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
||||||
LinkSetDown(link netlink.Link) (err error)
|
LinkSetDown(link netlink.Link) (err error)
|
||||||
LinkSetMTU(link netlink.Link, mtu int) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type clier interface {
|
type clier interface {
|
||||||
|
|||||||
61
go.mod
61
go.mod
@@ -1,29 +1,30 @@
|
|||||||
module github.com/qdm12/gluetun
|
module github.com/qdm12/gluetun
|
||||||
|
|
||||||
go 1.25.0
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/breml/rootcerts v0.3.3
|
github.com/breml/rootcerts v0.2.17
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.17.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.9
|
||||||
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.2
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9
|
github.com/qdm12/dns/v2 v2.0.0-rc6
|
||||||
github.com/qdm12/gosettings v0.4.4
|
github.com/qdm12/goservices v0.1.0
|
||||||
|
github.com/qdm12/gosettings v0.4.2
|
||||||
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.9.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-20231110203233-9a3e6036ecaa
|
||||||
golang.org/x/net v0.46.0
|
golang.org/x/net v0.28.0
|
||||||
golang.org/x/sys v0.37.0
|
golang.org/x/sys v0.24.0
|
||||||
golang.org/x/text v0.30.0
|
golang.org/x/text v0.17.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
|
||||||
@@ -31,32 +32,32 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
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.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/miekg/dns v1.1.55 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
github.com/prometheus/common v0.60.1 // indirect
|
github.com/prometheus/common v0.42.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.10.1 // indirect
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/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.43.0 // indirect
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
golang.org/x/mod v0.28.0 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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.33.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 // indirect
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 // indirect
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
139
go.sum
139
go.sum
@@ -1,23 +1,30 @@
|
|||||||
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.17 h1:0/M2BE2Apw0qEJCXDOkaiu7d5Sx5ObNfe1BkImJ4u1I=
|
||||||
github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
github.com/breml/rootcerts v0.2.17/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
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.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
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=
|
||||||
@@ -29,36 +36,36 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9 h1:qDzRkHr6993jknNB/ZOCnZOyIG6bsZcl2MIfdeUd0kI=
|
github.com/qdm12/dns/v2 v2.0.0-rc6 h1:h5KpuqZ3IMoSbz2a0OkHzIVc9/jk2vuIm9RoKJuaI78=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
github.com/qdm12/dns/v2 v2.0.0-rc6/go.mod h1:Oh34IJIG55BgHoACOf+cgZCgDiFuiJZ6r6gQW58FN+k=
|
||||||
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.2 h1:Gb39NScPr7OQV+oy0o1OD7A121udITDJuUGa7ljDF58=
|
||||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
github.com/qdm12/gosettings v0.4.2/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||||
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||||
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||||
@@ -71,38 +78,46 @@ github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE
|
|||||||
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
|
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||||
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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
|
||||||
|
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a 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=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||||
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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-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=
|
||||||
@@ -112,22 +127,23 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.24.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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
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-20191204190536-9bdfabe68543/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=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
@@ -135,18 +151,21 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI
|
|||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
||||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 h1:N0m3tKYbkRMmDobh/47ngz+AWeV7PcfXMDi8xu3Vrag=
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69/go.mod h1:Tk5Ip2TuxaWGpccL7//rAsLRH6RQ/jfqTGxuN/+i/FQ=
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 h1:IdrOs1ZgwGw5CI+BH6GgVVlOt+LAXoPyh7enr8lfaXs=
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
allSettings.SetDefaults()
|
|
||||||
|
|
||||||
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrCommandEmpty = errors.New("command is empty")
|
|
||||||
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
|
||||||
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
|
||||||
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Split splits a command string into a slice of arguments.
|
|
||||||
// This is especially important for commands such as:
|
|
||||||
// /bin/sh -c "echo hello"
|
|
||||||
// which should be split into: ["/bin/sh", "-c", "echo hello"]
|
|
||||||
// It supports backslash-escapes, single-quotes and double-quotes.
|
|
||||||
// It does not support:
|
|
||||||
// - the $" quoting style.
|
|
||||||
// - expansion (brace, shell or pathname).
|
|
||||||
func Split(command string) (words []string, err error) {
|
|
||||||
if command == "" {
|
|
||||||
return nil, fmt.Errorf("%w", ErrCommandEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
const bufferSize = 1024
|
|
||||||
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
|
||||||
|
|
||||||
startIndex := 0
|
|
||||||
|
|
||||||
for startIndex < len(command) {
|
|
||||||
// skip any split characters at the start
|
|
||||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
|
|
||||||
switch {
|
|
||||||
case strings.ContainsRune(" \n\t", character):
|
|
||||||
startIndex += runeSize
|
|
||||||
case character == '\\':
|
|
||||||
// Look ahead to eventually skip an escaped newline
|
|
||||||
if command[startIndex+runeSize:] == "" {
|
|
||||||
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
|
|
||||||
}
|
|
||||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
|
|
||||||
if character == '\n' {
|
|
||||||
startIndex += runeSize + runeSize // backslash and newline
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
var word string
|
|
||||||
buffer.Reset()
|
|
||||||
word, startIndex, err = splitWord(command, startIndex, buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
|
|
||||||
}
|
|
||||||
words = append(words, word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return words, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WARNING: buffer must be cleared before calling this function.
|
|
||||||
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
|
|
||||||
word string, newStartIndex int, err error,
|
|
||||||
) {
|
|
||||||
cursor := startIndex
|
|
||||||
for cursor < len(input) {
|
|
||||||
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
|
|
||||||
cursor += runeLength
|
|
||||||
if character == '"' ||
|
|
||||||
character == '\'' ||
|
|
||||||
character == '\\' ||
|
|
||||||
character == ' ' ||
|
|
||||||
character == '\n' ||
|
|
||||||
character == '\t' {
|
|
||||||
buffer.WriteString(input[startIndex : cursor-runeLength])
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.ContainsRune(" \n\t", character): // spacing character
|
|
||||||
return buffer.String(), cursor, nil
|
|
||||||
case character == '"':
|
|
||||||
return handleDoubleQuoted(input, cursor, buffer)
|
|
||||||
case character == '\'':
|
|
||||||
return handleSingleQuoted(input, cursor, buffer)
|
|
||||||
case character == '\\':
|
|
||||||
return handleEscaped(input, cursor, buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.WriteString(input[startIndex:])
|
|
||||||
return buffer.String(), len(input), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
|
||||||
word string, newStartIndex int, err error,
|
|
||||||
) {
|
|
||||||
cursor := startIndex
|
|
||||||
for cursor < len(input) {
|
|
||||||
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
|
||||||
cursor += nextRuneLength
|
|
||||||
switch nextCharacter {
|
|
||||||
case '"': // end of the double quoted string
|
|
||||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
|
|
||||||
return splitWord(input, cursor, buffer)
|
|
||||||
case '\\': // escaped character
|
|
||||||
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
|
||||||
cursor += escapedRuneLength
|
|
||||||
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
|
|
||||||
if escapedCharacter != '\n' {
|
|
||||||
// skip backslash entirely for the newline character
|
|
||||||
buffer.WriteRune(escapedCharacter)
|
|
||||||
}
|
|
||||||
startIndex = cursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
|
||||||
word string, newStartIndex int, err error,
|
|
||||||
) {
|
|
||||||
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
|
|
||||||
if closingQuoteIndex == -1 {
|
|
||||||
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
|
|
||||||
}
|
|
||||||
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
|
|
||||||
const singleQuoteRuneLength = 1
|
|
||||||
startIndex += closingQuoteIndex + singleQuoteRuneLength
|
|
||||||
return splitWord(input, startIndex, buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
|
|
||||||
word string, newStartIndex int, err error,
|
|
||||||
) {
|
|
||||||
if input[startIndex:] == "" {
|
|
||||||
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
|
|
||||||
}
|
|
||||||
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
|
|
||||||
if character != '\n' { // backslash-escaped newline is ignored
|
|
||||||
buffer.WriteString(input[startIndex : startIndex+runeLength])
|
|
||||||
}
|
|
||||||
startIndex += runeLength
|
|
||||||
return splitWord(input, startIndex, buffer)
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Split(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
command string
|
|
||||||
words []string
|
|
||||||
errWrapped error
|
|
||||||
errMessage string
|
|
||||||
}{
|
|
||||||
"empty": {
|
|
||||||
command: "",
|
|
||||||
errWrapped: ErrCommandEmpty,
|
|
||||||
errMessage: "command is empty",
|
|
||||||
},
|
|
||||||
"concrete_sh_command": {
|
|
||||||
command: `/bin/sh -c "echo 123"`,
|
|
||||||
words: []string{"/bin/sh", "-c", "echo 123"},
|
|
||||||
},
|
|
||||||
"single_word": {
|
|
||||||
command: "word1",
|
|
||||||
words: []string{"word1"},
|
|
||||||
},
|
|
||||||
"two_words_single_space": {
|
|
||||||
command: "word1 word2",
|
|
||||||
words: []string{"word1", "word2"},
|
|
||||||
},
|
|
||||||
"two_words_multiple_space": {
|
|
||||||
command: "word1 word2",
|
|
||||||
words: []string{"word1", "word2"},
|
|
||||||
},
|
|
||||||
"two_words_no_expansion": {
|
|
||||||
command: "word1* word2?",
|
|
||||||
words: []string{"word1*", "word2?"},
|
|
||||||
},
|
|
||||||
"escaped_single quote": {
|
|
||||||
command: "ain\\'t good",
|
|
||||||
words: []string{"ain't", "good"},
|
|
||||||
},
|
|
||||||
"escaped_single_quote_all_single_quoted": {
|
|
||||||
command: "'ain'\\''t good'",
|
|
||||||
words: []string{"ain't good"},
|
|
||||||
},
|
|
||||||
"empty_single_quoted": {
|
|
||||||
command: "word1 '' word2",
|
|
||||||
words: []string{"word1", "", "word2"},
|
|
||||||
},
|
|
||||||
"escaped_newline": {
|
|
||||||
command: "word1\\\nword2",
|
|
||||||
words: []string{"word1word2"},
|
|
||||||
},
|
|
||||||
"quoted_newline": {
|
|
||||||
command: "text \"with\na\" quoted newline",
|
|
||||||
words: []string{"text", "with\na", "quoted", "newline"},
|
|
||||||
},
|
|
||||||
"quoted_escaped_newline": {
|
|
||||||
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
|
|
||||||
words: []string{"word1\\d\\\" word2word3 word4"},
|
|
||||||
},
|
|
||||||
"escaped_separated_newline": {
|
|
||||||
command: "word1 \\\n word2",
|
|
||||||
words: []string{"word1", "word2"},
|
|
||||||
},
|
|
||||||
"double_quotes_no_spacing": {
|
|
||||||
command: "word1\"word2\"word3",
|
|
||||||
words: []string{"word1word2word3"},
|
|
||||||
},
|
|
||||||
"unterminated_single_quote": {
|
|
||||||
command: "'abc'\\''def",
|
|
||||||
errWrapped: ErrSingleQuoteUnterminated,
|
|
||||||
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
|
||||||
},
|
|
||||||
"unterminated_double_quote": {
|
|
||||||
command: "\"abc'def",
|
|
||||||
errWrapped: ErrDoubleQuoteUnterminated,
|
|
||||||
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
|
||||||
},
|
|
||||||
"unterminated_escape": {
|
|
||||||
command: "abc\\",
|
|
||||||
errWrapped: ErrEscapeUnterminated,
|
|
||||||
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
|
||||||
},
|
|
||||||
"unterminated_escape_only": {
|
|
||||||
command: " \\",
|
|
||||||
errWrapped: ErrEscapeUnterminated,
|
|
||||||
errMessage: `unterminated backslash-escape: " \\"`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
words, err := Split(testCase.command)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.words, words)
|
|
||||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
|
||||||
if testCase.errWrapped != nil {
|
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,11 +9,9 @@ import (
|
|||||||
|
|
||||||
func readObsolete(r *reader.Reader) (warnings []string) {
|
func readObsolete(r *reader.Reader) (warnings []string) {
|
||||||
keyToMessage := map[string]string{
|
keyToMessage := map[string]string{
|
||||||
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
|
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
|
||||||
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
|
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
|
||||||
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
|
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
|
||||||
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
|
|
||||||
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
|
|
||||||
}
|
}
|
||||||
sortedKeys := maps.Keys(keyToMessage)
|
sortedKeys := maps.Keys(keyToMessage)
|
||||||
slices.Sort(sortedKeys)
|
slices.Sort(sortedKeys)
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/provider"
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
@@ -15,31 +11,10 @@ import (
|
|||||||
|
|
||||||
// DNS contains settings to configure DNS.
|
// DNS contains settings to configure DNS.
|
||||||
type DNS struct {
|
type DNS struct {
|
||||||
// ServerEnabled is true if the server should be running
|
|
||||||
// and used. It defaults to true, and cannot be nil
|
|
||||||
// in the internal state.
|
|
||||||
ServerEnabled *bool
|
|
||||||
// UpstreamType can be dot or plain, and defaults to dot.
|
|
||||||
UpstreamType string `json:"upstream_type"`
|
|
||||||
// UpdatePeriod is the period to update DNS block lists.
|
|
||||||
// It can be set to 0 to disable the update.
|
|
||||||
// It defaults to 24h and cannot be nil in
|
|
||||||
// the internal state.
|
|
||||||
UpdatePeriod *time.Duration
|
|
||||||
// Providers is a list of DNS providers
|
|
||||||
Providers []string `json:"providers"`
|
|
||||||
// Caching is true if the server should cache
|
|
||||||
// DNS responses.
|
|
||||||
Caching *bool `json:"caching"`
|
|
||||||
// IPv6 is true if the server should connect over IPv6.
|
|
||||||
IPv6 *bool `json:"ipv6"`
|
|
||||||
// Blacklist contains settings to configure the filter
|
|
||||||
// block lists.
|
|
||||||
Blacklist DNSBlacklist
|
|
||||||
// ServerAddress is the DNS server to use inside
|
// ServerAddress is the DNS server to use inside
|
||||||
// the Go program and for the system.
|
// the Go program and for the system.
|
||||||
// It defaults to '127.0.0.1' to be used with the
|
// It defaults to '127.0.0.1' to be used with the
|
||||||
// local server. It cannot be the zero value in the internal
|
// DoT server. It cannot be the zero value in the internal
|
||||||
// state.
|
// state.
|
||||||
ServerAddress netip.Addr
|
ServerAddress netip.Addr
|
||||||
// KeepNameserver is true if the existing DNS server
|
// KeepNameserver is true if the existing DNS server
|
||||||
@@ -48,40 +23,20 @@ type DNS struct {
|
|||||||
// outside the VPN tunnel since it would go through
|
// outside the VPN tunnel since it would go through
|
||||||
// the local DNS server of your Docker/Kubernetes
|
// the local DNS server of your Docker/Kubernetes
|
||||||
// configuration, which is likely not going through the tunnel.
|
// configuration, which is likely not going through the tunnel.
|
||||||
// This will also disable the DNS forwarder server and the
|
// This will also disable the DNS over TLS server and the
|
||||||
// `ServerAddress` field will be ignored.
|
// `ServerAddress` field will be ignored.
|
||||||
// It defaults to false and cannot be nil in the
|
// It defaults to false and cannot be nil in the
|
||||||
// internal state.
|
// internal state.
|
||||||
KeepNameserver *bool
|
KeepNameserver *bool
|
||||||
|
// DOT contains settings to configure the DoT
|
||||||
|
// server.
|
||||||
|
DoT DoT
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
|
|
||||||
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d DNS) validate() (err error) {
|
func (d DNS) validate() (err error) {
|
||||||
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
|
err = d.DoT.validate()
|
||||||
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
|
|
||||||
}
|
|
||||||
|
|
||||||
const minUpdatePeriod = 30 * time.Second
|
|
||||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
|
||||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
|
||||||
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
providers := provider.NewProviders()
|
|
||||||
for _, providerName := range d.Providers {
|
|
||||||
_, err := providers.Get(providerName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Blacklist.validate()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("validating DoT settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -89,15 +44,9 @@ func (d DNS) validate() (err error) {
|
|||||||
|
|
||||||
func (d *DNS) Copy() (copied DNS) {
|
func (d *DNS) Copy() (copied DNS) {
|
||||||
return DNS{
|
return DNS{
|
||||||
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
|
|
||||||
UpstreamType: d.UpstreamType,
|
|
||||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
|
||||||
Providers: gosettings.CopySlice(d.Providers),
|
|
||||||
Caching: gosettings.CopyPointer(d.Caching),
|
|
||||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
|
||||||
Blacklist: d.Blacklist.copy(),
|
|
||||||
ServerAddress: d.ServerAddress,
|
ServerAddress: d.ServerAddress,
|
||||||
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||||
|
DoT: d.DoT.copy(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,48 +54,16 @@ func (d *DNS) Copy() (copied DNS) {
|
|||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (d *DNS) overrideWith(other DNS) {
|
func (d *DNS) overrideWith(other DNS) {
|
||||||
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
|
|
||||||
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
|
|
||||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
|
||||||
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
|
||||||
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
|
||||||
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
|
||||||
d.Blacklist.overrideWith(other.Blacklist)
|
|
||||||
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||||
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||||
|
d.DoT.overrideWith(other.DoT)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) setDefaults() {
|
func (d *DNS) setDefaults() {
|
||||||
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
|
|
||||||
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
|
|
||||||
const defaultUpdatePeriod = 24 * time.Hour
|
|
||||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
|
||||||
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
|
||||||
provider.Cloudflare().Name,
|
|
||||||
})
|
|
||||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
|
||||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
|
||||||
d.Blacklist.setDefaults()
|
|
||||||
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
|
|
||||||
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
|
|
||||||
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
|
||||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||||
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
|
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
||||||
return d.ServerAddress
|
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||||
}
|
d.DoT.setDefaults()
|
||||||
|
|
||||||
providers := provider.NewProviders()
|
|
||||||
provider, err := providers.Get(d.Providers[0])
|
|
||||||
if err != nil {
|
|
||||||
// Settings should be validated before calling this function,
|
|
||||||
// so an error happening here is a programming error.
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.Plain.IPv4[0].Addr()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DNS) String() string {
|
func (d DNS) String() string {
|
||||||
@@ -160,63 +77,11 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||||
|
node.AppendNode(d.DoT.toLinesNode())
|
||||||
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled))
|
|
||||||
if !*d.ServerEnabled {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
|
|
||||||
|
|
||||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
|
||||||
for _, provider := range d.Providers {
|
|
||||||
upstreamResolvers.Append(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
|
||||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
|
||||||
|
|
||||||
update := "disabled"
|
|
||||||
if *d.UpdatePeriod > 0 {
|
|
||||||
update = "every " + d.UpdatePeriod.String()
|
|
||||||
}
|
|
||||||
node.Appendf("Update period: %s", update)
|
|
||||||
|
|
||||||
node.AppendNode(d.Blacklist.toLinesNode())
|
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) read(r *reader.Reader) (err error) {
|
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||||
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
|
||||||
|
|
||||||
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
|
|
||||||
|
|
||||||
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Blacklist.read(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -227,5 +92,10 @@ func (d *DNS) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = d.DoT.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DNS over TLS settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,45 +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
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -25,16 +24,16 @@ type Health struct {
|
|||||||
// HTTP server. It defaults to 500 milliseconds.
|
// HTTP server. It defaults to 500 milliseconds.
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
// TargetAddress is the address (host or host:port)
|
// TargetAddress is the address (host or host:port)
|
||||||
// to TCP TLS dial to periodically for the health check.
|
// to TCP dial to periodically for the health check.
|
||||||
// It cannot be the empty string in the internal state.
|
// It cannot be the empty string in the internal state.
|
||||||
TargetAddress string
|
TargetAddress string
|
||||||
// ICMPTargetIP is the IP address to use for ICMP echo requests
|
// SuccessWait is the duration to wait to re-run the
|
||||||
// in the health checker. It can be set to an unspecified address (0.0.0.0)
|
// healthcheck after a successful healthcheck.
|
||||||
// such that the VPN server IP is used, which is also the default behavior.
|
// It defaults to 5 seconds and cannot be zero in
|
||||||
ICMPTargetIP netip.Addr
|
// the internal state.
|
||||||
// RestartVPN indicates whether to restart the VPN connection
|
SuccessWait time.Duration
|
||||||
// when the healthcheck fails.
|
// VPN has health settings specific to the VPN loop.
|
||||||
RestartVPN *bool
|
VPN HealthyWait
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Health) Validate() (err error) {
|
func (h Health) Validate() (err error) {
|
||||||
@@ -43,6 +42,11 @@ func (h Health) Validate() (err error) {
|
|||||||
return fmt.Errorf("server listening address is not valid: %w", err)
|
return fmt.Errorf("server listening address is not valid: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = h.VPN.validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("health VPN settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +56,8 @@ func (h *Health) copy() (copied Health) {
|
|||||||
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||||
ReadTimeout: h.ReadTimeout,
|
ReadTimeout: h.ReadTimeout,
|
||||||
TargetAddress: h.TargetAddress,
|
TargetAddress: h.TargetAddress,
|
||||||
ICMPTargetIP: h.ICMPTargetIP,
|
SuccessWait: h.SuccessWait,
|
||||||
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
VPN: h.VPN.copy(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +69,8 @@ func (h *Health) OverrideWith(other Health) {
|
|||||||
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||||
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
||||||
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
||||||
h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
|
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
|
||||||
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
|
h.VPN.overrideWith(other.VPN)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Health) SetDefaults() {
|
func (h *Health) SetDefaults() {
|
||||||
@@ -76,8 +80,9 @@ func (h *Health) SetDefaults() {
|
|||||||
const defaultReadTimeout = 500 * time.Millisecond
|
const defaultReadTimeout = 500 * time.Millisecond
|
||||||
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
||||||
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
||||||
h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
|
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 {
|
||||||
@@ -88,12 +93,10 @@ 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)
|
||||||
node.Appendf("Target address: %s", h.TargetAddress)
|
node.Appendf("Target address: %s", h.TargetAddress)
|
||||||
icmpTarget := "VPN server IP"
|
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
|
||||||
if !h.ICMPTargetIP.IsUnspecified() {
|
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
||||||
icmpTarget = h.ICMPTargetIP.String()
|
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
||||||
}
|
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
||||||
node.Appendf("ICMP target IP: %s", icmpTarget)
|
|
||||||
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,13 +104,16 @@ func (h *Health) Read(r *reader.Reader) (err error) {
|
|||||||
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
||||||
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
|
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
|
||||||
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
|
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
|
||||||
h.ICMPTargetIP, err = r.NetipAddr("HEALTH_ICMP_TARGET_IP")
|
|
||||||
|
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -29,14 +29,6 @@ type PortForwarding struct {
|
|||||||
// to write to a file. It cannot be nil for the
|
// to write to a file. It cannot be nil for the
|
||||||
// internal state
|
// internal state
|
||||||
Filepath *string `json:"status_file_path"`
|
Filepath *string `json:"status_file_path"`
|
||||||
// UpCommand is the command to use when the port forwarding is up.
|
|
||||||
// It can be the empty string to indicate not to run a command.
|
|
||||||
// It cannot be nil in the internal state.
|
|
||||||
UpCommand *string `json:"up_command"`
|
|
||||||
// DownCommand is the command to use after the port forwarding goes down.
|
|
||||||
// It can be the empty string to indicate to NOT run a command.
|
|
||||||
// It cannot be nil in the internal state.
|
|
||||||
DownCommand *string `json:"down_command"`
|
|
||||||
// ListeningPort is the port traffic would be redirected to from the
|
// ListeningPort is the port traffic would be redirected to from the
|
||||||
// forwarded port. The redirection is disabled if it is set to 0, which
|
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||||
// is its default as well.
|
// is its default as well.
|
||||||
@@ -92,8 +84,6 @@ func (p *PortForwarding) Copy() (copied PortForwarding) {
|
|||||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
Provider: gosettings.CopyPointer(p.Provider),
|
Provider: gosettings.CopyPointer(p.Provider),
|
||||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||||
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
|
||||||
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
|
||||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||||
Username: p.Username,
|
Username: p.Username,
|
||||||
Password: p.Password,
|
Password: p.Password,
|
||||||
@@ -104,8 +94,6 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
|||||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||||
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||||
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
|
||||||
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
|
||||||
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
||||||
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||||
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||||
@@ -115,8 +103,6 @@ func (p *PortForwarding) setDefaults() {
|
|||||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||||
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
||||||
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||||
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
|
||||||
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
|
||||||
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,13 +135,6 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
|||||||
}
|
}
|
||||||
node.Appendf("Forwarded port file path: %s", filepath)
|
node.Appendf("Forwarded port file path: %s", filepath)
|
||||||
|
|
||||||
if *p.UpCommand != "" {
|
|
||||||
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
|
|
||||||
}
|
|
||||||
if *p.DownCommand != "" {
|
|
||||||
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Username != "" {
|
if p.Username != "" {
|
||||||
credentialsNode := node.Appendf("Credentials:")
|
credentialsNode := node.Appendf("Credentials:")
|
||||||
credentialsNode.Appendf("Username: %s", p.Username)
|
credentialsNode.Appendf("Username: %s", p.Username)
|
||||||
@@ -184,12 +163,6 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
|
|||||||
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
||||||
))
|
))
|
||||||
|
|
||||||
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
|
|
||||||
reader.ForceLowercase(false))
|
|
||||||
|
|
||||||
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
|
|
||||||
reader.ForceLowercase(false))
|
|
||||||
|
|
||||||
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -17,7 +17,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.
|
||||||
@@ -354,8 +354,11 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled
|
|||||||
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
||||||
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
||||||
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
||||||
defaultPortForwardOnly := portForwardingEnabled &&
|
defaultPortForwardOnly := false
|
||||||
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
|
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
|
||||||
|
providers.PrivateInternetAccess, providers.Protonvpn) {
|
||||||
|
defaultPortForwardOnly = true
|
||||||
|
}
|
||||||
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
||||||
ss.OpenVPN.setDefaults(vpnProvider)
|
ss.OpenVPN.setDefaults(vpnProvider)
|
||||||
ss.Wireguard.setDefaults()
|
ss.Wireguard.setDefaults()
|
||||||
|
|||||||
@@ -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,17 +40,17 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
├── DNS settings:
|
├── DNS settings:
|
||||||
| ├── Keep existing nameserver(s): no
|
| ├── Keep existing nameserver(s): no
|
||||||
| ├── DNS server address to use: 127.0.0.1
|
| ├── DNS server address to use: 127.0.0.1
|
||||||
| ├── DNS forwarder server enabled: yes
|
| └── DNS over TLS settings:
|
||||||
| ├── Upstream resolver type: dot
|
| ├── Enabled: yes
|
||||||
| ├── Upstream resolvers:
|
| ├── Update period: every 24h0m0s
|
||||||
| | └── Cloudflare
|
| ├── Upstream resolvers:
|
||||||
| ├── Caching: yes
|
| | └── Cloudflare
|
||||||
| ├── IPv6: no
|
| ├── Caching: yes
|
||||||
| ├── Update period: every 24h0m0s
|
| ├── IPv6: no
|
||||||
| └── DNS filtering settings:
|
| └── DNS filtering settings:
|
||||||
| ├── Block malicious: yes
|
| ├── Block malicious: yes
|
||||||
| ├── Block ads: no
|
| ├── Block ads: no
|
||||||
| └── Block surveillance: yes
|
| └── Block surveillance: yes
|
||||||
├── Firewall settings:
|
├── Firewall settings:
|
||||||
| └── Enabled: yes
|
| └── Enabled: yes
|
||||||
├── Log settings:
|
├── Log settings:
|
||||||
@@ -58,8 +58,12 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
├── Health settings:
|
├── Health settings:
|
||||||
| ├── Server listening address: 127.0.0.1:9999
|
| ├── Server listening address: 127.0.0.1:9999
|
||||||
| ├── Target address: cloudflare.com:443
|
| ├── Target address: cloudflare.com:443
|
||||||
| ├── ICMP target IP: VPN server IP
|
| ├── Duration to wait after success: 5s
|
||||||
| └── Restart VPN on healthcheck failure: yes
|
| ├── Read header timeout: 100ms
|
||||||
|
| ├── Read timeout: 500ms
|
||||||
|
| └── VPN wait durations:
|
||||||
|
| ├── Initial duration: 6s
|
||||||
|
| └── Additional duration: 5s
|
||||||
├── Shadowsocks server settings:
|
├── Shadowsocks server settings:
|
||||||
| └── Enabled: no
|
| └── Enabled: no
|
||||||
├── HTTP proxy settings:
|
├── HTTP proxy settings:
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ 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) {
|
||||||
return s.Settings.Validate()
|
return s.Settings.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Shadowsocks) copy() (copied Shadowsocks) {
|
func (s *Shadowsocks) Copy() (copied Shadowsocks) {
|
||||||
return Shadowsocks{
|
return Shadowsocks{
|
||||||
Enabled: gosettings.CopyPointer(s.Enabled),
|
Enabled: gosettings.CopyPointer(s.Enabled),
|
||||||
Settings: s.Settings.Copy(),
|
Settings: s.Settings.Copy(),
|
||||||
@@ -32,7 +32,7 @@ func (s *Shadowsocks) copy() (copied Shadowsocks) {
|
|||||||
// overrideWith overrides fields of the receiver
|
// overrideWith overrides fields of the receiver
|
||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (s *Shadowsocks) overrideWith(other Shadowsocks) {
|
func (s *Shadowsocks) OverrideWith(other Shadowsocks) {
|
||||||
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, other.Enabled)
|
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, other.Enabled)
|
||||||
s.Settings.OverrideWith(other.Settings)
|
s.Settings.OverrideWith(other.Settings)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,13 +39,9 @@ type Wireguard struct {
|
|||||||
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
|
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
|
||||||
// Maximum Transmission Unit (MTU) of the Wireguard interface.
|
// Maximum Transmission Unit (MTU) of the Wireguard interface.
|
||||||
// It cannot be zero in the internal state, and defaults to
|
// It cannot be zero in the internal state, and defaults to
|
||||||
// 1320. Note it is not the wireguard-go MTU default of 1420
|
// 1400. Note it is not the wireguard-go MTU default of 1420
|
||||||
// because this impacts bandwidth a lot on some VPN providers,
|
// because this impacts bandwidth a lot on some VPN providers,
|
||||||
// see https://github.com/qdm12/gluetun/issues/1650.
|
// see https://github.com/qdm12/gluetun/issues/1650.
|
||||||
// It has been lowered to 1320 following quite a bit of
|
|
||||||
// investigation in the issue:
|
|
||||||
// https://github.com/qdm12/gluetun/issues/2533.
|
|
||||||
// Note this should now be replaced with the PMTUD feature.
|
|
||||||
MTU uint16 `json:"mtu"`
|
MTU uint16 `json:"mtu"`
|
||||||
// Implementation is the Wireguard implementation to use.
|
// Implementation is the Wireguard implementation to use.
|
||||||
// It can be "auto", "userspace" or "kernelspace".
|
// It can be "auto", "userspace" or "kernelspace".
|
||||||
@@ -195,7 +191,7 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
|
|||||||
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
|
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
|
||||||
w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
|
w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
|
||||||
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
|
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
|
||||||
const defaultMTU = 1320
|
const defaultMTU = 1400
|
||||||
w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
|
w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
|
||||||
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
|
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/dot"
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||||
"github.com/qdm12/dns/v2/pkg/server"
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/dns/state"
|
"github.com/qdm12/gluetun/internal/dns/state"
|
||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
type Loop struct {
|
type Loop struct {
|
||||||
statusManager *loopstate.State
|
statusManager *loopstate.State
|
||||||
state *state.State
|
state *state.State
|
||||||
server *server.Server
|
server *dot.Server
|
||||||
filter *mapfilter.Filter
|
filter *mapfilter.Filter
|
||||||
resolvConf string
|
resolvConf string
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -26,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 {
|
||||||
@@ -56,7 +56,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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/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/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,82 +21,55 @@ 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, logger Logger) (
|
filter *mapfilter.Filter, logger Logger) (
|
||||||
serverSettings server.Settings, err error,
|
dotSettings dot.ServerSettings, err error,
|
||||||
) {
|
) {
|
||||||
serverSettings.Logger = logger
|
var middlewares []dot.Middleware
|
||||||
|
|
||||||
providersData := provider.NewProviders()
|
if *settings.DoT.Caching {
|
||||||
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
|
|
||||||
for i := range settings.Providers {
|
|
||||||
var err error
|
|
||||||
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // this should already had been checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ipVersion := "ipv4"
|
|
||||||
if *settings.IPv6 {
|
|
||||||
ipVersion = "ipv6"
|
|
||||||
}
|
|
||||||
|
|
||||||
var dialer server.Dialer
|
|
||||||
switch settings.UpstreamType {
|
|
||||||
case "dot":
|
|
||||||
dialerSettings := dot.Settings{
|
|
||||||
UpstreamResolvers: upstreamResolvers,
|
|
||||||
IPVersion: ipVersion,
|
|
||||||
}
|
|
||||||
dialer, err = dot.New(dialerSettings)
|
|
||||||
if err != nil {
|
|
||||||
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
|
||||||
}
|
|
||||||
case "doh":
|
|
||||||
dialerSettings := doh.Settings{
|
|
||||||
UpstreamResolvers: upstreamResolvers,
|
|
||||||
IPVersion: ipVersion,
|
|
||||||
}
|
|
||||||
dialer, err = doh.New(dialerSettings)
|
|
||||||
if err != nil {
|
|
||||||
return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err)
|
|
||||||
}
|
|
||||||
case "plain":
|
|
||||||
dialerSettings := plain.Settings{
|
|
||||||
UpstreamResolvers: upstreamResolvers,
|
|
||||||
IPVersion: ipVersion,
|
|
||||||
}
|
|
||||||
dialer, err = plain.New(dialerSettings)
|
|
||||||
if err != nil {
|
|
||||||
return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("unknown upstream type: " + settings.UpstreamType)
|
|
||||||
}
|
|
||||||
serverSettings.Dialer = dialer
|
|
||||||
|
|
||||||
if *settings.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 dot.ServerSettings{}, fmt.Errorf("creating LRU cache: %w", err)
|
||||||
}
|
}
|
||||||
cacheMiddleware, err := cachemiddleware.New(cachemiddleware.Settings{
|
cacheMiddleware, err := cachemiddleware.New(cachemiddleware.Settings{
|
||||||
Cache: lruCache,
|
Cache: lruCache,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return server.Settings{}, fmt.Errorf("creating cache middleware: %w", err)
|
return dot.ServerSettings{}, fmt.Errorf("creating cache middleware: %w", err)
|
||||||
}
|
}
|
||||||
serverSettings.Middlewares = append(serverSettings.Middlewares, cacheMiddleware)
|
middlewares = append(middlewares, cacheMiddleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
filterMiddleware, err := filtermiddleware.New(filtermiddleware.Settings{
|
filterMiddleware, err := filtermiddleware.New(filtermiddleware.Settings{
|
||||||
Filter: filter,
|
Filter: filter,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return server.Settings{}, fmt.Errorf("creating filter middleware: %w", err)
|
return dot.ServerSettings{}, fmt.Errorf("creating filter middleware: %w", err)
|
||||||
}
|
}
|
||||||
serverSettings.Middlewares = append(serverSettings.Middlewares, filterMiddleware)
|
middlewares = append(middlewares, filterMiddleware)
|
||||||
|
|
||||||
return serverSettings, nil
|
providersData := provider.NewProviders()
|
||||||
|
providers := make([]provider.Provider, len(settings.DoT.Providers))
|
||||||
|
for i := range settings.DoT.Providers {
|
||||||
|
var err error
|
||||||
|
providers[i], err = providersData.Get(settings.DoT.Providers[i])
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // this should already had been checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipVersion := "ipv4"
|
||||||
|
if *settings.DoT.IPv6 {
|
||||||
|
ipVersion = "ipv6"
|
||||||
|
}
|
||||||
|
return dot.ServerSettings{
|
||||||
|
Resolver: dot.ResolverSettings{
|
||||||
|
UpstreamResolvers: providers,
|
||||||
|
IPVersion: ipVersion,
|
||||||
|
Warner: logger,
|
||||||
|
},
|
||||||
|
Middlewares: middlewares,
|
||||||
|
Logger: logger,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ 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/dot"
|
||||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||||
"github.com/qdm12/dns/v2/pkg/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var errUpdateBlockLists = errors.New("cannot update filter block lists")
|
var errUpdateBlockLists = errors.New("cannot update filter block lists")
|
||||||
@@ -21,29 +20,28 @@ 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.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 := dot.NewServer(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()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("starting server: %w", err)
|
return nil, fmt.Errorf("starting server: %w", err)
|
||||||
}
|
}
|
||||||
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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type chainRule struct {
|
|||||||
packets uint64
|
packets uint64
|
||||||
bytes uint64
|
bytes uint64
|
||||||
target string // "ACCEPT", "DROP", "REJECT" or "REDIRECT"
|
target string // "ACCEPT", "DROP", "REJECT" or "REDIRECT"
|
||||||
protocol string // "icmp", "tcp", "udp" or "" for all protocols.
|
protocol string // "tcp", "udp" or "" for all protocols.
|
||||||
inputInterface string // input interface, for example "tun0" or "*""
|
inputInterface string // input interface, for example "tun0" or "*""
|
||||||
outputInterface string // output interface, for example "eth0" or "*""
|
outputInterface string // output interface, for example "eth0" or "*""
|
||||||
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
|
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
|
||||||
@@ -323,12 +323,10 @@ 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 "6":
|
||||||
protocol = "icmp"
|
|
||||||
case "6", "tcp":
|
|
||||||
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)
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ num pkts bytes target prot opt in out source destinati
|
|||||||
num pkts bytes target prot opt in out source destination
|
num pkts bytes target prot opt in out source destination
|
||||||
1 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
|
1 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
|
||||||
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
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 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",
|
||||||
@@ -94,17 +92,6 @@ num pkts bytes target prot opt in out source destinati
|
|||||||
lineNumber: 3,
|
lineNumber: 3,
|
||||||
packets: 0,
|
packets: 0,
|
||||||
bytes: 0,
|
bytes: 0,
|
||||||
target: "ACCEPT",
|
|
||||||
protocol: "icmp",
|
|
||||||
inputInterface: "tun0",
|
|
||||||
outputInterface: "*",
|
|
||||||
source: netip.MustParsePrefix("0.0.0.0/0"),
|
|
||||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lineNumber: 4,
|
|
||||||
packets: 0,
|
|
||||||
bytes: 0,
|
|
||||||
target: "DROP",
|
target: "DROP",
|
||||||
protocol: "",
|
protocol: "",
|
||||||
inputInterface: "tun0",
|
inputInterface: "tun0",
|
||||||
@@ -112,17 +99,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"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func testIptablesPath(ctx context.Context, path string,
|
|||||||
// Set policy as the existing policy so no mutation is done.
|
// Set policy as the existing policy so no mutation is done.
|
||||||
// This is an extra check for some buggy kernels where setting the policy
|
// This is an extra check for some buggy kernels where setting the policy
|
||||||
// does not work.
|
// does not work.
|
||||||
cmd = exec.CommandContext(ctx, path, "-nL", "INPUT")
|
cmd = exec.CommandContext(ctx, path, "-L", "INPUT")
|
||||||
output, err = runner.Run(cmd)
|
output, err = runner.Run(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
|
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func newDeleteTestRuleMatcher(path string) *cmdMatcher {
|
|||||||
|
|
||||||
func newListInputRulesMatcher(path string) *cmdMatcher {
|
func newListInputRulesMatcher(path string) *cmdMatcher {
|
||||||
return newCmdMatcher(path,
|
return newCmdMatcher(path,
|
||||||
"^-nL$", "^INPUT$")
|
"^-L$", "^INPUT$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSetPolicyMatcher(path, inputPolicy string) *cmdMatcher { //nolint:unparam
|
func newSetPolicyMatcher(path, inputPolicy string) *cmdMatcher { //nolint:unparam
|
||||||
|
|||||||
@@ -1,250 +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 {
|
|
||||||
tlsDialAddr string
|
|
||||||
dialer *net.Dialer
|
|
||||||
echoer *icmp.Echoer
|
|
||||||
dnsClient *dns.Client
|
|
||||||
logger Logger
|
|
||||||
icmpTarget netip.Addr
|
|
||||||
configMutex sync.Mutex
|
|
||||||
|
|
||||||
icmpNotPermitted bool
|
|
||||||
smallCheckName string
|
|
||||||
|
|
||||||
// Internal periodic service signals
|
|
||||||
stop context.CancelFunc
|
|
||||||
done <-chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChecker(logger Logger) *Checker {
|
|
||||||
return &Checker{
|
|
||||||
dialer: &net.Dialer{
|
|
||||||
Resolver: &net.Resolver{
|
|
||||||
PreferGo: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
echoer: icmp.NewEchoer(logger),
|
|
||||||
dnsClient: dns.New(),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetConfig sets the TCP+TLS dial address and the ICMP echo IP address
|
|
||||||
// to target by the [Checker].
|
|
||||||
// This function MUST be called before calling [Checker.Start].
|
|
||||||
func (c *Checker) SetConfig(tlsDialAddr string, icmpTarget netip.Addr) {
|
|
||||||
c.configMutex.Lock()
|
|
||||||
defer c.configMutex.Unlock()
|
|
||||||
c.tlsDialAddr = tlsDialAddr
|
|
||||||
c.icmpTarget = icmpTarget
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the checker by first running a blocking 2s-timed TCP+TLS check,
|
|
||||||
// and, on success, starts the periodic checks in a separate goroutine:
|
|
||||||
// - a "small" ICMP echo check every 15 seconds
|
|
||||||
// - a "full" TCP+TLS check every 5 minutes
|
|
||||||
// It returns a channel `runError` that receives an error (nil or not) when a periodic check is performed.
|
|
||||||
// It returns an error if the initial TCP+TLS check fails.
|
|
||||||
// The Checker has to be ultimately stopped by calling [Checker.Stop].
|
|
||||||
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
|
|
||||||
if c.tlsDialAddr == "" || c.icmpTarget.IsUnspecified() {
|
|
||||||
panic("call Checker.SetConfig with non empty values before Checker.Start")
|
|
||||||
}
|
|
||||||
|
|
||||||
// connection isn't under load yet when the checker starts, so a short
|
|
||||||
// 6 seconds timeout suffices and provides quick enough feedback that
|
|
||||||
// the new connection is not working.
|
|
||||||
const timeout = 6 * time.Second
|
|
||||||
tcpTLSCheckCtx, tcpTLSCheckCancel := context.WithTimeout(ctx, timeout)
|
|
||||||
err = tcpTLSCheck(tcpTLSCheckCtx, c.dialer, c.tlsDialAddr)
|
|
||||||
tcpTLSCheckCancel()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("startup check: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ready := make(chan struct{})
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
c.stop = cancel
|
|
||||||
done := make(chan struct{})
|
|
||||||
c.done = done
|
|
||||||
c.smallCheckName = "ICMP echo"
|
|
||||||
const smallCheckPeriod = time.Minute
|
|
||||||
smallCheckTimer := time.NewTimer(smallCheckPeriod)
|
|
||||||
const fullCheckPeriod = 5 * time.Minute
|
|
||||||
fullCheckTimer := time.NewTimer(fullCheckPeriod)
|
|
||||||
runErrorCh := make(chan error)
|
|
||||||
runError = runErrorCh
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
close(ready)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
fullCheckTimer.Stop()
|
|
||||||
smallCheckTimer.Stop()
|
|
||||||
return
|
|
||||||
case <-smallCheckTimer.C:
|
|
||||||
err := c.smallPeriodicCheck(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("small periodic check: %w", err)
|
|
||||||
}
|
|
||||||
runErrorCh <- err
|
|
||||||
smallCheckTimer.Reset(smallCheckPeriod)
|
|
||||||
case <-fullCheckTimer.C:
|
|
||||||
err := c.fullPeriodicCheck(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("full periodic check: %w", err)
|
|
||||||
}
|
|
||||||
runErrorCh <- err
|
|
||||||
fullCheckTimer.Reset(fullCheckPeriod)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
<-ready
|
|
||||||
return runError, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) Stop() error {
|
|
||||||
c.stop()
|
|
||||||
<-c.done
|
|
||||||
c.icmpTarget = netip.Addr{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
|
|
||||||
c.configMutex.Lock()
|
|
||||||
ip := c.icmpTarget
|
|
||||||
c.configMutex.Unlock()
|
|
||||||
const maxTries = 3
|
|
||||||
const timeout = 10 * time.Second
|
|
||||||
const extraTryTime = 10 * time.Second // 10s added for each subsequent retry
|
|
||||||
check := func(ctx context.Context) error {
|
|
||||||
if c.icmpNotPermitted {
|
|
||||||
return c.dnsClient.Check(ctx)
|
|
||||||
}
|
|
||||||
err := c.echoer.Echo(ctx, ip)
|
|
||||||
if errors.Is(err, icmp.ErrNotPermitted) {
|
|
||||||
c.icmpNotPermitted = true
|
|
||||||
c.smallCheckName = "plain DNS over UDP"
|
|
||||||
c.logger.Infof("%s; permanently falling back to %s checks.", c.smallCheckName, err)
|
|
||||||
return c.dnsClient.Check(ctx)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, c.smallCheckName, check)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Checker) fullPeriodicCheck(ctx context.Context) error {
|
|
||||||
const maxTries = 2
|
|
||||||
// 20s timeout in case the connection is under stress
|
|
||||||
// See https://github.com/qdm12/gluetun/issues/2270
|
|
||||||
const timeout = 20 * time.Second
|
|
||||||
const extraTryTime = 10 * time.Second // 10s added for each subsequent retry
|
|
||||||
check := func(ctx context.Context) error {
|
|
||||||
return tcpTLSCheck(ctx, c.dialer, c.tlsDialAddr)
|
|
||||||
}
|
|
||||||
return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, "TCP+TLS dial", check)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tcpTLSCheck(ctx context.Context, dialer *net.Dialer, targetAddress string) error {
|
|
||||||
// TODO use mullvad API if current provider is Mullvad
|
|
||||||
|
|
||||||
address, err := makeAddressToDial(targetAddress)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialNetwork = "tcp4"
|
|
||||||
connection, err := dialer.DialContext(ctx, dialNetwork, address)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("dialing: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(address, ":443") {
|
|
||||||
host, _, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("splitting host and port: %w", err)
|
|
||||||
}
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
ServerName: host,
|
|
||||||
}
|
|
||||||
tlsConnection := tls.Client(connection, tlsConfig)
|
|
||||||
err = tlsConnection.HandshakeContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("running TLS handshake: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = connection.Close()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("closing connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeAddressToDial(address string) (addressToDial string, err error) {
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
addrErr := new(net.AddrError)
|
|
||||||
ok := errors.As(err, &addrErr)
|
|
||||||
if !ok || addrErr.Err != "missing port in address" {
|
|
||||||
return "", fmt.Errorf("splitting host and port from address: %w", err)
|
|
||||||
}
|
|
||||||
host = address
|
|
||||||
const defaultPort = "443"
|
|
||||||
port = defaultPort
|
|
||||||
}
|
|
||||||
address = net.JoinHostPort(host, port)
|
|
||||||
return address, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrAllCheckTriesFailed = errors.New("all check tries failed")
|
|
||||||
|
|
||||||
func withRetries(ctx context.Context, maxTries uint, tryTimeout, extraTryTime time.Duration,
|
|
||||||
logger Logger, checkName string, check func(ctx context.Context) error,
|
|
||||||
) error {
|
|
||||||
try := uint(0)
|
|
||||||
var errs []error
|
|
||||||
for {
|
|
||||||
timeout := tryTimeout + time.Duration(try)*extraTryTime //nolint:gosec
|
|
||||||
checkCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
err := check(checkCtx)
|
|
||||||
cancel()
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return nil
|
|
||||||
case ctx.Err() != nil:
|
|
||||||
return fmt.Errorf("%s: %w", checkName, ctx.Err())
|
|
||||||
}
|
|
||||||
logger.Debugf("%s attempt %d/%d failed: %s", checkName, try+1, maxTries, err)
|
|
||||||
errs = append(errs, err)
|
|
||||||
try++
|
|
||||||
if try < maxTries {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
errStrings := make([]string, len(errs))
|
|
||||||
for i, err := range errs {
|
|
||||||
errStrings[i] = fmt.Sprintf("attempt %d: %s", i+1, err.Error())
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%w: after %d %s attempts (%s)",
|
|
||||||
ErrAllCheckTriesFailed, maxTries, checkName, strings.Join(errStrings, "; "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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].Addr()
|
|
||||||
resolver := &net.Resolver{
|
|
||||||
PreferGo: true,
|
|
||||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
dialer := net.Dialer{}
|
|
||||||
return dialer.DialContext(ctx, "udp", dnsAddr.String())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ips, err := resolver.LookupIP(ctx, "ip", "github.com")
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
|
||||||
return err
|
|
||||||
case len(ips) == 0:
|
|
||||||
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
|
||||||
return fmt.Errorf("%w", ErrLookupNoIPs)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
104
internal/healthcheck/health.go
Normal file
104
internal/healthcheck/health.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package healthcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,11 +7,12 @@ 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) {
|
||||||
@@ -20,28 +21,26 @@ func Test_Checker_fullcheck(t *testing.T) {
|
|||||||
dialer := &net.Dialer{}
|
dialer := &net.Dialer{}
|
||||||
const address = "cloudflare.com:443"
|
const address = "cloudflare.com:443"
|
||||||
|
|
||||||
checker := &Checker{
|
server := &Server{
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
tlsDialAddr: address,
|
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,
|
||||||
tlsDialAddr: 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: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
receivedData, err := receiveEchoReply(conn, id, i.buffer, ipVersion, i.logger)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, net.ErrClosed) && ctx.Err() != nil {
|
|
||||||
return fmt.Errorf("%w", ErrTimedOut)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("receiving ICMP echo reply: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sentData := message.Body.(*icmp.Echo).Data //nolint:forcetypeassert
|
|
||||||
if !bytes.Equal(receivedData, sentData) {
|
|
||||||
return fmt.Errorf("%w: sent %x and received %x", ErrICMPEchoDataMismatch, sentData, receivedData)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMessageToSend(ipVersion string, size uint, randomSource io.Reader) (id int, message *icmp.Message) {
|
|
||||||
const uint16Bytes = 2
|
|
||||||
idBytes := make([]byte, uint16Bytes)
|
|
||||||
_, _ = randomSource.Read(idBytes)
|
|
||||||
id = int(binary.BigEndian.Uint16(idBytes))
|
|
||||||
|
|
||||||
var icmpType icmp.Type
|
|
||||||
switch ipVersion {
|
|
||||||
case "v4":
|
|
||||||
icmpType = ipv4.ICMPTypeEcho
|
|
||||||
case "v6":
|
|
||||||
icmpType = ipv6.ICMPTypeEchoRequest
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("IP version %q not supported", ipVersion))
|
|
||||||
}
|
|
||||||
messageBodyData := make([]byte, size)
|
|
||||||
_, _ = randomSource.Read(messageBodyData)
|
|
||||||
|
|
||||||
// See https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types
|
|
||||||
message = &icmp.Message{
|
|
||||||
Type: icmpType, // echo request
|
|
||||||
Code: 0, // no code
|
|
||||||
Checksum: 0, // calculated at encoding (ipv4) or sending (ipv6)
|
|
||||||
Body: &icmp.Echo{
|
|
||||||
ID: id,
|
|
||||||
Seq: 0, // only one packet
|
|
||||||
Data: messageBodyData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return id, message
|
|
||||||
}
|
|
||||||
|
|
||||||
func receiveEchoReply(conn net.PacketConn, id int, buffer []byte, ipVersion string, logger Logger,
|
|
||||||
) (data []byte, err error) {
|
|
||||||
var icmpProtocol int
|
|
||||||
const (
|
|
||||||
icmpv4Protocol = 1
|
|
||||||
icmpv6Protocol = 58
|
|
||||||
)
|
|
||||||
switch ipVersion {
|
|
||||||
case "v4":
|
|
||||||
icmpProtocol = icmpv4Protocol
|
|
||||||
case "v6":
|
|
||||||
icmpProtocol = icmpv6Protocol
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown IP version: %s", ipVersion))
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Note we need to read the whole packet in one call to ReadFrom, so the buffer
|
|
||||||
// must be large enough to read the entire reply packet. See:
|
|
||||||
// https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J
|
|
||||||
bytesRead, returnAddr, err := conn.ReadFrom(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("reading from ICMP connection: %w", err)
|
|
||||||
}
|
|
||||||
packetBytes := buffer[:bytesRead]
|
|
||||||
|
|
||||||
// Parse the ICMP message
|
|
||||||
message, err := icmp.ParseMessage(icmpProtocol, packetBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch body := message.Body.(type) {
|
|
||||||
case *icmp.Echo:
|
|
||||||
if id != body.ID {
|
|
||||||
logger.Warnf("ignoring ICMP echo reply mismatching expected id %d "+
|
|
||||||
"(id: %d, type: %d, code: %d, length: %d, return address %s)",
|
|
||||||
id, body.ID, message.Type, message.Code, len(packetBytes), returnAddr)
|
|
||||||
continue // not the ID we are looking for
|
|
||||||
}
|
|
||||||
return body.Data, nil
|
|
||||||
case *icmp.DstUnreach:
|
|
||||||
logger.Debugf("ignoring ICMP destination unreachable message (type: 3, code: %d, return address %s, expected-id %d)",
|
|
||||||
message.Code, returnAddr, id)
|
|
||||||
// See https://github.com/qdm12/gluetun/pull/2923#issuecomment-3377532249
|
|
||||||
// on why we ignore this message. If it is actually unreachable, the timeout on waiting for
|
|
||||||
// the echo reply will do instead of returning an error error.
|
|
||||||
continue
|
|
||||||
case *icmp.TimeExceeded:
|
|
||||||
logger.Debugf("ignoring ICMP time exceeded message (type: 11, code: %d, return address %s, expected-id %d)",
|
|
||||||
message.Code, returnAddr, id)
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: %T (type %d, code %d, return address %s, expected-id %d)",
|
|
||||||
ErrICMPBodyUnsupported, body, message.Type, message.Code, returnAddr, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
s.logger.Info("program has been unhealthy for " +
|
||||||
|
s.vpn.healthyWait.String() + ": restarting VPN")
|
||||||
|
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,6 +10,9 @@ 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)
|
||||||
|
|
||||||
|
loopDone := make(chan struct{})
|
||||||
|
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,
|
||||||
@@ -34,5 +37,6 @@ func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
s.logger.Error(err.Error())
|
s.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<-loopDone
|
||||||
<-serverDone
|
<-serverDone
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package healthcheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
@@ -10,21 +11,30 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
logger Logger
|
logger Logger
|
||||||
handler *handler
|
handler *handler
|
||||||
|
dialer *net.Dialer
|
||||||
config settings.Health
|
config settings.Health
|
||||||
|
vpn vpnHealth
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(config settings.Health, logger Logger) *Server {
|
func NewServer(config settings.Health,
|
||||||
|
logger Logger, vpnLoop StatusApplier,
|
||||||
|
) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
handler: newHandler(logger),
|
handler: newHandler(),
|
||||||
config: config,
|
dialer: &net.Dialer{
|
||||||
|
Resolver: &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: config,
|
||||||
|
vpn: vpnHealth{
|
||||||
|
loop: vpnLoop,
|
||||||
|
healthyWait: *config.VPN.Initial,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) SetError(err error) {
|
|
||||||
s.handler.setErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusApplier interface {
|
type StatusApplier interface {
|
||||||
ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
||||||
outcome string, err error)
|
outcome string, err error)
|
||||||
|
|||||||
@@ -18,15 +18,22 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ctx.Err() == nil {
|
for ctx.Err() == nil {
|
||||||
runCtx, runCancel := context.WithCancel(ctx)
|
|
||||||
|
|
||||||
settings := l.state.GetSettings()
|
settings := l.state.GetSettings()
|
||||||
server := New(runCtx, settings.ListeningAddress, l.logger,
|
server, err := New(settings.ListeningAddress, l.logger,
|
||||||
*settings.Stealth, *settings.Log, *settings.User,
|
*settings.Stealth, *settings.Log, *settings.User,
|
||||||
*settings.Password, settings.ReadHeaderTimeout, settings.ReadTimeout)
|
*settings.Password, settings.ReadHeaderTimeout, settings.ReadTimeout)
|
||||||
|
if err != nil {
|
||||||
|
l.statusManager.SetStatus(constants.Crashed)
|
||||||
|
l.logAndWait(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
errorCh := make(chan error)
|
errorCh, err := server.Start(ctx)
|
||||||
go server.Run(runCtx, errorCh)
|
if err != nil {
|
||||||
|
l.statusManager.SetStatus(constants.Crashed)
|
||||||
|
l.logAndWait(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// TODO stable timer, check Shadowsocks
|
// TODO stable timer, check Shadowsocks
|
||||||
if l.userTrigger {
|
if l.userTrigger {
|
||||||
@@ -41,31 +48,23 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
for stayHere {
|
for stayHere {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
runCancel()
|
_ = server.Stop()
|
||||||
<-errorCh
|
|
||||||
close(errorCh)
|
|
||||||
return
|
return
|
||||||
case <-l.start:
|
case <-l.start:
|
||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
l.logger.Info("starting")
|
l.logger.Info("starting")
|
||||||
runCancel()
|
_ = server.Stop()
|
||||||
<-errorCh
|
|
||||||
close(errorCh)
|
|
||||||
stayHere = false
|
stayHere = false
|
||||||
case <-l.stop:
|
case <-l.stop:
|
||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
l.logger.Info("stopping")
|
l.logger.Info("stopping")
|
||||||
runCancel()
|
_ = server.Stop()
|
||||||
<-errorCh
|
|
||||||
// Do not close errorCh or this for loop won't work
|
|
||||||
l.stopped <- struct{}{}
|
l.stopped <- struct{}{}
|
||||||
case err := <-errorCh:
|
case err := <-errorCh:
|
||||||
close(errorCh)
|
|
||||||
l.statusManager.SetStatus(constants.Crashed)
|
l.statusManager.SetStatus(constants.Crashed)
|
||||||
l.logAndWait(ctx, err)
|
l.logAndWait(ctx, err)
|
||||||
stayHere = false
|
stayHere = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runCancel() // repetition for linter only
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,57 +2,81 @@ package httpproxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/goservices"
|
||||||
|
"github.com/qdm12/goservices/httpserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
address string
|
httpServer *httpserver.Server
|
||||||
handler http.Handler
|
handlerCtx context.Context //nolint:containedctx
|
||||||
logger infoErrorer
|
handlerCancel context.CancelFunc
|
||||||
internalWG *sync.WaitGroup
|
handlerWg *sync.WaitGroup
|
||||||
readHeaderTimeout time.Duration
|
|
||||||
readTimeout time.Duration
|
// Server settings
|
||||||
|
httpServerSettings httpserver.Settings
|
||||||
|
|
||||||
|
// Handler settings
|
||||||
|
logger Logger
|
||||||
|
stealth bool
|
||||||
|
verbose bool
|
||||||
|
username string
|
||||||
|
password string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, address string, logger Logger,
|
func ptrTo[T any](x T) *T { return &x }
|
||||||
|
|
||||||
|
func New(address string, logger Logger,
|
||||||
stealth, verbose bool, username, password string,
|
stealth, verbose bool, username, password string,
|
||||||
readHeaderTimeout, readTimeout time.Duration,
|
readHeaderTimeout, readTimeout time.Duration,
|
||||||
) *Server {
|
) (server *Server, err error) {
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
return &Server{
|
return &Server{
|
||||||
address: address,
|
handlerWg: &sync.WaitGroup{},
|
||||||
handler: newHandler(ctx, wg, logger, stealth, verbose, username, password),
|
httpServerSettings: httpserver.Settings{
|
||||||
logger: logger,
|
// Handler is set when calling Start and reset when Stop is called
|
||||||
internalWG: wg,
|
Handler: nil,
|
||||||
readHeaderTimeout: readHeaderTimeout,
|
Name: ptrTo("proxy"),
|
||||||
readTimeout: readTimeout,
|
Address: ptrTo(address),
|
||||||
}
|
ReadTimeout: readTimeout,
|
||||||
|
ReadHeaderTimeout: readHeaderTimeout,
|
||||||
|
Logger: logger,
|
||||||
|
},
|
||||||
|
logger: logger,
|
||||||
|
stealth: stealth,
|
||||||
|
verbose: verbose,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Run(ctx context.Context, errorCh chan<- error) {
|
func (s *Server) Start(ctx context.Context) (
|
||||||
server := http.Server{
|
runError <-chan error, err error,
|
||||||
Addr: s.address,
|
) {
|
||||||
Handler: s.handler,
|
if s.httpServer != nil {
|
||||||
ReadHeaderTimeout: s.readHeaderTimeout,
|
return nil, fmt.Errorf("%w", goservices.ErrAlreadyStarted)
|
||||||
ReadTimeout: s.readTimeout,
|
|
||||||
}
|
}
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
s.handlerCtx, s.handlerCancel = context.WithCancel(context.Background())
|
||||||
const shutdownGraceDuration = 100 * time.Millisecond
|
s.httpServerSettings.Handler = newHandler(s.handlerCtx, s.handlerWg,
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
|
s.logger, s.stealth, s.verbose, s.username, s.password)
|
||||||
defer cancel()
|
s.httpServer, err = httpserver.New(s.httpServerSettings)
|
||||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed shutting down: " + err.Error())
|
return nil, fmt.Errorf("creating http server: %w", err)
|
||||||
}
|
|
||||||
}()
|
|
||||||
s.logger.Info("listening on " + s.address)
|
|
||||||
err := server.ListenAndServe()
|
|
||||||
s.internalWG.Wait()
|
|
||||||
if err != nil && ctx.Err() == nil {
|
|
||||||
errorCh <- err
|
|
||||||
} else {
|
|
||||||
errorCh <- nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return s.httpServer.Start(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Stop() (err error) {
|
||||||
|
if s.httpServer == nil {
|
||||||
|
return fmt.Errorf("%w", goservices.ErrAlreadyStopped)
|
||||||
|
}
|
||||||
|
s.handlerCancel()
|
||||||
|
err = s.httpServer.Stop()
|
||||||
|
s.handlerWg.Wait()
|
||||||
|
s.httpServer = nil // signal the server is down
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package natpmp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,15 +23,14 @@ func Test_Client_ExternalAddress(t *testing.T) {
|
|||||||
durationSinceStartOfEpoch time.Duration
|
durationSinceStartOfEpoch time.Duration
|
||||||
externalIPv4Address netip.Addr
|
externalIPv4Address netip.Addr
|
||||||
err error
|
err error
|
||||||
errMessageRegex string
|
errMessage string
|
||||||
}{
|
}{
|
||||||
"failure": {
|
"failure": {
|
||||||
ctx: canceledCtx,
|
ctx: canceledCtx,
|
||||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||||
initialConnDuration: initialConnectionDuration,
|
initialConnDuration: initialConnectionDuration,
|
||||||
err: net.ErrClosed,
|
err: context.Canceled,
|
||||||
errMessageRegex: "executing remote procedure call: setting connection deadline: " +
|
errMessage: "executing remote procedure call: reading from udp connection: context canceled",
|
||||||
"set udp 127.0.0.1:[1-9][0-9]{1,4}: use of closed network connection",
|
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@@ -62,7 +60,7 @@ func Test_Client_ExternalAddress(t *testing.T) {
|
|||||||
durationSinceStartOfEpoch, externalIPv4Address, err := client.ExternalAddress(testCase.ctx, testCase.gateway)
|
durationSinceStartOfEpoch, externalIPv4Address, err := client.ExternalAddress(testCase.ctx, testCase.gateway)
|
||||||
assert.ErrorIs(t, err, testCase.err)
|
assert.ErrorIs(t, err, testCase.err)
|
||||||
if testCase.err != nil {
|
if testCase.err != nil {
|
||||||
assert.Regexp(t, testCase.errMessageRegex, err.Error())
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
}
|
}
|
||||||
assert.Equal(t, testCase.durationSinceStartOfEpoch, durationSinceStartOfEpoch)
|
assert.Equal(t, testCase.durationSinceStartOfEpoch, durationSinceStartOfEpoch)
|
||||||
assert.Equal(t, testCase.externalIPv4Address, externalIPv4Address)
|
assert.Equal(t, testCase.externalIPv4Address, externalIPv4Address)
|
||||||
|
|||||||
@@ -45,10 +45,8 @@ func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
|
|||||||
cancel()
|
cancel()
|
||||||
<-endGoroutineDone
|
<-endGoroutineDone
|
||||||
}()
|
}()
|
||||||
ctxListeningReady := make(chan struct{})
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(endGoroutineDone)
|
defer close(endGoroutineDone)
|
||||||
close(ctxListeningReady)
|
|
||||||
// Context is canceled either by the parent context or
|
// Context is canceled either by the parent context or
|
||||||
// when this function returns.
|
// when this function returns.
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@@ -62,7 +60,6 @@ func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
|
|||||||
}
|
}
|
||||||
err = fmt.Errorf("%w; closing connection: %w", err, closeErr)
|
err = fmt.Errorf("%w; closing connection: %w", err, closeErr)
|
||||||
}()
|
}()
|
||||||
<-ctxListeningReady // really to make unit testing reliable
|
|
||||||
|
|
||||||
const maxResponseSize = 16
|
const maxResponseSize = 16
|
||||||
response = make([]byte, maxResponseSize)
|
response = make([]byte, maxResponseSize)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ func (n *NetLink) LinkSetDown(link Link) (err error) {
|
|||||||
return netlink.LinkSetDown(linkToNetlinkLink(&link))
|
return netlink.LinkSetDown(linkToNetlinkLink(&link))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NetLink) LinkSetMTU(link Link, mtu int) error {
|
|
||||||
return netlink.LinkSetMTU(linkToNetlinkLink(&link), mtu)
|
|
||||||
}
|
|
||||||
|
|
||||||
type netlinkLinkImpl struct {
|
type netlinkLinkImpl struct {
|
||||||
attrs *netlink.LinkAttrs
|
attrs *netlink.LinkAttrs
|
||||||
linkType string
|
linkType string
|
||||||
|
|||||||
@@ -77,18 +77,7 @@ func netlinkRuleToRule(netlinkRule netlink.Rule) (rule Rule) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ruleDbgMsg(add bool, rule Rule) (debugMessage string) {
|
func ruleDbgMsg(add bool, rule Rule) (debugMessage string) {
|
||||||
debugMessage = "ip"
|
debugMessage = "ip rule"
|
||||||
|
|
||||||
switch rule.Family {
|
|
||||||
case FamilyV4:
|
|
||||||
debugMessage += " -f inet"
|
|
||||||
case FamilyV6:
|
|
||||||
debugMessage += " -f inet6"
|
|
||||||
default:
|
|
||||||
debugMessage += " -f " + fmt.Sprint(rule.Family)
|
|
||||||
}
|
|
||||||
|
|
||||||
debugMessage += " rule"
|
|
||||||
|
|
||||||
if add {
|
if add {
|
||||||
debugMessage += " add"
|
debugMessage += " add"
|
||||||
|
|||||||
@@ -15,28 +15,26 @@ func Test_ruleDbgMsg(t *testing.T) {
|
|||||||
dbgMsg string
|
dbgMsg string
|
||||||
}{
|
}{
|
||||||
"default values": {
|
"default values": {
|
||||||
dbgMsg: "ip -f 0 rule del pref 0",
|
dbgMsg: "ip rule del pref 0",
|
||||||
},
|
},
|
||||||
"add rule": {
|
"add rule": {
|
||||||
add: true,
|
add: true,
|
||||||
rule: Rule{
|
rule: Rule{
|
||||||
Family: FamilyV4,
|
|
||||||
Src: makeNetipPrefix(1),
|
Src: makeNetipPrefix(1),
|
||||||
Dst: makeNetipPrefix(2),
|
Dst: makeNetipPrefix(2),
|
||||||
Table: 100,
|
Table: 100,
|
||||||
Priority: 101,
|
Priority: 101,
|
||||||
},
|
},
|
||||||
dbgMsg: "ip -f inet rule add from 1.1.1.0/24 to 2.2.2.0/24 lookup 100 pref 101",
|
dbgMsg: "ip rule add from 1.1.1.0/24 to 2.2.2.0/24 lookup 100 pref 101",
|
||||||
},
|
},
|
||||||
"del rule": {
|
"del rule": {
|
||||||
rule: Rule{
|
rule: Rule{
|
||||||
Family: FamilyV4,
|
|
||||||
Src: makeNetipPrefix(1),
|
Src: makeNetipPrefix(1),
|
||||||
Dst: makeNetipPrefix(2),
|
Dst: makeNetipPrefix(2),
|
||||||
Table: 100,
|
Table: 100,
|
||||||
Priority: 101,
|
Priority: 101,
|
||||||
},
|
},
|
||||||
dbgMsg: "ip -f inet rule del from 1.1.1.0/24 to 2.2.2.0/24 lookup 100 pref 101",
|
dbgMsg: "ip rule del from 1.1.1.0/24 to 2.2.2.0/24 lookup 100 pref 101",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
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,83 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"golang.org/x/net/icmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrICMPNextHopMTUTooLow = errors.New("ICMP Next Hop MTU is too low")
|
|
||||||
ErrICMPNextHopMTUTooHigh = errors.New("ICMP Next Hop MTU is too high")
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkMTU(mtu, minMTU, physicalLinkMTU int) (err error) {
|
|
||||||
switch {
|
|
||||||
case mtu < minMTU:
|
|
||||||
return fmt.Errorf("%w: %d", ErrICMPNextHopMTUTooLow, mtu)
|
|
||||||
case mtu > physicalLinkMTU:
|
|
||||||
return fmt.Errorf("%w: %d is larger than physical link MTU %d",
|
|
||||||
ErrICMPNextHopMTUTooHigh, mtu, physicalLinkMTU)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkInvokingReplyIDMatch(icmpProtocol int, received []byte,
|
|
||||||
outboundMessage *icmp.Message,
|
|
||||||
) (match bool, err error) {
|
|
||||||
inboundMessage, err := icmp.ParseMessage(icmpProtocol, received)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("parsing invoking packet: %w", err)
|
|
||||||
}
|
|
||||||
inboundBody, ok := inboundMessage.Body.(*icmp.Echo)
|
|
||||||
if !ok {
|
|
||||||
return false, fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, inboundMessage.Body)
|
|
||||||
}
|
|
||||||
outboundBody := outboundMessage.Body.(*icmp.Echo) //nolint:forcetypeassert
|
|
||||||
return inboundBody.ID == outboundBody.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrICMPIDMismatch = errors.New("ICMP id mismatch")
|
|
||||||
|
|
||||||
func checkEchoReply(icmpProtocol int, received []byte,
|
|
||||||
outboundMessage *icmp.Message, truncatedBody bool,
|
|
||||||
) (err error) {
|
|
||||||
inboundMessage, err := icmp.ParseMessage(icmpProtocol, received)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing invoking packet: %w", err)
|
|
||||||
}
|
|
||||||
inboundBody, ok := inboundMessage.Body.(*icmp.Echo)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, inboundMessage.Body)
|
|
||||||
}
|
|
||||||
outboundBody := outboundMessage.Body.(*icmp.Echo) //nolint:forcetypeassert
|
|
||||||
if inboundBody.ID != outboundBody.ID {
|
|
||||||
return fmt.Errorf("%w: sent id %d and received id %d",
|
|
||||||
ErrICMPIDMismatch, outboundBody.ID, inboundBody.ID)
|
|
||||||
}
|
|
||||||
err = checkEchoBodies(outboundBody.Data, inboundBody.Data, truncatedBody)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("checking sent and received bodies: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrICMPEchoDataMismatch = errors.New("ICMP data mismatch")
|
|
||||||
|
|
||||||
func checkEchoBodies(sent, received []byte, receivedTruncated bool) (err error) {
|
|
||||||
if len(received) > len(sent) {
|
|
||||||
return fmt.Errorf("%w: sent %d bytes and received %d bytes",
|
|
||||||
ErrICMPEchoDataMismatch, len(sent), len(received))
|
|
||||||
}
|
|
||||||
if receivedTruncated {
|
|
||||||
sent = sent[:len(received)]
|
|
||||||
}
|
|
||||||
if !bytes.Equal(received, sent) {
|
|
||||||
return fmt.Errorf("%w: sent %x and received %x",
|
|
||||||
ErrICMPEchoDataMismatch, sent, received)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//go:build !linux && !windows
|
|
||||||
|
|
||||||
package pmtud
|
|
||||||
|
|
||||||
// setDontFragment for platforms other than Linux and Windows
|
|
||||||
// is not implemented, so we just return assuming the don't
|
|
||||||
// fragment flag is set on IP packets.
|
|
||||||
func setDontFragment(fd uintptr) (err error) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setDontFragment(fd uintptr) (err error) {
|
|
||||||
return syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP,
|
|
||||||
syscall.IP_MTU_DISCOVER, syscall.IP_PMTUDISC_PROBE)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setDontFragment(fd uintptr) (err error) {
|
|
||||||
// https://docs.microsoft.com/en-us/troubleshoot/windows/win32/header-library-requirement-socket-ipproto-ip
|
|
||||||
// #define IP_DONTFRAGMENT 14 /* don't fragment IP datagrams */
|
|
||||||
return syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, 14, 1)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrICMPNotPermitted = errors.New("ICMP not permitted")
|
|
||||||
ErrICMPDestinationUnreachable = errors.New("ICMP destination unreachable")
|
|
||||||
ErrICMPCommunicationAdministrativelyProhibited = errors.New("communication administratively prohibited")
|
|
||||||
ErrICMPBodyUnsupported = errors.New("ICMP body type is not supported")
|
|
||||||
)
|
|
||||||
|
|
||||||
func wrapConnErr(err error, timedCtx context.Context, pingTimeout time.Duration) error { //nolint:revive
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(err.Error(), "sendto: operation not permitted"):
|
|
||||||
err = fmt.Errorf("%w", ErrICMPNotPermitted)
|
|
||||||
case errors.Is(timedCtx.Err(), context.DeadlineExceeded):
|
|
||||||
err = fmt.Errorf("%w (timed out after %s)", net.ErrClosed, pingTimeout)
|
|
||||||
case timedCtx.Err() != nil:
|
|
||||||
err = timedCtx.Err()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
type Logger interface {
|
|
||||||
Debug(msg string)
|
|
||||||
Debugf(msg string, args ...any)
|
|
||||||
Warnf(msg string, args ...any)
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/icmp"
|
|
||||||
"golang.org/x/net/ipv4"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// see https://en.wikipedia.org/wiki/Maximum_transmission_unit#MTUs_for_common_media
|
|
||||||
minIPv4MTU = 68
|
|
||||||
icmpv4Protocol = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func listenICMPv4(ctx context.Context) (conn net.PacketConn, err error) {
|
|
||||||
var listenConfig net.ListenConfig
|
|
||||||
listenConfig.Control = func(_, _ string, rawConn syscall.RawConn) error {
|
|
||||||
var setDFErr error
|
|
||||||
err := rawConn.Control(func(fd uintptr) {
|
|
||||||
setDFErr = setDontFragment(fd) // runs when calling ListenPacket
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
err = setDFErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listenAddress = ""
|
|
||||||
packetConn, err := listenConfig.ListenPacket(ctx, "ip4:icmp", listenAddress)
|
|
||||||
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", ErrICMPNotPermitted)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
|
||||||
packetConn = ipv4ToNetPacketConn(ipv4.NewPacketConn(packetConn))
|
|
||||||
}
|
|
||||||
|
|
||||||
return packetConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findIPv4NextHopMTU(ctx context.Context, ip netip.Addr,
|
|
||||||
physicalLinkMTU int, pingTimeout time.Duration, logger Logger,
|
|
||||||
) (mtu int, err error) {
|
|
||||||
if ip.Is6() {
|
|
||||||
panic("IP address is not v4")
|
|
||||||
}
|
|
||||||
conn, err := listenICMPv4(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("listening for ICMP packets: %w", err)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, pingTimeout)
|
|
||||||
defer cancel()
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// First try to send a packet which is too big to get the maximum MTU
|
|
||||||
// directly.
|
|
||||||
outboundID, outboundMessage := buildMessageToSend("v4", physicalLinkMTU)
|
|
||||||
encodedMessage, err := outboundMessage.Marshal(nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("encoding ICMP message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice()})
|
|
||||||
if err != nil {
|
|
||||||
err = wrapConnErr(err, ctx, pingTimeout)
|
|
||||||
return 0, fmt.Errorf("writing ICMP message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := make([]byte, physicalLinkMTU)
|
|
||||||
|
|
||||||
for { // for loop in case we read an echo reply for another ICMP request
|
|
||||||
// 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, _, err := conn.ReadFrom(buffer)
|
|
||||||
if err != nil {
|
|
||||||
err = wrapConnErr(err, ctx, pingTimeout)
|
|
||||||
return 0, fmt.Errorf("reading from ICMP connection: %w", err)
|
|
||||||
}
|
|
||||||
packetBytes := buffer[:bytesRead]
|
|
||||||
// Side note: echo reply should be at most the number of bytes
|
|
||||||
// sent, and can be lower, more precisely 576-ipHeader bytes,
|
|
||||||
// in case the next hop we are reaching replies with a destination
|
|
||||||
// unreachable and wants to ensure the response makes it way back
|
|
||||||
// by keeping a low packet size, see:
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc1122#page-59
|
|
||||||
|
|
||||||
inboundMessage, err := icmp.ParseMessage(icmpv4Protocol, packetBytes)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("parsing message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch typedBody := inboundMessage.Body.(type) {
|
|
||||||
case *icmp.DstUnreach:
|
|
||||||
const fragmentationRequiredAndDFFlagSetCode = 4
|
|
||||||
const communicationAdministrativelyProhibitedCode = 13
|
|
||||||
switch inboundMessage.Code {
|
|
||||||
case fragmentationRequiredAndDFFlagSetCode:
|
|
||||||
case communicationAdministrativelyProhibitedCode:
|
|
||||||
return 0, fmt.Errorf("%w: %w (code %d)",
|
|
||||||
ErrICMPDestinationUnreachable,
|
|
||||||
ErrICMPCommunicationAdministrativelyProhibited,
|
|
||||||
inboundMessage.Code)
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("%w: code %d",
|
|
||||||
ErrICMPDestinationUnreachable, inboundMessage.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://datatracker.ietf.org/doc/html/rfc1191#section-4
|
|
||||||
// Note: the go library does not handle this NextHopMTU section.
|
|
||||||
nextHopMTU := packetBytes[6:8]
|
|
||||||
mtu = int(binary.BigEndian.Uint16(nextHopMTU))
|
|
||||||
err = checkMTU(mtu, minIPv4MTU, physicalLinkMTU)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("checking next-hop-mtu found: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The code below is really for sanity checks
|
|
||||||
packetBytes = packetBytes[8:]
|
|
||||||
header, err := ipv4.ParseHeader(packetBytes)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("parsing IPv4 header: %w", err)
|
|
||||||
}
|
|
||||||
packetBytes = packetBytes[header.Len:] // truncated original datagram
|
|
||||||
|
|
||||||
const truncated = true
|
|
||||||
err = checkEchoReply(icmpv4Protocol, packetBytes, outboundMessage, truncated)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("checking echo reply: %w", err)
|
|
||||||
}
|
|
||||||
return mtu, nil
|
|
||||||
case *icmp.Echo:
|
|
||||||
inboundID := uint16(typedBody.ID) //nolint:gosec
|
|
||||||
if inboundID == outboundID {
|
|
||||||
return physicalLinkMTU, nil
|
|
||||||
}
|
|
||||||
logger.Debugf("discarding received ICMP echo reply with id %d mismatching sent id %d",
|
|
||||||
inboundID, outboundID)
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, typedBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/icmp"
|
|
||||||
"golang.org/x/net/ipv6"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
minIPv6MTU = 1280
|
|
||||||
icmpv6Protocol = 58
|
|
||||||
)
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if strings.HasSuffix(err.Error(), "socket: operation not permitted") {
|
|
||||||
err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrICMPNotPermitted)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return packetConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIPv6PacketTooBig(ctx context.Context, ip netip.Addr,
|
|
||||||
physicalLinkMTU int, pingTimeout time.Duration, logger Logger,
|
|
||||||
) (mtu int, err error) {
|
|
||||||
if ip.Is4() {
|
|
||||||
panic("IP address is not v6")
|
|
||||||
}
|
|
||||||
conn, err := listenICMPv6(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("listening for ICMP packets: %w", err)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, pingTimeout)
|
|
||||||
defer cancel()
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// First try to send a packet which is too big to get the maximum MTU
|
|
||||||
// directly.
|
|
||||||
outboundID, outboundMessage := buildMessageToSend("v6", physicalLinkMTU)
|
|
||||||
encodedMessage, err := outboundMessage.Marshal(nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("encoding ICMP message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice(), Zone: ip.Zone()})
|
|
||||||
if err != nil {
|
|
||||||
err = wrapConnErr(err, ctx, pingTimeout)
|
|
||||||
return 0, fmt.Errorf("writing ICMP message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := make([]byte, physicalLinkMTU)
|
|
||||||
|
|
||||||
for { // for loop if we encounter another ICMP packet with an unknown id.
|
|
||||||
// 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, _, err := conn.ReadFrom(buffer)
|
|
||||||
if err != nil {
|
|
||||||
err = wrapConnErr(err, ctx, pingTimeout)
|
|
||||||
return 0, fmt.Errorf("reading from ICMP connection: %w", err)
|
|
||||||
}
|
|
||||||
packetBytes := buffer[:bytesRead]
|
|
||||||
|
|
||||||
packetBytes = packetBytes[ipv6.HeaderLen:]
|
|
||||||
|
|
||||||
inboundMessage, err := icmp.ParseMessage(icmpv6Protocol, packetBytes)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("parsing message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch typedBody := inboundMessage.Body.(type) {
|
|
||||||
case *icmp.PacketTooBig:
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc1885#section-3.2
|
|
||||||
mtu = typedBody.MTU
|
|
||||||
err = checkMTU(mtu, minIPv6MTU, physicalLinkMTU)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("checking MTU: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity checks
|
|
||||||
const truncatedBody = true
|
|
||||||
err = checkEchoReply(icmpv6Protocol, typedBody.Data, outboundMessage, truncatedBody)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("checking invoking message: %w", err)
|
|
||||||
}
|
|
||||||
return typedBody.MTU, nil
|
|
||||||
case *icmp.DstUnreach:
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc1885#section-3.1
|
|
||||||
idMatch, err := checkInvokingReplyIDMatch(icmpv6Protocol, packetBytes, outboundMessage)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("checking invoking message id: %w", err)
|
|
||||||
} else if idMatch {
|
|
||||||
return 0, fmt.Errorf("%w", ErrICMPDestinationUnreachable)
|
|
||||||
}
|
|
||||||
logger.Debug("discarding received ICMP destination unreachable reply with an unknown id")
|
|
||||||
continue
|
|
||||||
case *icmp.Echo:
|
|
||||||
inboundID := uint16(typedBody.ID) //nolint:gosec
|
|
||||||
if inboundID == outboundID {
|
|
||||||
return physicalLinkMTU, nil
|
|
||||||
}
|
|
||||||
logger.Debugf("discarding received ICMP echo reply with id %d mismatching sent id %d",
|
|
||||||
inboundID, outboundID)
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, typedBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
cryptorand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"math/rand/v2"
|
|
||||||
|
|
||||||
"golang.org/x/net/icmp"
|
|
||||||
"golang.org/x/net/ipv4"
|
|
||||||
"golang.org/x/net/ipv6"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildMessageToSend(ipVersion string, mtu int) (id uint16, message *icmp.Message) {
|
|
||||||
var seed [32]byte
|
|
||||||
_, _ = cryptorand.Read(seed[:])
|
|
||||||
randomSource := rand.NewChaCha8(seed)
|
|
||||||
|
|
||||||
const uint16Bytes = 2
|
|
||||||
idBytes := make([]byte, uint16Bytes)
|
|
||||||
_, _ = randomSource.Read(idBytes)
|
|
||||||
id = binary.BigEndian.Uint16(idBytes)
|
|
||||||
|
|
||||||
var ipHeaderLength int
|
|
||||||
var icmpType icmp.Type
|
|
||||||
switch ipVersion {
|
|
||||||
case "v4":
|
|
||||||
ipHeaderLength = ipv4.HeaderLen
|
|
||||||
icmpType = ipv4.ICMPTypeEcho
|
|
||||||
case "v6":
|
|
||||||
ipHeaderLength = ipv6.HeaderLen
|
|
||||||
icmpType = ipv6.ICMPTypeEchoRequest
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("IP version %q not supported", ipVersion))
|
|
||||||
}
|
|
||||||
const pingHeaderLength = 0 +
|
|
||||||
1 + // type
|
|
||||||
1 + // code
|
|
||||||
2 + // checksum
|
|
||||||
2 + // identifier
|
|
||||||
2 // sequence number
|
|
||||||
pingBodyDataSize := mtu - ipHeaderLength - pingHeaderLength
|
|
||||||
messageBodyData := make([]byte, pingBodyDataSize)
|
|
||||||
_, _ = 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: int(id),
|
|
||||||
Seq: 0, // only one packet
|
|
||||||
Data: messageBodyData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return id, message
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
type noopLogger struct{}
|
|
||||||
|
|
||||||
func (noopLogger) Debug(_ string) {}
|
|
||||||
func (noopLogger) Debugf(_ string, _ ...any) {}
|
|
||||||
func (noopLogger) Warnf(_ string, _ ...any) {}
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/icmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrMTUNotFound = errors.New("path MTU discovery failed to find MTU")
|
|
||||||
|
|
||||||
// PathMTUDiscover discovers the maximum MTU for the path to the given ip address.
|
|
||||||
// If the physicalLinkMTU is zero, it defaults to 1500 which is the ethernet standard MTU.
|
|
||||||
// If the pingTimeout is zero, it defaults to 1 second.
|
|
||||||
// If the logger is nil, a no-op logger is used.
|
|
||||||
// It returns [ErrMTUNotFound] if the MTU could not be determined.
|
|
||||||
func PathMTUDiscover(ctx context.Context, ip netip.Addr,
|
|
||||||
physicalLinkMTU int, pingTimeout time.Duration, logger Logger) (
|
|
||||||
mtu int, err error,
|
|
||||||
) {
|
|
||||||
if physicalLinkMTU == 0 {
|
|
||||||
const ethernetStandardMTU = 1500
|
|
||||||
physicalLinkMTU = ethernetStandardMTU
|
|
||||||
}
|
|
||||||
if pingTimeout == 0 {
|
|
||||||
pingTimeout = time.Second
|
|
||||||
}
|
|
||||||
if logger == nil {
|
|
||||||
logger = &noopLogger{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip.Is4() {
|
|
||||||
logger.Debug("finding IPv4 next hop MTU")
|
|
||||||
mtu, err = findIPv4NextHopMTU(ctx, ip, physicalLinkMTU, pingTimeout, logger)
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return mtu, nil
|
|
||||||
case errors.Is(err, net.ErrClosed) || errors.Is(err, ErrICMPCommunicationAdministrativelyProhibited): // blackhole
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("finding IPv4 next hop MTU: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Debug("requesting IPv6 ICMP packet-too-big reply")
|
|
||||||
mtu, err = getIPv6PacketTooBig(ctx, ip, physicalLinkMTU, pingTimeout, logger)
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return mtu, nil
|
|
||||||
case errors.Is(err, net.ErrClosed): // blackhole
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("getting IPv6 packet-too-big message: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back method: send echo requests with different packet
|
|
||||||
// sizes and check which ones succeed to find the maximum MTU.
|
|
||||||
logger.Debug("falling back to sending different sized echo packets")
|
|
||||||
minMTU := minIPv4MTU
|
|
||||||
if ip.Is6() {
|
|
||||||
minMTU = minIPv6MTU
|
|
||||||
}
|
|
||||||
return pmtudMultiSizes(ctx, ip, minMTU, physicalLinkMTU, pingTimeout, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
type pmtudTestUnit struct {
|
|
||||||
mtu int
|
|
||||||
echoID uint16
|
|
||||||
sentBytes int
|
|
||||||
ok bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func pmtudMultiSizes(ctx context.Context, ip netip.Addr,
|
|
||||||
minMTU, maxPossibleMTU int, pingTimeout time.Duration,
|
|
||||||
logger Logger,
|
|
||||||
) (maxMTU int, 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", ErrICMPNotPermitted)
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("listening for ICMP packets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mtusToTest := makeMTUsToTest(minMTU, maxPossibleMTU)
|
|
||||||
if len(mtusToTest) == 1 { // only minMTU because minMTU == maxPossibleMTU
|
|
||||||
return minMTU, nil
|
|
||||||
}
|
|
||||||
logger.Debugf("testing the following MTUs: %v", mtusToTest)
|
|
||||||
|
|
||||||
tests := make([]pmtudTestUnit, len(mtusToTest))
|
|
||||||
for i := range mtusToTest {
|
|
||||||
tests[i] = pmtudTestUnit{mtu: mtusToTest[i]}
|
|
||||||
}
|
|
||||||
|
|
||||||
timedCtx, cancel := context.WithTimeout(ctx, pingTimeout)
|
|
||||||
defer cancel()
|
|
||||||
go func() {
|
|
||||||
<-timedCtx.Done()
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := range tests {
|
|
||||||
id, message := buildMessageToSend(ipVersion, tests[i].mtu)
|
|
||||||
tests[i].echoID = id
|
|
||||||
|
|
||||||
encodedMessage, err := message.Marshal(nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("encoding ICMP message: %w", err)
|
|
||||||
}
|
|
||||||
tests[i].sentBytes = len(encodedMessage)
|
|
||||||
|
|
||||||
_, 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", ErrICMPNotPermitted)
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("writing ICMP message: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = collectReplies(conn, ipVersion, tests, logger)
|
|
||||||
switch {
|
|
||||||
case err == nil: // max possible MTU is working
|
|
||||||
return tests[len(tests)-1].mtu, nil
|
|
||||||
case err != nil && errors.Is(err, net.ErrClosed):
|
|
||||||
// we have timeouts (IPv4 testing or IPv6 PMTUD blackholes)
|
|
||||||
// so find the highest MTU which worked.
|
|
||||||
// Note we start from index len(tests) - 2 since the max MTU
|
|
||||||
// cannot be working if we had a timeout.
|
|
||||||
for i := len(tests) - 2; i >= 0; i-- { //nolint:mnd
|
|
||||||
if tests[i].ok {
|
|
||||||
return pmtudMultiSizes(ctx, ip, tests[i].mtu, tests[i+1].mtu-1,
|
|
||||||
pingTimeout, logger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All MTUs failed.
|
|
||||||
return 0, fmt.Errorf("%w: ICMP might be blocked", ErrMTUNotFound)
|
|
||||||
case err != nil:
|
|
||||||
return 0, fmt.Errorf("collecting ICMP echo replies: %w", err)
|
|
||||||
default:
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the MTU slice of length 11 such that:
|
|
||||||
// - the first element is the minMTU
|
|
||||||
// - the last element is the maxMTU
|
|
||||||
// - elements in-between are separated as close to each other
|
|
||||||
// The number 11 is chosen to find the final MTU in 3 searches,
|
|
||||||
// with a total search space of 1728 MTUs which is enough;
|
|
||||||
// to find it in 2 searches requires 37 parallel queries which
|
|
||||||
// could be blocked by firewalls.
|
|
||||||
func makeMTUsToTest(minMTU, maxMTU int) (mtus []int) {
|
|
||||||
const mtusLength = 11 // find the final MTU in 3 searches
|
|
||||||
diff := maxMTU - minMTU
|
|
||||||
switch {
|
|
||||||
case minMTU > maxMTU:
|
|
||||||
panic("minMTU > maxMTU")
|
|
||||||
case diff <= mtusLength:
|
|
||||||
mtus = make([]int, 0, diff)
|
|
||||||
for mtu := minMTU; mtu <= maxMTU; mtu++ {
|
|
||||||
mtus = append(mtus, mtu)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
step := float64(diff) / float64(mtusLength-1)
|
|
||||||
mtus = make([]int, 0, mtusLength)
|
|
||||||
for mtu := float64(minMTU); len(mtus) < mtusLength-1; mtu += step {
|
|
||||||
mtus = append(mtus, int(math.Round(mtu)))
|
|
||||||
}
|
|
||||||
mtus = append(mtus, maxMTU) // last element is the maxMTU
|
|
||||||
}
|
|
||||||
|
|
||||||
return mtus
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectReplies(conn net.PacketConn, ipVersion string,
|
|
||||||
tests []pmtudTestUnit, logger Logger,
|
|
||||||
) (err error) {
|
|
||||||
echoIDToTestIndex := make(map[uint16]int, len(tests))
|
|
||||||
for i, test := range tests {
|
|
||||||
echoIDToTestIndex[test.echoID] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
// The theoretical limit is 4GiB for IPv6 MTU path discovery jumbograms, but that would
|
|
||||||
// create huge buffers which we don't really want to support anyway.
|
|
||||||
// The standard frame maximum MTU is 1500 bytes, and there are Jumbo frames with
|
|
||||||
// a conventional maximum of 9000 bytes. However, some manufacturers support up
|
|
||||||
// 9216-20 = 9196 bytes for the maximum MTU. We thus use buffers of size 9196 to
|
|
||||||
// match eventual Jumbo frames. More information at:
|
|
||||||
// https://en.wikipedia.org/wiki/Maximum_transmission_unit#MTUs_for_common_media
|
|
||||||
const maxPossibleMTU = 9196
|
|
||||||
buffer := make([]byte, maxPossibleMTU)
|
|
||||||
|
|
||||||
idsFound := 0
|
|
||||||
for idsFound < len(tests) {
|
|
||||||
// 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, _, err := conn.ReadFrom(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading from ICMP connection: %w", err)
|
|
||||||
}
|
|
||||||
packetBytes := buffer[:bytesRead]
|
|
||||||
|
|
||||||
ipPacketLength := len(packetBytes)
|
|
||||||
|
|
||||||
var icmpProtocol int
|
|
||||||
switch ipVersion {
|
|
||||||
case "v4":
|
|
||||||
icmpProtocol = icmpv4Protocol
|
|
||||||
case "v6":
|
|
||||||
icmpProtocol = icmpv6Protocol
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown IP version: %s", ipVersion))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the ICMP message
|
|
||||||
// Note: this parsing works for a truncated 556 bytes ICMP reply packet.
|
|
||||||
message, err := icmp.ParseMessage(icmpProtocol, packetBytes)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
echoBody, ok := message.Body.(*icmp.Echo)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, message.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uint16(echoBody.ID) //nolint:gosec
|
|
||||||
testIndex, testing := echoIDToTestIndex[id]
|
|
||||||
if !testing { // not an id we expected so ignore it
|
|
||||||
logger.Warnf("ignoring ICMP reply with unexpected ID %d (type: %d, code: %d, length: %d)",
|
|
||||||
echoBody.ID, message.Type, message.Code, ipPacketLength)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
idsFound++
|
|
||||||
sentBytes := tests[testIndex].sentBytes
|
|
||||||
|
|
||||||
// echo reply should be at most the number of bytes sent,
|
|
||||||
// and can be lower, more precisely 556 bytes, in case
|
|
||||||
// the host we are reaching wants to stay out of trouble
|
|
||||||
// and ensure its echo reply goes through without
|
|
||||||
// fragmentation, see the following page:
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc1122#page-59
|
|
||||||
const conservativeReplyLength = 556
|
|
||||||
truncated := ipPacketLength < sentBytes &&
|
|
||||||
ipPacketLength == conservativeReplyLength
|
|
||||||
// Check the packet size is the same if the reply is not truncated
|
|
||||||
if !truncated && sentBytes != ipPacketLength {
|
|
||||||
return fmt.Errorf("%w: sent %dB and received %dB",
|
|
||||||
ErrICMPEchoDataMismatch, sentBytes, ipPacketLength)
|
|
||||||
}
|
|
||||||
// Truncated reply or matching reply size
|
|
||||||
tests[testIndex].ok = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
//go:build integration
|
|
||||||
|
|
||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_PathMTUDiscover(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
const physicalLinkMTU = 1500
|
|
||||||
const timeout = time.Second
|
|
||||||
mtu, err := PathMTUDiscover(context.Background(), netip.MustParseAddr("1.1.1.1"),
|
|
||||||
physicalLinkMTU, timeout, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Log("MTU found:", mtu)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package pmtud
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_makeMTUsToTest(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
minMTU int
|
|
||||||
maxMTU int
|
|
||||||
mtus []int
|
|
||||||
}{
|
|
||||||
"0_0": {
|
|
||||||
mtus: []int{0},
|
|
||||||
},
|
|
||||||
"0_1": {
|
|
||||||
maxMTU: 1,
|
|
||||||
mtus: []int{0, 1},
|
|
||||||
},
|
|
||||||
"0_8": {
|
|
||||||
maxMTU: 8,
|
|
||||||
mtus: []int{0, 1, 2, 3, 4, 5, 6, 7, 8},
|
|
||||||
},
|
|
||||||
"0_12": {
|
|
||||||
maxMTU: 12,
|
|
||||||
mtus: []int{0, 1, 2, 4, 5, 6, 7, 8, 10, 11, 12},
|
|
||||||
},
|
|
||||||
"0_80": {
|
|
||||||
maxMTU: 80,
|
|
||||||
mtus: []int{0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80},
|
|
||||||
},
|
|
||||||
"0_100": {
|
|
||||||
maxMTU: 100,
|
|
||||||
mtus: []int{0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100},
|
|
||||||
},
|
|
||||||
"1280_1500": {
|
|
||||||
minMTU: 1280,
|
|
||||||
maxMTU: 1500,
|
|
||||||
mtus: []int{1280, 1302, 1324, 1346, 1368, 1390, 1412, 1434, 1456, 1478, 1500},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
mtus := makeMTUsToTest(testCase.minMTU, testCase.maxMTU)
|
|
||||||
assert.Equal(t, testCase.mtus, mtus)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package portforward
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
@@ -30,8 +29,3 @@ type Logger interface {
|
|||||||
Warn(s string)
|
Warn(s string)
|
||||||
Error(s string)
|
Error(s string)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cmder interface {
|
|
||||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
|
||||||
waitError <-chan error, startErr error)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type Loop struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
portAllower PortAllower
|
portAllower PortAllower
|
||||||
logger Logger
|
logger Logger
|
||||||
cmder Cmder
|
|
||||||
// Fixed parameters
|
// Fixed parameters
|
||||||
uid, gid int
|
uid, gid int
|
||||||
// Internal channels and locks
|
// Internal channels and locks
|
||||||
@@ -35,7 +34,7 @@ type Loop struct {
|
|||||||
|
|
||||||
func NewLoop(settings settings.PortForwarding, routing Routing,
|
func NewLoop(settings settings.PortForwarding, routing Routing,
|
||||||
client *http.Client, portAllower PortAllower,
|
client *http.Client, portAllower PortAllower,
|
||||||
logger Logger, cmder Cmder, uid, gid int,
|
logger Logger, uid, gid int,
|
||||||
) *Loop {
|
) *Loop {
|
||||||
return &Loop{
|
return &Loop{
|
||||||
settings: Settings{
|
settings: Settings{
|
||||||
@@ -43,8 +42,6 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
|
|||||||
Service: service.Settings{
|
Service: service.Settings{
|
||||||
Enabled: settings.Enabled,
|
Enabled: settings.Enabled,
|
||||||
Filepath: *settings.Filepath,
|
Filepath: *settings.Filepath,
|
||||||
UpCommand: *settings.UpCommand,
|
|
||||||
DownCommand: *settings.DownCommand,
|
|
||||||
ListeningPort: *settings.ListeningPort,
|
ListeningPort: *settings.ListeningPort,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -52,7 +49,6 @@ func NewLoop(settings settings.PortForwarding, routing Routing,
|
|||||||
client: client,
|
client: client,
|
||||||
portAllower: portAllower,
|
portAllower: portAllower,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
cmder: cmder,
|
|
||||||
uid: uid,
|
uid: uid,
|
||||||
gid: gid,
|
gid: gid,
|
||||||
}
|
}
|
||||||
@@ -119,7 +115,7 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
|||||||
*serviceSettings.Enabled = *serviceSettings.Enabled && *l.settings.VPNIsUp
|
*serviceSettings.Enabled = *serviceSettings.Enabled && *l.settings.VPNIsUp
|
||||||
|
|
||||||
l.service = service.New(serviceSettings, l.routing, l.client,
|
l.service = service.New(serviceSettings, l.routing, l.client,
|
||||||
l.portAllower, l.logger, l.cmder, l.uid, l.gid)
|
l.portAllower, l.logger, l.uid, l.gid)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
serviceRunError, err = l.service.Start(runCtx)
|
serviceRunError, err = l.service.Start(runCtx)
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/command"
|
|
||||||
)
|
|
||||||
|
|
||||||
func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
|
||||||
commandTemplate string, ports []uint16,
|
|
||||||
) (err error) {
|
|
||||||
portStrings := make([]string, len(ports))
|
|
||||||
for i, port := range ports {
|
|
||||||
portStrings[i] = fmt.Sprint(int(port))
|
|
||||||
}
|
|
||||||
portsString := strings.Join(portStrings, ",")
|
|
||||||
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
|
||||||
args, err := command.Split(commandString)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec G204
|
|
||||||
stdout, stderr, waitError, err := cmder.Start(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
|
||||||
streamDone := make(chan struct{})
|
|
||||||
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
|
|
||||||
|
|
||||||
err = <-waitError
|
|
||||||
streamCancel()
|
|
||||||
<-streamDone
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamLines(ctx context.Context, done chan<- struct{},
|
|
||||||
logger Logger, stdout, stderr <-chan string,
|
|
||||||
) {
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
var line string
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case line = <-stdout:
|
|
||||||
logger.Info(line)
|
|
||||||
case line = <-stderr:
|
|
||||||
logger.Error(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
"github.com/qdm12/gluetun/internal/command"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Service_runCommand(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
cmder := command.New()
|
|
||||||
const commandTemplate = `/bin/sh -c "echo {{PORTS}}"`
|
|
||||||
ports := []uint16{1234, 5678}
|
|
||||||
logger := NewMockLogger(ctrl)
|
|
||||||
logger.EXPECT().Info("1234,5678")
|
|
||||||
|
|
||||||
err := runCommand(ctx, cmder, logger, commandTemplate, ports)
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||||
)
|
)
|
||||||
@@ -33,8 +32,3 @@ type PortForwarder interface {
|
|||||||
ports []uint16, err error)
|
ports []uint16, err error)
|
||||||
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cmder interface {
|
|
||||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
|
||||||
waitError <-chan error, startErr error)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Logger
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user