Compare commits

..

3 Commits
pmtu ... loops

Author SHA1 Message Date
Quentin McGaw
3f636a038c wip 2024-10-21 11:02:21 +00:00
Quentin McGaw
052317f95b wipp 2024-10-21 11:01:43 +00:00
Quentin McGaw
5f355fabbe http proxy 2024-10-21 11:01:41 +00:00
164 changed files with 23560 additions and 27974 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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",

View File

@@ -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.

View File

@@ -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/)

View File

@@ -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

View File

@@ -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 }}

View File

@@ -8,7 +8,6 @@
"retryOn429": false, "retryOn429": false,
"fallbackRetryDelay": "30s", "fallbackRetryDelay": "30s",
"aliveStatusCodes": [ "aliveStatusCodes": [
200, 200
429
] ]
} }

View File

@@ -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

View File

@@ -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:

View File

@@ -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 }}

View File

@@ -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

View File

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

3
.markdownlint.json Normal file
View File

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

35
.vscode/launch.json vendored Normal file
View 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
View File

@@ -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",
},
]
}

View File

@@ -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 \

View File

@@ -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
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg) ![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
@@ -26,6 +26,7 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
[![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues) [![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed) [![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
[![Lines of code](https://img.shields.io/tokei/lines/github/qdm12/gluetun)](https://github.com/qdm12/gluetun)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun) ![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun)
![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun) ![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun)
![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/gluetun) ![Go version](https://img.shields.io/github/go-mod/go-version/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

View File

@@ -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.")
}

View File

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

View File

@@ -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=

View File

@@ -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)
}
}

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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)
}
})
}
}

View File

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

View File

@@ -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
} }

View File

@@ -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
} }

View 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
}

View File

@@ -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
} }

View 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
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -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)
} }

View File

@@ -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")
} }

View File

@@ -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

View File

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

View File

@@ -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())
} }
} }

View File

@@ -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
} }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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)
} }

View File

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

View File

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

View File

@@ -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"),
},
}, },
}, },
}, },

View File

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

View File

@@ -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

View File

@@ -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, "; "))
}
}

View File

@@ -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
}
}

View File

@@ -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,
} }
} }

View 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
}

View File

@@ -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)
}) })

View File

@@ -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)
}

View File

@@ -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)
}
}
}

View File

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

View File

@@ -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
}

View File

@@ -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)
}

View File

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

View 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)
}

View File

@@ -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
} }

View File

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

View File

@@ -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
} }
} }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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)
} }

View File

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

View File

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

View File

@@ -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:

View File

@@ -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

View File

@@ -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"

View File

@@ -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",
}, },
} }

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -1,7 +0,0 @@
package pmtud
type Logger interface {
Debug(msg string)
Debugf(msg string, args ...any)
Warnf(msg string, args ...any)
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -1,7 +0,0 @@
package pmtud
type noopLogger struct{}
func (noopLogger) Debug(_ string) {}
func (noopLogger) Debugf(_ string, _ ...any) {}
func (noopLogger) Warnf(_ string, _ ...any) {}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
})
}
}

View File

@@ -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)
}

View File

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

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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