Compare commits
7 Commits
remove-kee
...
v3.39.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67ae5f5065 | ||
|
|
cbfdb25190 | ||
|
|
638f233b3c | ||
|
|
c450c54d67 | ||
|
|
d166314f8b | ||
|
|
7064a44403 | ||
|
|
c33158c13c |
@@ -1,4 +1,5 @@
|
||||
.dockerignore
|
||||
devcontainer.json
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
README.md
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
||||
FROM qmcgaw/godevcontainer
|
||||
RUN apk add wireguard-tools htop openssl
|
||||
|
||||
@@ -2,47 +2,68 @@
|
||||
|
||||
Development container that can be used with VSCode.
|
||||
|
||||
It works on Linux, Windows (WSL2) and OSX.
|
||||
It works on Linux, Windows and OSX.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [VS code](https://code.visualstudio.com/download) installed
|
||||
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create the following files and directory on your host if you don't have them:
|
||||
1. Create the following files on your host if you don't have them:
|
||||
|
||||
```sh
|
||||
touch ~/.gitconfig ~/.zsh_history
|
||||
mkdir -p ~/.ssh
|
||||
```
|
||||
|
||||
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
|
||||
Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them.
|
||||
|
||||
1. **For Docker on OSX or Windows without WSL**: ensure your home directory `~` is accessible by Docker.
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
||||
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
|
||||
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
|
||||
|
||||
## Customization
|
||||
|
||||
For any customization to take effect, you should "rebuild and reopen":
|
||||
### Customize the image
|
||||
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
||||
2. Select `Dev-Containers: Rebuild Container`
|
||||
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
|
||||
|
||||
Changes you can make are notably:
|
||||
```Dockerfile
|
||||
FROM qmcgaw/godevcontainer
|
||||
RUN apk add curl
|
||||
```
|
||||
|
||||
- Changes to the Docker image in [Dockerfile](Dockerfile)
|
||||
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
|
||||
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
|
||||
To rebuild the image, either:
|
||||
|
||||
```json
|
||||
// Welcome script
|
||||
{
|
||||
"source": "/yourpath/.welcome.sh",
|
||||
"target": "/root/.welcome.sh",
|
||||
"type": "bind"
|
||||
},
|
||||
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
|
||||
- With a terminal, go to this directory and `docker-compose build`
|
||||
|
||||
### Customize VS code settings
|
||||
|
||||
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
|
||||
|
||||
### Entrypoint script
|
||||
|
||||
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh).
|
||||
|
||||
### Publish a port
|
||||
|
||||
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml). You can also now do it directly with VSCode without restarting the container.
|
||||
|
||||
### Run other services
|
||||
|
||||
1. Modify [docker-compose.yml](docker-compose.yml) to launch other services at the same time as this development container, such as a test database:
|
||||
|
||||
```yml
|
||||
database:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: password
|
||||
```
|
||||
|
||||
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).
|
||||
1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`.
|
||||
1. In the VS code command palette, rebuild the container.
|
||||
|
||||
@@ -1,50 +1,16 @@
|
||||
{
|
||||
"name": "gluetun-dev",
|
||||
// User defined settings
|
||||
"containerEnv": {
|
||||
"TZ": ""
|
||||
},
|
||||
// Fixed settings
|
||||
"build": {
|
||||
"dockerfile": "./Dockerfile"
|
||||
},
|
||||
"postCreateCommand": "~/.windows.sh && go mod download",
|
||||
"capAdd": [
|
||||
"NET_ADMIN", // Gluetun specific
|
||||
"SYS_PTRACE" // for dlv Go debugging
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
],
|
||||
"securityOpt": [
|
||||
"seccomp=unconfined" // for dlv Go debugging
|
||||
],
|
||||
"mounts": [
|
||||
// Zsh commands history persistence
|
||||
{
|
||||
"source": "${localEnv:HOME}/.zsh_history",
|
||||
"target": "/root/.zsh_history",
|
||||
"type": "bind"
|
||||
},
|
||||
// Git configuration file
|
||||
{
|
||||
"source": "${localEnv:HOME}/.gitconfig",
|
||||
"target": "/root/.gitconfig",
|
||||
"type": "bind"
|
||||
},
|
||||
// SSH directory for Linux, OSX and WSL
|
||||
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||
// created in the container. On Windows, files are copied
|
||||
// from /mnt/ssh to ~/.ssh to fix permissions.
|
||||
{
|
||||
"source": "${localEnv:HOME}/.ssh",
|
||||
"target": "/mnt/ssh",
|
||||
"type": "bind"
|
||||
},
|
||||
// Docker socket to access the host Docker server
|
||||
{
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"type": "bind"
|
||||
}
|
||||
"service": "vscode",
|
||||
"runServices": [
|
||||
"vscode"
|
||||
],
|
||||
"shutdownAction": "stopCompose",
|
||||
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
|
||||
"workspaceFolder": "/workspace",
|
||||
// "overrideCommand": "",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
@@ -81,11 +47,7 @@
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true,
|
||||
"ui.diagnostic.analyses": {
|
||||
"ST1000": false
|
||||
},
|
||||
"formatting.gofumpt": true,
|
||||
"staticcheck": true
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package",
|
||||
|
||||
28
.devcontainer/docker-compose.yml
Normal file
28
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
vscode:
|
||||
build: .
|
||||
volumes:
|
||||
- ../:/workspace
|
||||
# Docker socket to access Docker server
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# SSH directory for Linux, OSX and WSL
|
||||
# On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||
# created in the container. On Windows, files are copied
|
||||
# from /mnt/ssh to ~/.ssh to fix permissions.
|
||||
- ~/.ssh:/mnt/ssh
|
||||
# Shell history persistence
|
||||
- ~/.zsh_history:/root/.zsh_history
|
||||
# Git config
|
||||
- ~/.gitconfig:/root/.gitconfig
|
||||
environment:
|
||||
- TZ=
|
||||
cap_add:
|
||||
# For debugging with dlv
|
||||
- SYS_PTRACE
|
||||
- NET_ADMIN
|
||||
security_opt:
|
||||
# For debugging with dlv
|
||||
- seccomp:unconfined
|
||||
entrypoint: [ "zsh", "-c", "while sleep 1000; do :; done" ]
|
||||
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -50,7 +50,6 @@ body:
|
||||
- Cyberghost
|
||||
- ExpressVPN
|
||||
- FastestVPN
|
||||
- Giganews
|
||||
- HideMyAss
|
||||
- IPVanish
|
||||
- IVPN
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/provider.md
vendored
37
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -6,35 +6,12 @@ labels: ":bulb: New provider"
|
||||
|
||||
---
|
||||
|
||||
Important notes:
|
||||
One of the following is required:
|
||||
|
||||
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
||||
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
||||
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
|
||||
## For Wireguard
|
||||
|
||||
Wireguard can be natively supported ONLY if:
|
||||
|
||||
- the `PrivateKey` field value is the same across all servers for one user account
|
||||
- the `Address` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `PublicKey` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `Endpoint` field value:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available
|
||||
- can be determined using a pattern, for example using country codes in hostnames
|
||||
|
||||
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
|
||||
|
||||
If **all** of these conditions are met, please provide an answer for each of them.
|
||||
|
||||
## For OpenVPN
|
||||
|
||||
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
|
||||
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
- 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.
|
||||
|
||||
14
.github/labels.yml
vendored
14
.github/labels.yml
vendored
@@ -9,9 +9,6 @@
|
||||
- name: "Status: 🔒 After next release"
|
||||
color: "f7d692"
|
||||
description: "Will be done after the next release"
|
||||
- name: "Status: 🟡 Nearly resolved"
|
||||
color: "f7d692"
|
||||
description: "This might be resolved or is about to be resolved"
|
||||
|
||||
- name: "Closed: ⚰️ Inactive"
|
||||
color: "959a9c"
|
||||
@@ -46,8 +43,6 @@
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ Cyberghost"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ Giganews"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ HideMyAss"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ IPVanish"
|
||||
@@ -91,8 +86,7 @@
|
||||
- name: "☁️ Windscribe"
|
||||
color: "cfe8d4"
|
||||
|
||||
- name: "Category: User error 🤦"
|
||||
from_name: "Category: Config problem 📝"
|
||||
- name: "Category: Config problem 📝"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: Healthcheck 🩺"
|
||||
color: "ffc7ea"
|
||||
@@ -144,9 +138,3 @@
|
||||
color: "ffc7ea"
|
||||
- name: "Category: public IP service 💬"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: servers storage 📦"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: Performance 🚀"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: Investigation 🔍"
|
||||
color: "ffc7ea"
|
||||
|
||||
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -1,12 +0,0 @@
|
||||
# Description
|
||||
|
||||
<!-- Please describe the reason for the changes being proposed. -->
|
||||
|
||||
# Issue
|
||||
|
||||
<!-- Please link to the issue(s) this change relates to. -->
|
||||
|
||||
# Assertions
|
||||
|
||||
* [ ] I am aware that we do not accept manual changes to the servers.json file <!-- If this is your goal, please consult https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-using-the-command-line -->
|
||||
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
|
||||
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
@@ -59,40 +59,13 @@ jobs:
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm --device /dev/net/tun \
|
||||
docker run --rm \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
- name: Build final image
|
||||
run: docker build -t final-image .
|
||||
|
||||
verify-private:
|
||||
if: |
|
||||
github.repository == 'qdm12/gluetun' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||
)
|
||||
needs: [verify]
|
||||
runs-on: ubuntu-latest
|
||||
environment: secrets
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- run: docker build -t qmcgaw/gluetun .
|
||||
|
||||
- name: Setup Go for CI utility
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ci/go.mod
|
||||
|
||||
- name: Build utility
|
||||
run: go build -C ./ci -o runner ./cmd/main.go
|
||||
|
||||
- name: Run Gluetun container with Mullvad configuration
|
||||
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{ secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
|
||||
|
||||
codeql:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -100,15 +73,15 @@ jobs:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- uses: github/codeql-action/init@v4
|
||||
go-version: "^1.22"
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
- uses: github/codeql-action/autobuild@v4
|
||||
- uses: github/codeql-action/analyze@v4
|
||||
- uses: github/codeql-action/autobuild@v3
|
||||
- uses: github/codeql-action/analyze@v3
|
||||
|
||||
publish:
|
||||
if: |
|
||||
@@ -125,7 +98,7 @@ jobs:
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
@@ -165,7 +138,7 @@ jobs:
|
||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
2
.github/workflows/closed-issue.yml
vendored
2
.github/workflows/closed-issue.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/create-or-update-comment@v5
|
||||
- uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
|
||||
3
.github/workflows/configs/mlc-config.json
vendored
3
.github/workflows/configs/mlc-config.json
vendored
@@ -8,7 +8,6 @@
|
||||
"retryOn429": false,
|
||||
"fallbackRetryDelay": "30s",
|
||||
"aliveStatusCodes": [
|
||||
200,
|
||||
429
|
||||
200
|
||||
]
|
||||
}
|
||||
2
.github/workflows/labels.yml
vendored
2
.github/workflows/labels.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crazy-max/ghaction-github-labeler@v5
|
||||
with:
|
||||
yaml-file: .github/labels.yml
|
||||
|
||||
6
.github/workflows/markdown.yml
vendored
6
.github/workflows/markdown.yml
vendored
@@ -18,12 +18,12 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v21
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
with:
|
||||
globs: "**.md"
|
||||
config: .markdownlint-cli2.jsonc
|
||||
config: .markdownlint.json
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
|
||||
2
.github/workflows/opened-issue.yml
vendored
2
.github/workflows/opened-issue.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/create-or-update-comment@v5
|
||||
- uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
|
||||
127
.golangci.yml
127
.golangci.yml
@@ -1,73 +1,54 @@
|
||||
version: "2"
|
||||
linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- goerr113
|
||||
- containedctx
|
||||
- goconst
|
||||
- maintidx
|
||||
- path: "internal\\/server\\/.+\\.go"
|
||||
linters:
|
||||
- dupl
|
||||
- path: "internal\\/configuration\\/settings\\/.+\\.go"
|
||||
linters:
|
||||
- dupl
|
||||
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
|
||||
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)"
|
||||
linters:
|
||||
- gomnd
|
||||
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
|
||||
source: "^.+= os\\.MkdirAll\\(.+, 0[0-9]{3}\\)"
|
||||
linters:
|
||||
- gomnd
|
||||
- linters:
|
||||
- lll
|
||||
source: "^//go:generate .+$"
|
||||
- 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
|
||||
- path: "internal\\/firewall\\/.*\\.go"
|
||||
text: "string `-i ` has [1-9][0-9]* occurrences, make it a constant"
|
||||
linters:
|
||||
- goconst
|
||||
- path: "internal\\/provider\\/ipvanish\\/updater\\/servers.go"
|
||||
text: "string ` in ` has 3 occurrences, make it a constant"
|
||||
linters:
|
||||
- goconst
|
||||
- path: "internal\\/vpn\\/portforward.go"
|
||||
text: 'directive `//nolint:ireturn` is unused for linter "ireturn"'
|
||||
linters:
|
||||
- nolintlint
|
||||
|
||||
linters:
|
||||
settings:
|
||||
misspell:
|
||||
locale: US
|
||||
goconst:
|
||||
ignore-string-values:
|
||||
# commonly used settings strings
|
||||
- "^disabled$"
|
||||
# Firewall and routing strings
|
||||
- "^(ACCEPT|DROP)$"
|
||||
- "^--delete$"
|
||||
- "^all$"
|
||||
- "^(tcp|udp)$"
|
||||
# Server route strings
|
||||
- "^/status$"
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- containedctx
|
||||
- dupl
|
||||
- err113
|
||||
- maintidx
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- dupl
|
||||
path: internal\/server\/.+\.go
|
||||
- linters:
|
||||
- ireturn
|
||||
text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
|
||||
- linters:
|
||||
- ireturn
|
||||
path: internal\/openvpn\/pkcs8\/descbc\.go
|
||||
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
|
||||
- linters:
|
||||
- revive
|
||||
path: internal\/provider\/(common|utils)\/.+\.go
|
||||
text: "var-naming: avoid (bad|meaningless) package names"
|
||||
- linters:
|
||||
- lll
|
||||
source: "^// https://.+$"
|
||||
- linters:
|
||||
- err113
|
||||
- mnd
|
||||
path: ci\/.+\.go
|
||||
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
enable:
|
||||
# - cyclop
|
||||
# - errorlint
|
||||
@@ -76,18 +57,18 @@ linters:
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- copyloopvar
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- dupword
|
||||
- durationcheck
|
||||
- err113
|
||||
- errchkjson
|
||||
- errname
|
||||
- execinquery
|
||||
- exhaustive
|
||||
- fatcontext
|
||||
- exportloopref
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
@@ -96,7 +77,10 @@ linters:
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goerr113
|
||||
- goheader
|
||||
- goimports
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
@@ -104,14 +88,12 @@ linters:
|
||||
- grouper
|
||||
- importas
|
||||
- interfacebloat
|
||||
- intrange
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- mirror
|
||||
- misspell
|
||||
- mnd
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
@@ -129,6 +111,7 @@ linters:
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- tagalign
|
||||
- tenv
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
},
|
||||
"ignores": [
|
||||
".github/pull_request_template.md"
|
||||
]
|
||||
}
|
||||
3
.markdownlint.json
Normal file
3
.markdownlint.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"MD013": false
|
||||
}
|
||||
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Update a VPN provider servers data",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "cmd/gluetun/main.go",
|
||||
"args": [
|
||||
"update",
|
||||
"${input:updateMode}",
|
||||
"-providers",
|
||||
"${input:provider}"
|
||||
],
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "provider",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a provider (or comma separated list of providers)",
|
||||
},
|
||||
{
|
||||
"id": "updateMode",
|
||||
"type": "pickString",
|
||||
"description": "Update mode to use",
|
||||
"options": [
|
||||
"-maintainer",
|
||||
"-enduser"
|
||||
],
|
||||
"default": "-maintainer"
|
||||
},
|
||||
]
|
||||
}
|
||||
51
.vscode/tasks.json
vendored
51
.vscode/tasks.json
vendored
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Update a VPN provider servers data",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"run",
|
||||
"./cmd/gluetun/main.go",
|
||||
"update",
|
||||
"${input:updateMode}",
|
||||
"-providers",
|
||||
"${input:provider}"
|
||||
],
|
||||
},
|
||||
{
|
||||
"label": "Add a Gluetun Github Git remote",
|
||||
"type": "shell",
|
||||
"command": "git",
|
||||
"args": [
|
||||
"remote",
|
||||
"add",
|
||||
"${input:githubRemoteUsername}",
|
||||
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
|
||||
],
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "provider",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a provider (or comma separated list of providers)",
|
||||
},
|
||||
{
|
||||
"id": "updateMode",
|
||||
"type": "pickString",
|
||||
"description": "Update mode to use",
|
||||
"options": [
|
||||
"-maintainer",
|
||||
"-enduser"
|
||||
],
|
||||
"default": "-maintainer"
|
||||
},
|
||||
{
|
||||
"id": "githubRemoteUsername",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a Github username",
|
||||
},
|
||||
]
|
||||
}
|
||||
74
Dockerfile
74
Dockerfile
@@ -1,14 +1,14 @@
|
||||
ARG ALPINE_VERSION=3.22
|
||||
ARG GO_ALPINE_VERSION=3.22
|
||||
ARG GO_VERSION=1.25
|
||||
ARG XCPUTRANSLATE_VERSION=v0.9.0
|
||||
ARG GOLANGCI_LINT_VERSION=v2.4.0
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG GO_ALPINE_VERSION=3.20
|
||||
ARG GO_VERSION=1.22
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.56.2
|
||||
ARG MOCKGEN_VERSION=v1.6.0
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||
@@ -32,7 +32,7 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS lint
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS mocks
|
||||
RUN git init && \
|
||||
@@ -91,7 +91,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
OPENVPN_CIPHERS= \
|
||||
OPENVPN_AUTH= \
|
||||
OPENVPN_PROCESS_USER=root \
|
||||
OPENVPN_MSSFIX= \
|
||||
OPENVPN_CUSTOM_CONFIG= \
|
||||
# Wireguard
|
||||
WIREGUARD_ENDPOINT_IP= \
|
||||
@@ -106,7 +105,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
||||
WIREGUARD_ADDRESSES= \
|
||||
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
||||
WIREGUARD_MTU=1320 \
|
||||
WIREGUARD_MTU=1400 \
|
||||
WIREGUARD_IMPLEMENTATION=auto \
|
||||
# VPN server filtering
|
||||
SERVER_REGIONS= \
|
||||
@@ -125,8 +124,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||
VPN_PORT_FORWARDING_USERNAME= \
|
||||
VPN_PORT_FORWARDING_PASSWORD= \
|
||||
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||
# # Cyberghost only:
|
||||
OPENVPN_CERT= \
|
||||
OPENVPN_KEY= \
|
||||
@@ -141,17 +138,15 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
SERVER_NUMBER= \
|
||||
# # PIA only:
|
||||
SERVER_NAMES= \
|
||||
# # VPNUnlimited and ProtonVPN only:
|
||||
STREAM_ONLY= \
|
||||
FREE_ONLY= \
|
||||
# # ProtonVPN only:
|
||||
FREE_ONLY= \
|
||||
SECURE_CORE_ONLY= \
|
||||
TOR_ONLY= \
|
||||
# # Surfshark only:
|
||||
MULTIHOP_ONLY= \
|
||||
# # VPN Secure only:
|
||||
PREMIUM_ONLY= \
|
||||
# # PIA and ProtonVPN only:
|
||||
# # PIA only:
|
||||
PORT_FORWARD_ONLY= \
|
||||
# Firewall
|
||||
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
||||
@@ -164,22 +159,25 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
# Health
|
||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||
HEALTH_ICMP_TARGET_IP=1.1.1.1 \
|
||||
HEALTH_RESTART_VPN=on \
|
||||
# DNS
|
||||
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
||||
DNS_UPSTREAM_RESOLVERS=cloudflare \
|
||||
DNS_BLOCK_IPS= \
|
||||
DNS_BLOCK_IP_PREFIXES= \
|
||||
DNS_CACHING=on \
|
||||
DNS_UPSTREAM_IPV6=off \
|
||||
HEALTH_SUCCESS_WAIT_DURATION=5s \
|
||||
HEALTH_VPN_DURATION_INITIAL=6s \
|
||||
HEALTH_VPN_DURATION_ADDITION=5s \
|
||||
# DNS over TLS
|
||||
DOT=on \
|
||||
DOT_PROVIDERS=cloudflare \
|
||||
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
||||
DOT_VERBOSITY=1 \
|
||||
DOT_VERBOSITY_DETAILS=0 \
|
||||
DOT_VALIDATION_LOGLEVEL=0 \
|
||||
DOT_CACHING=on \
|
||||
DOT_IPV6=off \
|
||||
BLOCK_MALICIOUS=on \
|
||||
BLOCK_SURVEILLANCE=off \
|
||||
BLOCK_ADS=off \
|
||||
DNS_UNBLOCK_HOSTNAMES= \
|
||||
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
||||
UNBLOCK= \
|
||||
DNS_UPDATE_PERIOD=24h \
|
||||
DNS_UPSTREAM_PLAIN_ADDRESSES= \
|
||||
DNS_ADDRESS=127.0.0.1 \
|
||||
DNS_KEEP_NAMESERVER=off \
|
||||
# HTTP proxy
|
||||
HTTPPROXY= \
|
||||
HTTPPROXY_LOG=off \
|
||||
@@ -200,20 +198,15 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
HTTP_CONTROL_SERVER_LOG=on \
|
||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
|
||||
# Server data updater
|
||||
UPDATER_PERIOD=0 \
|
||||
UPDATER_MIN_RATIO=0.8 \
|
||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||
UPDATER_PROTONVPN_USERNAME= \
|
||||
UPDATER_PROTONVPN_PASSWORD= \
|
||||
# Public IP
|
||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||
PUBLICIP_ENABLED=on \
|
||||
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
|
||||
PUBLICIP_PERIOD=12h \
|
||||
PUBLICIP_API=ipinfo \
|
||||
PUBLICIP_API_TOKEN= \
|
||||
# Storage
|
||||
STORAGE_FILEPATH=/gluetun/servers.json \
|
||||
# Pprof
|
||||
PPROF_ENABLED=no \
|
||||
PPROF_BLOCK_PROFILE_RATE=0 \
|
||||
@@ -222,8 +215,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
# Extras
|
||||
VERSION_INFORMATION=on \
|
||||
TZ= \
|
||||
PUID=1000 \
|
||||
PGID=1000
|
||||
PUID= \
|
||||
PGID=
|
||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
||||
@@ -232,9 +225,10 @@ RUN apk add --no-cache --update -l wget && \
|
||||
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \
|
||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
|
||||
apk del openvpn && \
|
||||
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
|
||||
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy unbound tzdata && \
|
||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
|
||||
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
||||
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
||||
deluser openvpn && \
|
||||
deluser unbound && \
|
||||
mkdir /gluetun
|
||||
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
|
||||
|
||||
14
README.md
14
README.md
@@ -1,8 +1,6 @@
|
||||
# Gluetun VPN client
|
||||
|
||||
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
|
||||
|
||||
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
||||
Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
|
||||

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

|
||||

|
||||

|
||||
@@ -57,8 +56,8 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
||||
|
||||
## Features
|
||||
|
||||
- Based on Alpine 3.22 for a small Docker image of 41.1MB
|
||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||
- Supports OpenVPN for all providers listed
|
||||
- Supports Wireguard both kernelspace and userspace
|
||||
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
||||
@@ -74,8 +73,9 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
||||
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
|
||||
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
||||
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
||||
- Custom VPN server side port forwarding for [Perfect Privacy](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/perfect-privacy.md#vpn-server-port-forwarding), [Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding), [PrivateVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/privatevpn.md#vpn-server-port-forwarding) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
|
||||
- Custom VPN server side port forwarding for [Perfect Privacy](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/perfect-privacy.md#vpn-server-port-forwarding), [Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
|
||||
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||
- Unbound subprogram drops root privileges once launched
|
||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||
|
||||
## Setup
|
||||
@@ -89,7 +89,7 @@ Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
||||
Here's a docker-compose.yml for the laziest:
|
||||
|
||||
```yml
|
||||
---
|
||||
version: "3"
|
||||
services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/qdm12/gluetun/ci/internal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: " + os.Args[0] + " <command>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "mullvad":
|
||||
err = internal.MullvadTest(ctx)
|
||||
default:
|
||||
err = fmt.Errorf("unknown command: %s", os.Args[1])
|
||||
}
|
||||
stop()
|
||||
if err != nil {
|
||||
fmt.Println("❌", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("✅ Test completed successfully.")
|
||||
}
|
||||
36
ci/go.mod
36
ci/go.mod
@@ -1,36 +0,0 @@
|
||||
module github.com/qdm12/gluetun/ci
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/docker/docker v28.5.1+incompatible
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
97
ci/go.sum
97
ci/go.sum
@@ -1,97 +0,0 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
@@ -1,193 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func MullvadTest(ctx context.Context) error {
|
||||
secrets, err := readSecrets(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
const timeout = 15 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating Docker client: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
config := &container.Config{
|
||||
Image: "qmcgaw/gluetun",
|
||||
StopTimeout: ptrTo(3),
|
||||
Env: []string{
|
||||
"VPN_SERVICE_PROVIDER=mullvad",
|
||||
"VPN_TYPE=wireguard",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_COUNTRIES=USA",
|
||||
"WIREGUARD_PRIVATE_KEY=" + secrets.mullvadWireguardPrivateKey,
|
||||
"WIREGUARD_ADDRESSES=" + secrets.mullvadWireguardAddress,
|
||||
},
|
||||
}
|
||||
hostConfig := &container.HostConfig{
|
||||
AutoRemove: true,
|
||||
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
|
||||
}
|
||||
networkConfig := (*network.NetworkingConfig)(nil)
|
||||
platform := (*v1.Platform)(nil)
|
||||
const containerName = "" // auto-generated name
|
||||
|
||||
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating container: %w", err)
|
||||
}
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Println("Warning during container creation:", warning)
|
||||
}
|
||||
containerID := response.ID
|
||||
defer stopContainer(client, containerID)
|
||||
|
||||
beforeStartTime := time.Now()
|
||||
|
||||
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting container: %w", err)
|
||||
}
|
||||
|
||||
return waitForLogLine(ctx, client, containerID, beforeStartTime)
|
||||
}
|
||||
|
||||
func ptrTo[T any](v T) *T { return &v }
|
||||
|
||||
type secrets struct {
|
||||
mullvadWireguardPrivateKey string
|
||||
mullvadWireguardAddress string
|
||||
}
|
||||
|
||||
func readSecrets(ctx context.Context) (secrets, error) {
|
||||
expectedSecrets := [...]string{
|
||||
"Mullvad Wireguard private key",
|
||||
"Mullvad Wireguard address",
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
lines := make([]string, 0, len(expectedSecrets))
|
||||
|
||||
for i := range expectedSecrets {
|
||||
fmt.Println("🤫 reading", expectedSecrets[i], "from Stdin...")
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
||||
if ctx.Err() != nil {
|
||||
return secrets{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return secrets{}, fmt.Errorf("reading secrets from stdin: %w", err)
|
||||
}
|
||||
|
||||
if len(lines) < len(expectedSecrets) {
|
||||
return secrets{}, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
|
||||
len(expectedSecrets), len(lines))
|
||||
}
|
||||
for i, line := range lines {
|
||||
if line == "" {
|
||||
return secrets{}, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
return secrets{
|
||||
mullvadWireguardPrivateKey: lines[0],
|
||||
mullvadWireguardAddress: lines[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func stopContainer(client *client.Client, containerID string) {
|
||||
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
|
||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
||||
defer stopCancel()
|
||||
|
||||
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
|
||||
if err != nil {
|
||||
fmt.Println("failed to stop container:", err)
|
||||
}
|
||||
}
|
||||
|
||||
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
|
||||
|
||||
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
|
||||
beforeStartTime time.Time,
|
||||
) error {
|
||||
logOptions := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
Follow: true,
|
||||
Since: beforeStartTime.Format(time.RFC3339Nano),
|
||||
}
|
||||
|
||||
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting container logs: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var linesSeen []string
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for ctx.Err() == nil {
|
||||
if scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) > 8 { // remove Docker log prefix
|
||||
line = line[8:]
|
||||
}
|
||||
linesSeen = append(linesSeen, line)
|
||||
if successRegexp.MatchString(line) {
|
||||
fmt.Println("✅ Success line logged")
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
err := scanner.Err()
|
||||
if err != nil && err != io.EOF {
|
||||
logSeenLines(linesSeen)
|
||||
return fmt.Errorf("reading log stream: %w", err)
|
||||
}
|
||||
|
||||
// The scanner is either done or cannot read because of EOF
|
||||
fmt.Println("The log scanner stopped")
|
||||
logSeenLines(linesSeen)
|
||||
|
||||
// Check if the container is still running
|
||||
inspect, err := client.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspecting container: %w", err)
|
||||
}
|
||||
if !inspect.State.Running {
|
||||
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func logSeenLines(lines []string) {
|
||||
fmt.Println("Logs seen so far:")
|
||||
for _, line := range lines {
|
||||
fmt.Println(" " + line)
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -15,9 +13,9 @@ import (
|
||||
_ "time/tzdata"
|
||||
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/qdm12/dns/pkg/unbound"
|
||||
"github.com/qdm12/gluetun/internal/alpine"
|
||||
"github.com/qdm12/gluetun/internal/cli"
|
||||
"github.com/qdm12/gluetun/internal/command"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
||||
@@ -34,6 +32,7 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/pprof"
|
||||
"github.com/qdm12/gluetun/internal/provider"
|
||||
"github.com/qdm12/gluetun/internal/publicip"
|
||||
pubipapi "github.com/qdm12/gluetun/internal/publicip/api"
|
||||
"github.com/qdm12/gluetun/internal/routing"
|
||||
"github.com/qdm12/gluetun/internal/server"
|
||||
"github.com/qdm12/gluetun/internal/shadowsocks"
|
||||
@@ -43,6 +42,7 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
"github.com/qdm12/gluetun/internal/vpn"
|
||||
"github.com/qdm12/golibs/command"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gosettings/reader/sources/env"
|
||||
"github.com/qdm12/goshutdown"
|
||||
@@ -51,6 +51,7 @@ import (
|
||||
"github.com/qdm12/goshutdown/order"
|
||||
"github.com/qdm12/gosplash"
|
||||
"github.com/qdm12/log"
|
||||
"github.com/qdm12/updated/pkg/dnscrypto"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
@@ -79,7 +80,7 @@ func main() {
|
||||
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
||||
netLinker := netlink.New(netLinkDebugLogger)
|
||||
cli := cli.New()
|
||||
cmder := command.New()
|
||||
cmder := command.NewCmder()
|
||||
|
||||
reader := reader.New(reader.Settings{
|
||||
Sources: []reader.Source{
|
||||
@@ -98,13 +99,11 @@ func main() {
|
||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
||||
}()
|
||||
|
||||
// Wait for OS signal or run error
|
||||
var err error
|
||||
select {
|
||||
case receivedSignal := <-signalCh:
|
||||
signal.Stop(signalCh)
|
||||
case signal := <-signalCh:
|
||||
fmt.Println("")
|
||||
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
|
||||
logger.Warn("Caught OS signal " + signal.String() + ", shutting down")
|
||||
cancel()
|
||||
case err = <-errorCh:
|
||||
close(errorCh)
|
||||
@@ -115,14 +114,15 @@ func main() {
|
||||
cancel()
|
||||
}
|
||||
|
||||
// Shutdown timed sequence, and force exit on second OS signal
|
||||
const shutdownGracePeriod = 5 * time.Second
|
||||
timer := time.NewTimer(shutdownGracePeriod)
|
||||
select {
|
||||
case shutdownErr := <-errorCh:
|
||||
timer.Stop()
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
if shutdownErr != nil {
|
||||
logger.Warnf("Shutdown failed: %s", shutdownErr)
|
||||
logger.Warnf("Shutdown not completed gracefully: %s", shutdownErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -134,17 +134,21 @@ func main() {
|
||||
case <-timer.C:
|
||||
logger.Warn("Shutdown timed out")
|
||||
os.Exit(1)
|
||||
case signal := <-signalCh:
|
||||
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var errCommandUnknown = errors.New("command is unknown")
|
||||
var (
|
||||
errCommandUnknown = errors.New("command is unknown")
|
||||
)
|
||||
|
||||
//nolint:gocognit,gocyclo,maintidx
|
||||
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
||||
tun Tun, netLinker netLinker, cmder RunStarter,
|
||||
cli clier,
|
||||
) error {
|
||||
tun Tun, netLinker netLinker, cmder command.RunStarter,
|
||||
cli clier) error {
|
||||
if len(args) > 1 { // cli operation
|
||||
switch args[1] {
|
||||
case "healthcheck":
|
||||
@@ -186,7 +190,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
|
||||
var allSettings settings.Settings
|
||||
err = allSettings.Read(reader, logger)
|
||||
err = allSettings.Read(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -237,7 +241,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
// TODO run this in a loop or in openvpn to reload from file without restarting
|
||||
storageLogger := logger.New(log.SetComponent("storage"))
|
||||
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
|
||||
storage, err := storage.New(storageLogger, constants.ServersData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -247,7 +251,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
|
||||
err = allSettings.Validate(storage, ipv6Supported, logger)
|
||||
err = allSettings.Validate(storage, ipv6Supported)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -267,11 +271,16 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
ovpnConf := openvpn.New(
|
||||
logger.New(log.SetComponent("openvpn configurator")),
|
||||
cmder, puid, pgid)
|
||||
dnsCrypto := dnscrypto.New(httpClient, "", "")
|
||||
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
|
||||
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
|
||||
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
|
||||
|
||||
err = printVersions(ctx, logger, []printVersionElement{
|
||||
{name: "Alpine", getVersion: alpineConf.Version},
|
||||
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
||||
{name: "Unbound", getVersion: dnsConf.Version},
|
||||
{name: "IPtables", getVersion: firewallConf.Version},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -284,13 +293,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
logger.Warn(warning)
|
||||
}
|
||||
|
||||
const permission = fs.FileMode(0o644)
|
||||
err = os.MkdirAll("/tmp/gluetun", permission)
|
||||
if err != nil {
|
||||
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll("/gluetun", permission)
|
||||
if err != nil {
|
||||
if err := os.MkdirAll("/gluetun", 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -302,8 +308,15 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
if nonRootUsername != defaultUsername {
|
||||
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
|
||||
}
|
||||
// set it for Unbound
|
||||
// TODO remove this when migrating to qdm12/dns v2
|
||||
allSettings.DNS.DoT.Unbound.Username = nonRootUsername
|
||||
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
|
||||
|
||||
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := routingConf.Setup(); err != nil {
|
||||
if strings.Contains(err.Error(), "operation not permitted") {
|
||||
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
|
||||
@@ -362,8 +375,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
defaultGroupOptions := []group.Option{
|
||||
group.OptionTimeout(defaultShutdownTimeout),
|
||||
group.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||
}
|
||||
group.OptionOnSuccess(defaultShutdownOnSuccess)}
|
||||
|
||||
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
||||
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
||||
@@ -380,60 +392,51 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||
}
|
||||
|
||||
dnsLogger := logger.New(log.SetComponent("dns"))
|
||||
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
|
||||
dnsLogger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DNS loop: %w", err)
|
||||
}
|
||||
|
||||
unboundLogger := logger.New(log.SetComponent("dns"))
|
||||
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
|
||||
unboundLogger)
|
||||
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||
"dns", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker
|
||||
go dnsLooper.Run(dnsCtx, dnsDone)
|
||||
"unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
|
||||
go unboundLooper.Run(dnsCtx, dnsDone)
|
||||
otherGroupHandler.Add(dnsHandler)
|
||||
|
||||
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
|
||||
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||
controlGroupHandler.Add(dnsTickerHandler)
|
||||
|
||||
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
|
||||
logger.New(log.SetComponent("ip getter")))
|
||||
publicipAPI, _ := pubipapi.ParseProvider(allSettings.PublicIP.API)
|
||||
ipFetcher, err := pubipapi.New(publicipAPI, httpClient, *allSettings.PublicIP.APIToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating public ip loop: %w", err)
|
||||
return fmt.Errorf("creating public IP API client: %w", err)
|
||||
}
|
||||
publicIPLooper := publicip.NewLoop(ipFetcher,
|
||||
logger.New(log.SetComponent("ip getter")),
|
||||
allSettings.PublicIP, puid, pgid)
|
||||
publicIPRunError, err := publicIPLooper.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting public ip loop: %w", err)
|
||||
}
|
||||
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||
healthChecker := healthcheck.NewChecker(healthLogger)
|
||||
|
||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||
openvpnFileExtractor := extract.New()
|
||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||
openvpnFileExtractor, allSettings.Updater)
|
||||
httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
|
||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
|
||||
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
||||
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
|
||||
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
|
||||
buildInfo, *allSettings.Version.Enabled)
|
||||
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
||||
"vpn", goroutine.OptionTimeout(time.Second))
|
||||
@@ -467,11 +470,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
||||
otherGroupHandler.Add(shadowsocksHandler)
|
||||
|
||||
controlServerAddress := *allSettings.ControlServer.Address
|
||||
controlServerLogging := *allSettings.ControlServer.Log
|
||||
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
|
||||
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
||||
logger.New(log.SetComponent("http server")),
|
||||
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||
allSettings.ControlServer.AuthFilePath,
|
||||
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper,
|
||||
storage, ipv6Supported)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up control server: %w", err)
|
||||
@@ -481,6 +487,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
<-httpServerReady
|
||||
controlGroupHandler.Add(httpServerHandler)
|
||||
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||
|
||||
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
||||
order.OptionTimeout(totalShutdownTimeout),
|
||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||
@@ -525,8 +537,7 @@ type infoer interface {
|
||||
}
|
||||
|
||||
func printVersions(ctx context.Context, logger infoer,
|
||||
elements []printVersionElement,
|
||||
) (err error) {
|
||||
elements []printVersionElement) (err error) {
|
||||
const timeout = 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
@@ -594,9 +605,3 @@ type Tun interface {
|
||||
Check(tunDevice string) error
|
||||
Create(tunDevice string) error
|
||||
}
|
||||
|
||||
type RunStarter interface {
|
||||
Run(cmd *exec.Cmd) (output string, err error)
|
||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, err error)
|
||||
}
|
||||
|
||||
68
go.mod
68
go.mod
@@ -1,69 +1,57 @@
|
||||
module github.com/qdm12/gluetun
|
||||
|
||||
go 1.25.0
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-srp v0.0.7
|
||||
github.com/breml/rootcerts v0.3.3
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/breml/rootcerts v0.2.17
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/klauspost/compress v1.18.1
|
||||
github.com/klauspost/compress v1.17.8
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f
|
||||
github.com/qdm12/gosettings v0.4.4
|
||||
github.com/pelletier/go-toml/v2 v2.2.2
|
||||
github.com/qdm12/dns v1.11.0
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
||||
github.com/qdm12/gosettings v0.4.2
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.2.0
|
||||
github.com/qdm12/gotree v0.3.0
|
||||
github.com/qdm12/gotree v0.2.0
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/qdm12/ss-server v0.6.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.15.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/miekg/dns v1.1.40 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 // indirect
|
||||
kernel.org/pub/linux/libs/security/libcap/psx 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.69 // indirect
|
||||
)
|
||||
|
||||
279
go.sum
279
go.sum
@@ -1,45 +1,69 @@
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
|
||||
github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/breml/rootcerts v0.2.17 h1:0/M2BE2Apw0qEJCXDOkaiu7d5Sx5ObNfe1BkImJ4u1I=
|
||||
github.com/breml/rootcerts v0.2.17/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
||||
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
|
||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
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/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
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/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.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
@@ -49,123 +73,144 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
||||
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/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
|
||||
github.com/pkg/errors v0.8.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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f h1:6wN5D9wACfmXDsQ366egVt0jXY4nqL/QnIwg4nWhXco=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
|
||||
github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
|
||||
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
|
||||
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
||||
github.com/qdm12/gosettings v0.4.2 h1:Gb39NScPr7OQV+oy0o1OD7A121udITDJuUGa7ljDF58=
|
||||
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/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
|
||||
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
|
||||
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
|
||||
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
|
||||
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
|
||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
||||
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/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
|
||||
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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -175,18 +220,26 @@ 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/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
||||
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.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||
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.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
|
||||
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.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
||||
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.69/go.mod h1:Tk5Ip2TuxaWGpccL7//rAsLRH6RQ/jfqTGxuN/+i/FQ=
|
||||
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.69/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||
|
||||
@@ -3,13 +3,14 @@ package alpine
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var ErrUserAlreadyExists = errors.New("user already exists")
|
||||
var (
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
)
|
||||
|
||||
// CreateUser creates a user in Alpine with the given UID.
|
||||
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
|
||||
@@ -38,8 +39,7 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
|
||||
ErrUserAlreadyExists, username, u.Uid, uid)
|
||||
}
|
||||
|
||||
const permission = fs.FileMode(0o644)
|
||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
|
||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ func (c *CLI) ClientKey(args []string) error {
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s := string(data)
|
||||
s = strings.ReplaceAll(s, "\n", "")
|
||||
s = strings.ReplaceAll(s, "\r", "")
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -17,13 +16,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFormatNotRecognized = errors.New("format is not recognized")
|
||||
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
||||
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
|
||||
)
|
||||
|
||||
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
|
||||
provider string, titleCaser cases.Caser,
|
||||
) {
|
||||
provider string, titleCaser cases.Caser) {
|
||||
boolPtr, ok := providerToFormat[provider]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
|
||||
@@ -44,7 +43,7 @@ func (c *CLI) FormatServers(args []string) error {
|
||||
providersToFormat[provider] = new(bool)
|
||||
}
|
||||
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
|
||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown' or 'json'")
|
||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
|
||||
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
||||
titleCaser := cases.Title(language.English)
|
||||
for _, provider := range allProviderFlags {
|
||||
@@ -54,7 +53,9 @@ func (c *CLI) FormatServers(args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Note the format is validated by storage.Format
|
||||
if format != "markdown" {
|
||||
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
|
||||
}
|
||||
|
||||
// Verify only one provider is set to be formatted.
|
||||
var providers []string
|
||||
@@ -86,14 +87,10 @@ func (c *CLI) FormatServers(args []string) error {
|
||||
return fmt.Errorf("creating servers storage: %w", err)
|
||||
}
|
||||
|
||||
formatted, err := storage.Format(providerToFormat, format)
|
||||
if err != nil {
|
||||
return fmt.Errorf("formatting servers: %w", err)
|
||||
}
|
||||
formatted := storage.FormatToMarkdown(providerToFormat)
|
||||
|
||||
output = filepath.Clean(output)
|
||||
const permission = fs.FileMode(0o644)
|
||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
|
||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening output file: %w", err)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func base58Encode(data []byte) string {
|
||||
}
|
||||
|
||||
// integer simplification of ceil(log(256)/log(58))
|
||||
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:mnd
|
||||
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:gomnd
|
||||
size := zcount + ceilLog256Div58
|
||||
|
||||
output := make([]byte, size)
|
||||
@@ -43,7 +43,7 @@ func base58Encode(data []byte) string {
|
||||
for _, b := range data {
|
||||
i := size - 1
|
||||
for carry := uint32(b); i > high || carry != 0; i-- {
|
||||
carry += 256 * uint32(output[i]) //nolint:mnd
|
||||
carry += 256 * uint32(output[i]) //nolint:gomnd
|
||||
output[i] = byte(carry % radix)
|
||||
carry /= radix
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package cli
|
||||
|
||||
import "github.com/qdm12/golibs/logging"
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func newNoopLogger() *noopLogger {
|
||||
return new(noopLogger)
|
||||
}
|
||||
|
||||
func (l *noopLogger) Info(string) {}
|
||||
func (l *noopLogger) Warn(string) {}
|
||||
func (l *noopLogger) Debug(string) {}
|
||||
func (l *noopLogger) Info(string) {}
|
||||
func (l *noopLogger) Warn(string) {}
|
||||
func (l *noopLogger) Error(string) {}
|
||||
func (l *noopLogger) PatchLevel(logging.Level) {}
|
||||
func (l *noopLogger) PatchPrefix(string) {}
|
||||
|
||||
@@ -34,8 +34,6 @@ type ParallelResolver interface {
|
||||
}
|
||||
|
||||
type IPFetcher interface {
|
||||
String() string
|
||||
CanFetchAnyIP() bool
|
||||
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
|
||||
}
|
||||
|
||||
@@ -44,26 +42,24 @@ type IPv6Checker interface {
|
||||
}
|
||||
|
||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
ipv6Checker IPv6Checker,
|
||||
) error {
|
||||
ipv6Checker IPv6Checker) error {
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var allSettings settings.Settings
|
||||
err = allSettings.Read(reader, logger)
|
||||
err = allSettings.Read(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allSettings.SetDefaults()
|
||||
|
||||
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
|
||||
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
|
||||
if err = allSettings.Validate(storage, ipv6Supported); err != nil {
|
||||
return fmt.Errorf("validating settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -76,7 +72,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, warner, client,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
||||
connection, err := providerConf.GetConnection(
|
||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,8 +24,6 @@ import (
|
||||
var (
|
||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||
ErrUsernameMissing = errors.New("username is required for this provider")
|
||||
ErrPasswordMissing = errors.New("password is required for this provider")
|
||||
)
|
||||
|
||||
type UpdaterLogger interface {
|
||||
@@ -38,7 +35,7 @@ type UpdaterLogger interface {
|
||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||
options := settings.Updater{}
|
||||
var endUserMode, maintainerMode, updateAll bool
|
||||
var csvProviders, ipToken, protonUsername, protonPassword string
|
||||
var csvProviders, ipToken string
|
||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||
@@ -50,8 +47,6 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
||||
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
||||
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
||||
flagSet.StringVar(&protonUsername, "proton-username", "", "Username to use to authenticate with Proton")
|
||||
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
||||
if err := flagSet.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -69,11 +64,6 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
options.Providers = strings.Split(csvProviders, ",")
|
||||
}
|
||||
|
||||
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||
options.ProtonUsername = &protonUsername
|
||||
options.ProtonPassword = &protonPassword
|
||||
}
|
||||
|
||||
options.SetDefaults(options.Providers[0])
|
||||
|
||||
err := options.Validate()
|
||||
@@ -81,11 +71,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
return fmt.Errorf("options validation failed: %w", err)
|
||||
}
|
||||
|
||||
serversDataPath := constants.ServersData
|
||||
if maintainerMode {
|
||||
serversDataPath = ""
|
||||
}
|
||||
storage, err := storage.New(logger, serversDataPath)
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating servers storage: %w", err)
|
||||
}
|
||||
@@ -94,21 +80,14 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
||||
nameTokenPairs := []api.NameToken{
|
||||
{Name: string(api.IPInfo), Token: ipToken},
|
||||
{Name: string(api.IP2Location)},
|
||||
{Name: string(api.IfConfigCo)},
|
||||
}
|
||||
fetchers, err := api.New(nameTokenPairs, httpClient)
|
||||
ipFetcher, err := api.New(api.IPInfo, httpClient, ipToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating public IP fetchers: %w", err)
|
||||
return fmt.Errorf("creating public IP API client: %w", err)
|
||||
}
|
||||
ipFetcher := api.NewResilient(fetchers, logger)
|
||||
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
|
||||
updater := updater.New(httpClient, storage, providers, logger)
|
||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package command
|
||||
|
||||
// Cmder handles running subprograms synchronously and asynchronously.
|
||||
type Cmder struct{}
|
||||
|
||||
func New() *Cmder {
|
||||
return &Cmder{}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package command
|
||||
|
||||
import "io"
|
||||
|
||||
type execCmd interface {
|
||||
CombinedOutput() ([]byte, error)
|
||||
StdoutPipe() (io.ReadCloser, error)
|
||||
StderrPipe() (io.ReadCloser, error)
|
||||
Start() error
|
||||
Wait() error
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package command
|
||||
|
||||
//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go
|
||||
@@ -1,108 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: interfaces_local.go
|
||||
|
||||
// Package command is a generated GoMock package.
|
||||
package command
|
||||
|
||||
import (
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockexecCmd is a mock of execCmd interface.
|
||||
type MockexecCmd struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockexecCmdMockRecorder
|
||||
}
|
||||
|
||||
// MockexecCmdMockRecorder is the mock recorder for MockexecCmd.
|
||||
type MockexecCmdMockRecorder struct {
|
||||
mock *MockexecCmd
|
||||
}
|
||||
|
||||
// NewMockexecCmd creates a new mock instance.
|
||||
func NewMockexecCmd(ctrl *gomock.Controller) *MockexecCmd {
|
||||
mock := &MockexecCmd{ctrl: ctrl}
|
||||
mock.recorder = &MockexecCmdMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockexecCmd) EXPECT() *MockexecCmdMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CombinedOutput mocks base method.
|
||||
func (m *MockexecCmd) CombinedOutput() ([]byte, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CombinedOutput")
|
||||
ret0, _ := ret[0].([]byte)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CombinedOutput indicates an expected call of CombinedOutput.
|
||||
func (mr *MockexecCmdMockRecorder) CombinedOutput() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CombinedOutput", reflect.TypeOf((*MockexecCmd)(nil).CombinedOutput))
|
||||
}
|
||||
|
||||
// Start mocks base method.
|
||||
func (m *MockexecCmd) Start() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Start")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Start indicates an expected call of Start.
|
||||
func (mr *MockexecCmdMockRecorder) Start() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockexecCmd)(nil).Start))
|
||||
}
|
||||
|
||||
// StderrPipe mocks base method.
|
||||
func (m *MockexecCmd) StderrPipe() (io.ReadCloser, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "StderrPipe")
|
||||
ret0, _ := ret[0].(io.ReadCloser)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// StderrPipe indicates an expected call of StderrPipe.
|
||||
func (mr *MockexecCmdMockRecorder) StderrPipe() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StderrPipe", reflect.TypeOf((*MockexecCmd)(nil).StderrPipe))
|
||||
}
|
||||
|
||||
// StdoutPipe mocks base method.
|
||||
func (m *MockexecCmd) StdoutPipe() (io.ReadCloser, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "StdoutPipe")
|
||||
ret0, _ := ret[0].(io.ReadCloser)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// StdoutPipe indicates an expected call of StdoutPipe.
|
||||
func (mr *MockexecCmdMockRecorder) StdoutPipe() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StdoutPipe", reflect.TypeOf((*MockexecCmd)(nil).StdoutPipe))
|
||||
}
|
||||
|
||||
// Wait mocks base method.
|
||||
func (m *MockexecCmd) Wait() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Wait")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Wait indicates an expected call of Wait.
|
||||
func (mr *MockexecCmdMockRecorder) Wait() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockexecCmd)(nil).Wait))
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Run runs a command in a blocking manner, returning its output and
|
||||
// an error if it failed.
|
||||
func (c *Cmder) Run(cmd *exec.Cmd) (output string, err error) {
|
||||
return run(cmd)
|
||||
}
|
||||
|
||||
func run(cmd execCmd) (output string, err error) {
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
output = string(stdout)
|
||||
output = strings.TrimSuffix(output, "\n")
|
||||
lines := stringToLines(output)
|
||||
for i := range lines {
|
||||
lines[i] = strings.TrimPrefix(lines[i], "'")
|
||||
lines[i] = strings.TrimSuffix(lines[i], "'")
|
||||
}
|
||||
output = strings.Join(lines, "\n")
|
||||
return output, err
|
||||
}
|
||||
|
||||
func stringToLines(s string) (lines []string) {
|
||||
s = strings.TrimSuffix(s, "\n")
|
||||
return strings.Split(s, "\n")
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_run(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
errDummy := errors.New("dummy")
|
||||
|
||||
testCases := map[string]struct {
|
||||
stdout []byte
|
||||
cmdErr error
|
||||
output string
|
||||
err error
|
||||
}{
|
||||
"no output": {},
|
||||
"cmd error": {
|
||||
stdout: []byte("'hello \nworld'\n"),
|
||||
cmdErr: errDummy,
|
||||
output: "hello \nworld",
|
||||
err: errDummy,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
mockCmd := NewMockexecCmd(ctrl)
|
||||
|
||||
mockCmd.EXPECT().CombinedOutput().Return(testCase.stdout, testCase.cmdErr)
|
||||
|
||||
output, err := run(mockCmd)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommandEmpty = errors.New("command is empty")
|
||||
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
||||
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
||||
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
|
||||
)
|
||||
|
||||
// Split splits a command string into a slice of arguments.
|
||||
// This is especially important for commands such as:
|
||||
// /bin/sh -c "echo hello"
|
||||
// which should be split into: ["/bin/sh", "-c", "echo hello"]
|
||||
// It supports backslash-escapes, single-quotes and double-quotes.
|
||||
// It does not support:
|
||||
// - the $" quoting style.
|
||||
// - expansion (brace, shell or pathname).
|
||||
func Split(command string) (words []string, err error) {
|
||||
if command == "" {
|
||||
return nil, fmt.Errorf("%w", ErrCommandEmpty)
|
||||
}
|
||||
|
||||
const bufferSize = 1024
|
||||
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
||||
|
||||
startIndex := 0
|
||||
|
||||
for startIndex < len(command) {
|
||||
// skip any split characters at the start
|
||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
|
||||
switch {
|
||||
case strings.ContainsRune(" \n\t", character):
|
||||
startIndex += runeSize
|
||||
case character == '\\':
|
||||
// Look ahead to eventually skip an escaped newline
|
||||
if command[startIndex+runeSize:] == "" {
|
||||
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
|
||||
}
|
||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
|
||||
if character == '\n' {
|
||||
startIndex += runeSize + runeSize // backslash and newline
|
||||
}
|
||||
default:
|
||||
var word string
|
||||
buffer.Reset()
|
||||
word, startIndex, err = splitWord(command, startIndex, buffer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
|
||||
}
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
return words, nil
|
||||
}
|
||||
|
||||
// WARNING: buffer must be cleared before calling this function.
|
||||
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
cursor := startIndex
|
||||
for cursor < len(input) {
|
||||
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += runeLength
|
||||
if character == '"' ||
|
||||
character == '\'' ||
|
||||
character == '\\' ||
|
||||
character == ' ' ||
|
||||
character == '\n' ||
|
||||
character == '\t' {
|
||||
buffer.WriteString(input[startIndex : cursor-runeLength])
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.ContainsRune(" \n\t", character): // spacing character
|
||||
return buffer.String(), cursor, nil
|
||||
case character == '"':
|
||||
return handleDoubleQuoted(input, cursor, buffer)
|
||||
case character == '\'':
|
||||
return handleSingleQuoted(input, cursor, buffer)
|
||||
case character == '\\':
|
||||
return handleEscaped(input, cursor, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteString(input[startIndex:])
|
||||
return buffer.String(), len(input), nil
|
||||
}
|
||||
|
||||
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
cursor := startIndex
|
||||
for cursor < len(input) {
|
||||
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += nextRuneLength
|
||||
switch nextCharacter {
|
||||
case '"': // end of the double quoted string
|
||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
|
||||
return splitWord(input, cursor, buffer)
|
||||
case '\\': // escaped character
|
||||
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += escapedRuneLength
|
||||
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
|
||||
break
|
||||
}
|
||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
|
||||
if escapedCharacter != '\n' {
|
||||
// skip backslash entirely for the newline character
|
||||
buffer.WriteRune(escapedCharacter)
|
||||
}
|
||||
startIndex = cursor
|
||||
}
|
||||
}
|
||||
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
|
||||
}
|
||||
|
||||
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
|
||||
if closingQuoteIndex == -1 {
|
||||
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
|
||||
}
|
||||
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
|
||||
const singleQuoteRuneLength = 1
|
||||
startIndex += closingQuoteIndex + singleQuoteRuneLength
|
||||
return splitWord(input, startIndex, buffer)
|
||||
}
|
||||
|
||||
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
if input[startIndex:] == "" {
|
||||
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
|
||||
}
|
||||
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
|
||||
if character != '\n' { // backslash-escaped newline is ignored
|
||||
buffer.WriteString(input[startIndex : startIndex+runeLength])
|
||||
}
|
||||
startIndex += runeLength
|
||||
return splitWord(input, startIndex, buffer)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Split(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
command string
|
||||
words []string
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"empty": {
|
||||
command: "",
|
||||
errWrapped: ErrCommandEmpty,
|
||||
errMessage: "command is empty",
|
||||
},
|
||||
"concrete_sh_command": {
|
||||
command: `/bin/sh -c "echo 123"`,
|
||||
words: []string{"/bin/sh", "-c", "echo 123"},
|
||||
},
|
||||
"single_word": {
|
||||
command: "word1",
|
||||
words: []string{"word1"},
|
||||
},
|
||||
"two_words_single_space": {
|
||||
command: "word1 word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"two_words_multiple_space": {
|
||||
command: "word1 word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"two_words_no_expansion": {
|
||||
command: "word1* word2?",
|
||||
words: []string{"word1*", "word2?"},
|
||||
},
|
||||
"escaped_single quote": {
|
||||
command: "ain\\'t good",
|
||||
words: []string{"ain't", "good"},
|
||||
},
|
||||
"escaped_single_quote_all_single_quoted": {
|
||||
command: "'ain'\\''t good'",
|
||||
words: []string{"ain't good"},
|
||||
},
|
||||
"empty_single_quoted": {
|
||||
command: "word1 '' word2",
|
||||
words: []string{"word1", "", "word2"},
|
||||
},
|
||||
"escaped_newline": {
|
||||
command: "word1\\\nword2",
|
||||
words: []string{"word1word2"},
|
||||
},
|
||||
"quoted_newline": {
|
||||
command: "text \"with\na\" quoted newline",
|
||||
words: []string{"text", "with\na", "quoted", "newline"},
|
||||
},
|
||||
"quoted_escaped_newline": {
|
||||
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
|
||||
words: []string{"word1\\d\\\" word2word3 word4"},
|
||||
},
|
||||
"escaped_separated_newline": {
|
||||
command: "word1 \\\n word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"double_quotes_no_spacing": {
|
||||
command: "word1\"word2\"word3",
|
||||
words: []string{"word1word2word3"},
|
||||
},
|
||||
"unterminated_single_quote": {
|
||||
command: "'abc'\\''def",
|
||||
errWrapped: ErrSingleQuoteUnterminated,
|
||||
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
||||
},
|
||||
"unterminated_double_quote": {
|
||||
command: "\"abc'def",
|
||||
errWrapped: ErrDoubleQuoteUnterminated,
|
||||
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
||||
},
|
||||
"unterminated_escape": {
|
||||
command: "abc\\",
|
||||
errWrapped: ErrEscapeUnterminated,
|
||||
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
||||
},
|
||||
"unterminated_escape_only": {
|
||||
command: " \\",
|
||||
errWrapped: ErrEscapeUnterminated,
|
||||
errMessage: `unterminated backslash-escape: " \\"`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
words, err := Split(testCase.command)
|
||||
|
||||
assert.Equal(t, testCase.words, words)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Start launches a command and streams stdout and stderr to channels.
|
||||
// All the channels returned are ready only and won't be closed
|
||||
// if the command fails later.
|
||||
func (c *Cmder) Start(cmd *exec.Cmd) (
|
||||
stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error,
|
||||
) {
|
||||
return start(cmd)
|
||||
}
|
||||
|
||||
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error,
|
||||
) {
|
||||
stop := make(chan struct{})
|
||||
stdoutReady := make(chan struct{})
|
||||
stdoutLinesCh := make(chan string)
|
||||
stdoutDone := make(chan struct{})
|
||||
stderrReady := make(chan struct{})
|
||||
stderrLinesCh := make(chan string)
|
||||
stderrDone := make(chan struct{})
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
<-stderrDone
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
waitErrorCh := make(chan error)
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
<-stderrDone
|
||||
waitErrorCh <- err
|
||||
}()
|
||||
|
||||
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
||||
}
|
||||
|
||||
func streamToChannel(ready chan<- struct{},
|
||||
stop <-chan struct{}, done chan<- struct{},
|
||||
stream io.Reader, lines chan<- string,
|
||||
) {
|
||||
defer close(done)
|
||||
close(ready)
|
||||
scanner := bufio.NewScanner(stream)
|
||||
lineBuffer := make([]byte, bufio.MaxScanTokenSize) // 64KB
|
||||
const maxCapacity = 20 * 1024 * 1024 // 20MB
|
||||
scanner.Buffer(lineBuffer, maxCapacity)
|
||||
|
||||
for scanner.Scan() {
|
||||
// scanner is closed if the context is canceled
|
||||
// or if the command failed starting because the
|
||||
// stream is closed (io.EOF error).
|
||||
lines <- scanner.Text()
|
||||
}
|
||||
err := scanner.Err()
|
||||
if err == nil || errors.Is(err, os.ErrClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
// ignore the error if it is stopped.
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
lines <- "stream error: " + err.Error()
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func linesToReadCloser(lines []string) io.ReadCloser {
|
||||
s := strings.Join(lines, "\n")
|
||||
return io.NopCloser(bytes.NewBufferString(s))
|
||||
}
|
||||
|
||||
func Test_start(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
errDummy := errors.New("dummy")
|
||||
|
||||
testCases := map[string]struct {
|
||||
stdout []string
|
||||
stdoutPipeErr error
|
||||
stderr []string
|
||||
stderrPipeErr error
|
||||
startErr error
|
||||
waitErr error
|
||||
err error
|
||||
}{
|
||||
"no output": {},
|
||||
"success": {
|
||||
stdout: []string{"hello", "world"},
|
||||
stderr: []string{"some", "error"},
|
||||
},
|
||||
"stdout pipe error": {
|
||||
stdoutPipeErr: errDummy,
|
||||
err: errDummy,
|
||||
},
|
||||
"stderr pipe error": {
|
||||
stderrPipeErr: errDummy,
|
||||
err: errDummy,
|
||||
},
|
||||
"start error": {
|
||||
startErr: errDummy,
|
||||
err: errDummy,
|
||||
},
|
||||
"wait error": {
|
||||
waitErr: errDummy,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
stdout := linesToReadCloser(testCase.stdout)
|
||||
stderr := linesToReadCloser(testCase.stderr)
|
||||
|
||||
mockCmd := NewMockexecCmd(ctrl)
|
||||
|
||||
mockCmd.EXPECT().StdoutPipe().
|
||||
Return(stdout, testCase.stdoutPipeErr)
|
||||
if testCase.stdoutPipeErr == nil {
|
||||
mockCmd.EXPECT().StderrPipe().Return(stderr, testCase.stderrPipeErr)
|
||||
if testCase.stderrPipeErr == nil {
|
||||
mockCmd.EXPECT().Start().Return(testCase.startErr)
|
||||
if testCase.startErr == nil {
|
||||
mockCmd.EXPECT().Wait().Return(testCase.waitErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdoutLines, stderrLines, waitError, err := start(mockCmd)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
assert.Nil(t, stdoutLines)
|
||||
assert.Nil(t, stderrLines)
|
||||
assert.Nil(t, waitError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
var stdoutIndex, stderrIndex int
|
||||
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case line := <-stdoutLines:
|
||||
assert.Equal(t, testCase.stdout[stdoutIndex], line)
|
||||
stdoutIndex++
|
||||
case line := <-stderrLines:
|
||||
assert.Equal(t, testCase.stderr[stderrIndex], line)
|
||||
stderrIndex++
|
||||
case err := <-waitError:
|
||||
if testCase.waitErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.waitErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, len(testCase.stdout), stdoutIndex)
|
||||
assert.Equal(t, len(testCase.stderr), stderrIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
func readObsolete(r *reader.Reader) (warnings []string) {
|
||||
keyToMessage := map[string]string{
|
||||
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
|
||||
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
|
||||
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
|
||||
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
|
||||
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
|
||||
"DNS_SERVER": "DNS_SERVER is obsolete because the forwarding server is always enabled.",
|
||||
"DOT": "DOT is obsolete because the forwarding server is always enabled.",
|
||||
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because the forwarding server is always used and " +
|
||||
"forwards local names to private DNS resolvers found in /etc/resolv.conf",
|
||||
}
|
||||
sortedKeys := maps.Keys(keyToMessage)
|
||||
slices.Sort(sortedKeys)
|
||||
warnings = make([]string, 0, len(keyToMessage))
|
||||
for _, key := range sortedKeys {
|
||||
if r.Get(key) != nil {
|
||||
warnings = append(warnings, keyToMessage[key])
|
||||
}
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
@@ -1,96 +1,42 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
const (
|
||||
DNSUpstreamTypeDot = "dot"
|
||||
DNSUpstreamTypeDoh = "doh"
|
||||
DNSUpstreamTypePlain = "plain"
|
||||
)
|
||||
|
||||
// DNS contains settings to configure DNS.
|
||||
type DNS struct {
|
||||
// UpstreamType can be [dnsUpstreamTypeDot], [dnsUpstreamTypeDoh]
|
||||
// or [dnsUpstreamTypePlain]. It defaults to [dnsUpstreamTypeDot].
|
||||
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.
|
||||
// It defaults to either ["cloudflare"] or [] if the
|
||||
// UpstreamPlainAddresses field is set.
|
||||
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
|
||||
// UpstreamPlainAddresses are the upstream plaintext DNS resolver
|
||||
// addresses to use by the built-in DNS server forwarder.
|
||||
// Note, if the upstream type is [dnsUpstreamTypePlain] these are merged
|
||||
// together with provider names set in the Providers field.
|
||||
// If this field is set, the Providers field will default to the empty slice.
|
||||
UpstreamPlainAddresses []netip.AddrPort
|
||||
// ServerAddress is the DNS server to use inside
|
||||
// the Go program and for the system.
|
||||
// It defaults to '127.0.0.1' to be used with the
|
||||
// DoT server. It cannot be the zero value in the internal
|
||||
// state.
|
||||
ServerAddress netip.Addr
|
||||
// KeepNameserver is true if the existing DNS server
|
||||
// found in /etc/resolv.conf should be used
|
||||
// Note setting this to true will likely DNS traffic
|
||||
// outside the VPN tunnel since it would go through
|
||||
// the local DNS server of your Docker/Kubernetes
|
||||
// configuration, which is likely not going through the tunnel.
|
||||
// This will also disable the DNS over TLS server and the
|
||||
// `ServerAddress` field will be ignored.
|
||||
// It defaults to false and cannot be nil in the
|
||||
// internal state.
|
||||
KeepNameserver *bool
|
||||
// DOT contains settings to configure the DoT
|
||||
// server.
|
||||
DoT DoT
|
||||
}
|
||||
|
||||
var (
|
||||
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
|
||||
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
|
||||
ErrDNSUpstreamPlainNoIPv6 = errors.New("upstream plain addresses do not contain any IPv6 address")
|
||||
ErrDNSUpstreamPlainNoIPv4 = errors.New("upstream plain addresses do not contain any IPv4 address")
|
||||
)
|
||||
|
||||
func (d DNS) validate() (err error) {
|
||||
if !helpers.IsOneOf(d.UpstreamType, DNSUpstreamTypeDot, DNSUpstreamTypeDoh, DNSUpstreamTypePlain) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
if *d.IPv6 && !slices.ContainsFunc(d.UpstreamPlainAddresses, func(addrPort netip.AddrPort) bool {
|
||||
return addrPort.Addr().Is6()
|
||||
}) {
|
||||
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv6, len(d.UpstreamPlainAddresses))
|
||||
} else if !slices.ContainsFunc(d.UpstreamPlainAddresses, func(addrPort netip.AddrPort) bool {
|
||||
return addrPort.Addr().Is4()
|
||||
}) {
|
||||
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv4, len(d.UpstreamPlainAddresses))
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
err = d.DoT.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("validating DoT settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -98,13 +44,9 @@ func (d DNS) validate() (err error) {
|
||||
|
||||
func (d *DNS) Copy() (copied DNS) {
|
||||
return DNS{
|
||||
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(),
|
||||
UpstreamPlainAddresses: d.UpstreamPlainAddresses,
|
||||
ServerAddress: d.ServerAddress,
|
||||
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||
DoT: d.DoT.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,72 +54,16 @@ func (d *DNS) Copy() (copied DNS) {
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DNS) overrideWith(other DNS) {
|
||||
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.UpstreamPlainAddresses = gosettings.OverrideWithSlice(d.UpstreamPlainAddresses, other.UpstreamPlainAddresses)
|
||||
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||
d.DoT.overrideWith(other.DoT)
|
||||
}
|
||||
|
||||
func (d *DNS) setDefaults() {
|
||||
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, DNSUpstreamTypeDot)
|
||||
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.UpstreamPlainAddresses = gosettings.DefaultSlice(d.UpstreamPlainAddresses, []netip.AddrPort{})
|
||||
}
|
||||
|
||||
func defaultDNSProviders() []string {
|
||||
return []string{
|
||||
provider.Cloudflare().Name,
|
||||
}
|
||||
}
|
||||
|
||||
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
for _, addrPort := range d.UpstreamPlainAddresses {
|
||||
if addrPort.Addr().Is4() {
|
||||
return addrPort.Addr()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ipv4 = findPlainIPv4InProviders(d.Providers)
|
||||
if ipv4.IsValid() {
|
||||
return ipv4
|
||||
}
|
||||
|
||||
// Either:
|
||||
// - all upstream plain addresses are IPv6 and no provider is set
|
||||
// - all providers set do not have a plaintext IPv4 address
|
||||
ipv4 = findPlainIPv4InProviders(defaultDNSProviders())
|
||||
if !ipv4.IsValid() {
|
||||
panic("no plaintext IPv4 address found in default DNS providers")
|
||||
}
|
||||
return ipv4
|
||||
}
|
||||
|
||||
func findPlainIPv4InProviders(providerNames []string) netip.Addr {
|
||||
providers := provider.NewProviders()
|
||||
for _, name := range providerNames {
|
||||
provider, err := providers.Get(name)
|
||||
if err != nil {
|
||||
// Settings should be validated before calling this function,
|
||||
// so an error happening here is a programming error.
|
||||
panic(err)
|
||||
}
|
||||
if len(provider.Plain.IPv4) > 0 {
|
||||
return provider.Plain.IPv4[0].Addr()
|
||||
}
|
||||
}
|
||||
return netip.Addr{}
|
||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
||||
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||
d.DoT.setDefaults()
|
||||
}
|
||||
|
||||
func (d DNS) String() string {
|
||||
@@ -186,100 +72,30 @@ func (d DNS) String() string {
|
||||
|
||||
func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS settings:")
|
||||
|
||||
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
|
||||
|
||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||
if len(d.UpstreamPlainAddresses) > 0 {
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
for _, addr := range d.UpstreamPlainAddresses {
|
||||
upstreamResolvers.Append(addr.String())
|
||||
}
|
||||
} else {
|
||||
node.Appendf("Upstream plain addresses: ignored because upstream type is not plain")
|
||||
}
|
||||
} else {
|
||||
for _, provider := range d.Providers {
|
||||
upstreamResolvers.Append(provider)
|
||||
}
|
||||
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
|
||||
if *d.KeepNameserver {
|
||||
return node
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||
node.AppendNode(d.DoT.toLinesNode())
|
||||
return node
|
||||
}
|
||||
|
||||
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
||||
|
||||
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
|
||||
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||
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"))
|
||||
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
|
||||
err = d.DoT.read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Blacklist.read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.readUpstreamPlainAddresses(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("DNS over TLS settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNS) readUpstreamPlainAddresses(r *reader.Reader) (err error) {
|
||||
// If DNS_UPSTREAM_PLAIN_ADDRESSES is set, the user must also set DNS_UPSTREAM_TYPE=plain
|
||||
// for these to be used. This is an added safety measure to reduce misunderstandings, and
|
||||
// reduce odd settings overrides.
|
||||
d.UpstreamPlainAddresses, err = r.CSVNetipAddrPorts("DNS_UPSTREAM_PLAIN_ADDRESSES")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Retro-compatibility - remove in v4
|
||||
// If DNS_ADDRESS is set to a non-localhost address, append it to the other
|
||||
// upstream plain addresses, assuming port 53, and force the upstream type to plain AND
|
||||
// clear any user picked providers, to maintain retro-compatibility behavior.
|
||||
serverAddress, err := r.NetipAddr("DNS_ADDRESS",
|
||||
reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"),
|
||||
reader.IsRetro("DNS_UPSTREAM_PLAIN_ADDRESSES"))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !serverAddress.IsValid() {
|
||||
return nil
|
||||
}
|
||||
isLocalhost := serverAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) == 0
|
||||
if isLocalhost {
|
||||
return nil
|
||||
}
|
||||
const defaultPlainPort = 53
|
||||
addrPort := netip.AddrPortFrom(serverAddress, defaultPlainPort)
|
||||
d.UpstreamPlainAddresses = append(d.UpstreamPlainAddresses, addrPort)
|
||||
d.UpstreamType = DNSUpstreamTypePlain
|
||||
d.Providers = []string{}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_defaultDNSProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
names := defaultDNSProviders()
|
||||
|
||||
found := false
|
||||
providers := provider.NewProviders()
|
||||
for _, name := range names {
|
||||
provider, err := providers.Get(name)
|
||||
require.NoError(t, err)
|
||||
if len(provider.Plain.IPv4) > 0 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "no default DNS provider has a plaintext IPv4 address")
|
||||
}
|
||||
@@ -3,11 +3,10 @@ package settings
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/blockbuilder"
|
||||
"github.com/qdm12/dns/pkg/blacklist"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -22,9 +21,6 @@ type DNSBlacklist struct {
|
||||
AddBlockedHosts []string
|
||||
AddBlockedIPs []netip.Addr
|
||||
AddBlockedIPPrefixes []netip.Prefix
|
||||
// RebindingProtectionExemptHostnames is a list of hostnames
|
||||
// exempt from DNS rebinding protection.
|
||||
RebindingProtectionExemptHostnames []string
|
||||
}
|
||||
|
||||
func (b *DNSBlacklist) setDefaults() {
|
||||
@@ -36,9 +32,8 @@ func (b *DNSBlacklist) setDefaults() {
|
||||
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
||||
|
||||
var (
|
||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt host is not valid")
|
||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||
)
|
||||
|
||||
func (b DNSBlacklist) validate() (err error) {
|
||||
@@ -54,25 +49,18 @@ func (b DNSBlacklist) validate() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||
if !hostRegex.MatchString(host) {
|
||||
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
||||
return DNSBlacklist{
|
||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
|
||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,23 +72,18 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
||||
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
|
||||
other.RebindingProtectionExemptHostnames)
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
||||
settings blockbuilder.Settings,
|
||||
) {
|
||||
return blockbuilder.Settings{
|
||||
Client: client,
|
||||
BlockMalicious: b.BlockMalicious,
|
||||
BlockAds: b.BlockAds,
|
||||
BlockSurveillance: b.BlockSurveillance,
|
||||
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
|
||||
return blacklist.BuilderSettings{
|
||||
BlockMalicious: *b.BlockMalicious,
|
||||
BlockAds: *b.BlockAds,
|
||||
BlockSurveillance: *b.BlockSurveillance,
|
||||
AllowedHosts: b.AllowedHosts,
|
||||
AddBlockedHosts: b.AddBlockedHosts,
|
||||
AddBlockedIPs: b.AddBlockedIPs,
|
||||
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
|
||||
}
|
||||
AddBlockedIPs: netipAddressesToNetaddrIPs(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: netipPrefixesToNetaddrIPPrefixes(b.AddBlockedIPPrefixes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) String() string {
|
||||
@@ -115,37 +98,30 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
|
||||
|
||||
if len(b.AllowedHosts) > 0 {
|
||||
allowedHostsNode := node.Append("Allowed hosts:")
|
||||
allowedHostsNode := node.Appendf("Allowed hosts:")
|
||||
for _, host := range b.AllowedHosts {
|
||||
allowedHostsNode.Append(host)
|
||||
allowedHostsNode.Appendf(host)
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedHosts) > 0 {
|
||||
blockedHostsNode := node.Append("Blocked hosts:")
|
||||
blockedHostsNode := node.Appendf("Blocked hosts:")
|
||||
for _, host := range b.AddBlockedHosts {
|
||||
blockedHostsNode.Append(host)
|
||||
blockedHostsNode.Appendf(host)
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedIPs) > 0 {
|
||||
blockedIPsNode := node.Append("Blocked IP addresses:")
|
||||
blockedIPsNode := node.Appendf("Blocked IP addresses:")
|
||||
for _, ip := range b.AddBlockedIPs {
|
||||
blockedIPsNode.Append(ip.String())
|
||||
blockedIPsNode.Appendf(ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedIPPrefixes) > 0 {
|
||||
blockedIPPrefixesNode := node.Append("Blocked IP networks:")
|
||||
blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
|
||||
for _, ipNetwork := range b.AddBlockedIPPrefixes {
|
||||
blockedIPPrefixesNode.Append(ipNetwork.String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.RebindingProtectionExemptHostnames) > 0 {
|
||||
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
|
||||
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||
exemptHostsNode.Append(host)
|
||||
blockedIPPrefixesNode.Appendf(ipNetwork.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,47 +145,24 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
|
||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
|
||||
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
|
||||
|
||||
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
|
||||
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
|
||||
|
||||
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
|
||||
}
|
||||
var (
|
||||
ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||
)
|
||||
|
||||
// TODO v4 remove this block below
|
||||
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ips = append(ips, privateIPs...)
|
||||
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
|
||||
|
||||
return ips, ipPrefixes, nil
|
||||
}
|
||||
|
||||
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||
|
||||
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
|
||||
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
|
||||
ipPrefixes []netip.Prefix, err error) {
|
||||
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
||||
if len(privateAddresses) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
129
internal/configuration/settings/dot.go
Normal file
129
internal/configuration/settings/dot.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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 and cryptographic files for DNSSEC validation.
|
||||
// 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
|
||||
// Unbound contains settings to configure Unbound.
|
||||
Unbound Unbound
|
||||
// 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)
|
||||
}
|
||||
|
||||
err = d.Unbound.validate()
|
||||
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),
|
||||
Unbound: d.Unbound.copy(),
|
||||
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.Unbound.overrideWith(other.Unbound)
|
||||
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.Unbound.setDefaults()
|
||||
d.Blacklist.setDefaults()
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
node.AppendNode(d.Unbound.toLinesNode())
|
||||
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
|
||||
}
|
||||
|
||||
err = d.Unbound.read(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Blacklist.read(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -30,14 +30,13 @@ var (
|
||||
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
|
||||
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
|
||||
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
|
||||
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
|
||||
ErrRegionNotValid = errors.New("the region specified is not valid")
|
||||
ErrServerAddressNotValid = errors.New("server listening address is not valid")
|
||||
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
|
||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
|
||||
ErrUpdaterProtonUsernameMissing = errors.New("proton username is missing")
|
||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||
|
||||
@@ -104,6 +104,7 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
|
||||
if len(f.OutboundSubnets) > 0 {
|
||||
outboundSubnets := node.Appendf("Outbound subnets:")
|
||||
for _, subnet := range f.OutboundSubnets {
|
||||
subnet := subnet
|
||||
outboundSubnets.Appendf("%s", &subnet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ func Test_Firewall_validate(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
@@ -17,17 +17,23 @@ type Health struct {
|
||||
// for the health check server.
|
||||
// It cannot be the empty string in the internal state.
|
||||
ServerAddress string
|
||||
// ReadHeaderTimeout is the HTTP server header read timeout
|
||||
// duration of the HTTP server. It defaults to 100 milliseconds.
|
||||
ReadHeaderTimeout time.Duration
|
||||
// ReadTimeout is the HTTP read timeout duration of the
|
||||
// HTTP server. It defaults to 500 milliseconds.
|
||||
ReadTimeout time.Duration
|
||||
// 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.
|
||||
TargetAddress string
|
||||
// ICMPTargetIP is the IP address to use for ICMP echo requests
|
||||
// in the health checker. It can be set to an unspecified address (0.0.0.0)
|
||||
// such that the VPN server IP is used, which is also the default behavior.
|
||||
ICMPTargetIP netip.Addr
|
||||
// RestartVPN indicates whether to restart the VPN connection
|
||||
// when the healthcheck fails.
|
||||
RestartVPN *bool
|
||||
// SuccessWait is the duration to wait to re-run the
|
||||
// healthcheck after a successful healthcheck.
|
||||
// It defaults to 5 seconds and cannot be zero in
|
||||
// the internal state.
|
||||
SuccessWait time.Duration
|
||||
// VPN has health settings specific to the VPN loop.
|
||||
VPN HealthyWait
|
||||
}
|
||||
|
||||
func (h Health) Validate() (err error) {
|
||||
@@ -36,15 +42,22 @@ func (h Health) Validate() (err error) {
|
||||
return fmt.Errorf("server listening address is not valid: %w", err)
|
||||
}
|
||||
|
||||
err = h.VPN.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("health VPN settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Health) copy() (copied Health) {
|
||||
return Health{
|
||||
ServerAddress: h.ServerAddress,
|
||||
TargetAddress: h.TargetAddress,
|
||||
ICMPTargetIP: h.ICMPTargetIP,
|
||||
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
||||
ServerAddress: h.ServerAddress,
|
||||
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||
ReadTimeout: h.ReadTimeout,
|
||||
TargetAddress: h.TargetAddress,
|
||||
SuccessWait: h.SuccessWait,
|
||||
VPN: h.VPN.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,16 +66,23 @@ func (h *Health) copy() (copied Health) {
|
||||
// settings.
|
||||
func (h *Health) OverrideWith(other Health) {
|
||||
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
||||
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
||||
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
||||
h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
|
||||
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
|
||||
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
|
||||
h.VPN.overrideWith(other.VPN)
|
||||
}
|
||||
|
||||
func (h *Health) SetDefaults() {
|
||||
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
||||
const defaultReadHeaderTimeout = 100 * time.Millisecond
|
||||
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||
const defaultReadTimeout = 500 * time.Millisecond
|
||||
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
||||
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
||||
h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
|
||||
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
|
||||
const defaultSuccessWait = 5 * time.Second
|
||||
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
|
||||
h.VPN.setDefaults()
|
||||
}
|
||||
|
||||
func (h Health) String() string {
|
||||
@@ -73,12 +93,10 @@ func (h Health) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Health settings:")
|
||||
node.Appendf("Server listening address: %s", h.ServerAddress)
|
||||
node.Appendf("Target address: %s", h.TargetAddress)
|
||||
icmpTarget := "VPN server IP"
|
||||
if !h.ICMPTargetIP.IsUnspecified() {
|
||||
icmpTarget = h.ICMPTargetIP.String()
|
||||
}
|
||||
node.Appendf("ICMP target IP: %s", icmpTarget)
|
||||
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
|
||||
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
|
||||
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
||||
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
||||
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -86,13 +104,16 @@ func (h *Health) Read(r *reader.Reader) (err error) {
|
||||
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
||||
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
|
||||
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
|
||||
h.ICMPTargetIP, err = r.NetipAddr("HEALTH_ICMP_TARGET_IP")
|
||||
|
||||
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
|
||||
|
||||
err = h.VPN.read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("VPN health settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
76
internal/configuration/settings/healthywait.go
Normal file
76
internal/configuration/settings/healthywait.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type HealthyWait struct {
|
||||
// Initial is the initial duration to wait for the program
|
||||
// to be healthy before taking action.
|
||||
// It cannot be nil in the internal state.
|
||||
Initial *time.Duration
|
||||
// Addition is the duration to add to the Initial duration
|
||||
// after Initial has expired to wait longer for the program
|
||||
// to be healthy.
|
||||
// It cannot be nil in the internal state.
|
||||
Addition *time.Duration
|
||||
}
|
||||
|
||||
func (h HealthyWait) validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HealthyWait) copy() (copied HealthyWait) {
|
||||
return HealthyWait{
|
||||
Initial: gosettings.CopyPointer(h.Initial),
|
||||
Addition: gosettings.CopyPointer(h.Addition),
|
||||
}
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (h *HealthyWait) overrideWith(other HealthyWait) {
|
||||
h.Initial = gosettings.OverrideWithPointer(h.Initial, other.Initial)
|
||||
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition)
|
||||
}
|
||||
|
||||
func (h *HealthyWait) setDefaults() {
|
||||
const initialDurationDefault = 6 * time.Second
|
||||
const additionDurationDefault = 5 * time.Second
|
||||
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault)
|
||||
h.Addition = gosettings.DefaultPointer(h.Addition, additionDurationDefault)
|
||||
}
|
||||
|
||||
func (h HealthyWait) String() string {
|
||||
return h.toLinesNode("Health").String()
|
||||
}
|
||||
|
||||
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
|
||||
node = gotree.New(kind + " wait durations:")
|
||||
node.Appendf("Initial duration: %s", *h.Initial)
|
||||
node.Appendf("Additional duration: %s", *h.Addition)
|
||||
return node
|
||||
}
|
||||
|
||||
func (h *HealthyWait) read(r *reader.Reader) (err error) {
|
||||
h.Initial, err = r.DurationPtr(
|
||||
"HEALTH_VPN_DURATION_INITIAL",
|
||||
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Addition, err = r.DurationPtr(
|
||||
"HEALTH_VPN_DURATION_ADDITION",
|
||||
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,30 +1,4 @@
|
||||
package settings
|
||||
|
||||
import gomock "github.com/golang/mock/gomock"
|
||||
|
||||
type sourceKeyValue struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
func newMockSource(ctrl *gomock.Controller, keyValues []sourceKeyValue) *MockSource {
|
||||
source := NewMockSource(ctrl)
|
||||
var previousCall *gomock.Call
|
||||
for _, keyValue := range keyValues {
|
||||
transformedKey := keyValue.key
|
||||
keyTransformCall := source.EXPECT().KeyTransform(keyValue.key).Return(transformedKey)
|
||||
if previousCall != nil {
|
||||
keyTransformCall.After(previousCall)
|
||||
}
|
||||
isSet := keyValue.value != ""
|
||||
previousCall = source.EXPECT().Get(transformedKey).
|
||||
Return(keyValue.value, isSet).After(keyTransformCall)
|
||||
if isSet {
|
||||
previousCall = source.EXPECT().KeyTransform(keyValue.key).
|
||||
Return(transformedKey).After(previousCall)
|
||||
previousCall = source.EXPECT().String().
|
||||
Return("mock source").After(previousCall)
|
||||
}
|
||||
}
|
||||
return source
|
||||
}
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
func uint8Ptr(n uint8) *uint8 { return &n }
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package settings
|
||||
|
||||
type Warner interface {
|
||||
Warn(message string)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package settings
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Warner
|
||||
//go:generate mockgen -destination=mocks_reader_test.go -package=$GOPACKAGE github.com/qdm12/gosettings/reader Source
|
||||
@@ -1,77 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gosettings/reader (interfaces: Source)
|
||||
|
||||
// Package settings is a generated GoMock package.
|
||||
package settings
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockSource is a mock of Source interface.
|
||||
type MockSource struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSourceMockRecorder
|
||||
}
|
||||
|
||||
// MockSourceMockRecorder is the mock recorder for MockSource.
|
||||
type MockSourceMockRecorder struct {
|
||||
mock *MockSource
|
||||
}
|
||||
|
||||
// NewMockSource creates a new mock instance.
|
||||
func NewMockSource(ctrl *gomock.Controller) *MockSource {
|
||||
mock := &MockSource{ctrl: ctrl}
|
||||
mock.recorder = &MockSourceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSource) EXPECT() *MockSourceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockSource) Get(arg0 string) (string, bool) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(bool)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockSourceMockRecorder) Get(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSource)(nil).Get), arg0)
|
||||
}
|
||||
|
||||
// KeyTransform mocks base method.
|
||||
func (m *MockSource) KeyTransform(arg0 string) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "KeyTransform", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// KeyTransform indicates an expected call of KeyTransform.
|
||||
func (mr *MockSourceMockRecorder) KeyTransform(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyTransform", reflect.TypeOf((*MockSource)(nil).KeyTransform), arg0)
|
||||
}
|
||||
|
||||
// String mocks base method.
|
||||
func (m *MockSource) String() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "String")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// String indicates an expected call of String.
|
||||
func (mr *MockSourceMockRecorder) String() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockSource)(nil).String))
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/configuration/settings (interfaces: Warner)
|
||||
|
||||
// Package settings is a generated GoMock package.
|
||||
package settings
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockWarner is a mock of Warner interface.
|
||||
type MockWarner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockWarnerMockRecorder
|
||||
}
|
||||
|
||||
// MockWarnerMockRecorder is the mock recorder for MockWarner.
|
||||
type MockWarnerMockRecorder struct {
|
||||
mock *MockWarner
|
||||
}
|
||||
|
||||
// NewMockWarner creates a new mock instance.
|
||||
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
|
||||
mock := &MockWarner{ctrl: ctrl}
|
||||
mock.recorder = &MockWarnerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Warn mocks base method.
|
||||
func (m *MockWarner) Warn(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Warn", arg0)
|
||||
}
|
||||
|
||||
// Warn indicates an expected call of Warn.
|
||||
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
|
||||
}
|
||||
36
internal/configuration/settings/netaddr.go
Normal file
36
internal/configuration/settings/netaddr.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func netipAddressToNetaddrIP(address netip.Addr) (ip netaddr.IP) {
|
||||
if address.Is4() {
|
||||
return netaddr.IPFrom4(address.As4())
|
||||
}
|
||||
return netaddr.IPFrom16(address.As16())
|
||||
}
|
||||
|
||||
func netipAddressesToNetaddrIPs(addresses []netip.Addr) (ips []netaddr.IP) {
|
||||
ips = make([]netaddr.IP, len(addresses))
|
||||
for i := range addresses {
|
||||
ips[i] = netipAddressToNetaddrIP(addresses[i])
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func netipPrefixToNetaddrIPPrefix(prefix netip.Prefix) (ipPrefix netaddr.IPPrefix) {
|
||||
netaddrIP := netipAddressToNetaddrIP(prefix.Addr())
|
||||
bits := prefix.Bits()
|
||||
return netaddr.IPPrefixFrom(netaddrIP, uint8(bits))
|
||||
}
|
||||
|
||||
func netipPrefixesToNetaddrIPPrefixes(prefixes []netip.Prefix) (ipPrefixes []netaddr.IPPrefix) {
|
||||
ipPrefixes = make([]netaddr.IPPrefix, len(prefixes))
|
||||
for i := range ipPrefixes {
|
||||
ipPrefixes[i] = netipPrefixToNetaddrIPPrefix(prefixes[i])
|
||||
}
|
||||
return ipPrefixes
|
||||
}
|
||||
@@ -4,8 +4,7 @@ package settings
|
||||
// and SERVER_REGIONS is now the continent field for servers.
|
||||
// TODO v4 remove.
|
||||
func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) (
|
||||
updatedSelection ServerSelection,
|
||||
) {
|
||||
updatedSelection ServerSelection) {
|
||||
validRegionsMap := stringSliceToMap(validRegions)
|
||||
validCountriesMap := stringSliceToMap(validCountries)
|
||||
|
||||
|
||||
@@ -155,8 +155,7 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
|
||||
}
|
||||
|
||||
func validateOpenVPNConfigFilepath(isCustom bool,
|
||||
confFile string,
|
||||
) (err error) {
|
||||
confFile string) (err error) {
|
||||
if !isCustom {
|
||||
return nil
|
||||
}
|
||||
@@ -180,8 +179,7 @@ func validateOpenVPNConfigFilepath(isCustom bool,
|
||||
}
|
||||
|
||||
func validateOpenVPNClientCertificate(vpnProvider,
|
||||
clientCert string,
|
||||
) (err error) {
|
||||
clientCert string) (err error) {
|
||||
switch vpnProvider {
|
||||
case
|
||||
providers.Airvpn,
|
||||
@@ -228,8 +226,7 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
|
||||
}
|
||||
|
||||
func validateOpenVPNEncryptedKey(vpnProvider,
|
||||
encryptedPrivateKey string,
|
||||
) (err error) {
|
||||
encryptedPrivateKey string) (err error) {
|
||||
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
|
||||
return fmt.Errorf("%w", ErrMissingValue)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ func Test_ivpnAccountID(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
|
||||
// Validate TCP
|
||||
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
|
||||
providers.Giganews,
|
||||
providers.Ipvanish,
|
||||
providers.Perfectprivacy,
|
||||
providers.Privado,
|
||||
@@ -68,7 +67,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
providers.Privatevpn, providers.Torguard:
|
||||
// no custom port allowed
|
||||
case providers.Expressvpn, providers.Fastestvpn,
|
||||
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||
providers.Ipvanish, providers.Nordvpn,
|
||||
providers.Privado, providers.Purevpn,
|
||||
providers.Surfshark, providers.VPNSecure,
|
||||
providers.VPNUnlimited, providers.Vyprvpn:
|
||||
@@ -193,6 +192,9 @@ func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
|
||||
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
|
||||
|
||||
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
|
||||
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
|
||||
|
||||
@@ -29,14 +29,6 @@ type PortForwarding struct {
|
||||
// to write to a file. It cannot be nil for the
|
||||
// internal state
|
||||
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
|
||||
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||
// is its default as well.
|
||||
@@ -60,7 +52,6 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
validProviders := []string{
|
||||
providers.Perfectprivacy,
|
||||
providers.PrivateInternetAccess,
|
||||
providers.Privatevpn,
|
||||
providers.Protonvpn,
|
||||
}
|
||||
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
|
||||
@@ -92,8 +83,6 @@ func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Provider: gosettings.CopyPointer(p.Provider),
|
||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||
Username: p.Username,
|
||||
Password: p.Password,
|
||||
@@ -104,8 +93,6 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||
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.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||
@@ -115,8 +102,6 @@ func (p *PortForwarding) setDefaults() {
|
||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -149,13 +134,6 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
||||
}
|
||||
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 != "" {
|
||||
credentialsNode := node.Appendf("Credentials:")
|
||||
credentialsNode.Appendf("Username: %s", p.Username)
|
||||
@@ -184,12 +162,6 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
|
||||
"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")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,7 +10,7 @@ func Test_PortForwarding_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := PortForwarding{
|
||||
Enabled: ptrTo(false),
|
||||
Enabled: boolPtr(false),
|
||||
}
|
||||
|
||||
s := settings.String()
|
||||
|
||||
@@ -25,7 +25,7 @@ type Provider struct {
|
||||
}
|
||||
|
||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
|
||||
func (p *Provider) validate(vpnType string, storage Storage) (err error) {
|
||||
// Validate Name
|
||||
var validNames []string
|
||||
if vpnType == vpn.OpenVPN {
|
||||
@@ -48,7 +48,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
|
||||
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
|
||||
}
|
||||
|
||||
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
|
||||
err = p.ServerSelection.validate(p.Name, storage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server selection: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package settings
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/publicip/api"
|
||||
"github.com/qdm12/gosettings"
|
||||
@@ -12,28 +13,24 @@ import (
|
||||
|
||||
// PublicIP contains settings for port forwarding.
|
||||
type PublicIP struct {
|
||||
// Enabled is set to true to fetch the public ip address
|
||||
// information on VPN connection. It defaults to true.
|
||||
Enabled *bool
|
||||
// Period is the period to get the public IP address.
|
||||
// It can be set to 0 to disable periodic checking.
|
||||
// It cannot be nil for the internal state.
|
||||
// TODO change to value and add enabled field
|
||||
Period *time.Duration
|
||||
// IPFilepath is the public IP address status file path
|
||||
// to use. It can be the empty string to indicate not
|
||||
// to write to a file. It cannot be nil for the
|
||||
// internal state
|
||||
IPFilepath *string
|
||||
// APIs is the list of public ip APIs to use to fetch public IP information.
|
||||
// If there is more than one API, the first one is used
|
||||
// by default and the others are used as fallbacks in case of
|
||||
// the service rate limiting us. It defaults to use all services,
|
||||
// with the first one being ipinfo.io for historical reasons.
|
||||
APIs []PublicIPAPI
|
||||
}
|
||||
|
||||
type PublicIPAPI struct {
|
||||
// Name is the name of the public ip API service.
|
||||
// It can be "cloudflare", "ifconfigco", "ip2location" or "ipinfo".
|
||||
Name string
|
||||
// Token is the token to use for the public ip API service.
|
||||
Token string
|
||||
// API is the API name to use to fetch public IP information.
|
||||
// It can be ipinfo or ip2location. It defaults to ipinfo.
|
||||
API string
|
||||
// APIToken is the token to use for the IP data service
|
||||
// such as ipinfo.io. It can be the empty string to
|
||||
// indicate not to use a token. It cannot be nil for the
|
||||
// internal state.
|
||||
APIToken *string
|
||||
}
|
||||
|
||||
// UpdateWith deep copies the receiving settings, overrides the copy with
|
||||
@@ -51,6 +48,12 @@ func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP,
|
||||
}
|
||||
|
||||
func (p PublicIP) validate() (err error) {
|
||||
const minPeriod = 5 * time.Second
|
||||
if *p.Period < minPeriod {
|
||||
return fmt.Errorf("%w: %s must be at least %s",
|
||||
ErrPublicIPPeriodTooShort, p.Period, minPeriod)
|
||||
}
|
||||
|
||||
if *p.IPFilepath != "" { // optional
|
||||
_, err := filepath.Abs(*p.IPFilepath)
|
||||
if err != nil {
|
||||
@@ -58,11 +61,9 @@ func (p PublicIP) validate() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, publicIPAPI := range p.APIs {
|
||||
_, err = api.ParseProvider(publicIPAPI.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API name: %w", err)
|
||||
}
|
||||
_, err = api.ParseProvider(p.API)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API name: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -70,27 +71,26 @@ func (p PublicIP) validate() (err error) {
|
||||
|
||||
func (p *PublicIP) copy() (copied PublicIP) {
|
||||
return PublicIP{
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Period: gosettings.CopyPointer(p.Period),
|
||||
IPFilepath: gosettings.CopyPointer(p.IPFilepath),
|
||||
APIs: gosettings.CopySlice(p.APIs),
|
||||
API: p.API,
|
||||
APIToken: gosettings.CopyPointer(p.APIToken),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PublicIP) overrideWith(other PublicIP) {
|
||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||
p.Period = gosettings.OverrideWithPointer(p.Period, other.Period)
|
||||
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
|
||||
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
|
||||
p.API = gosettings.OverrideWithComparable(p.API, other.API)
|
||||
p.APIToken = gosettings.OverrideWithPointer(p.APIToken, other.APIToken)
|
||||
}
|
||||
|
||||
func (p *PublicIP) setDefaults() {
|
||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
|
||||
const defaultPeriod = 12 * time.Hour
|
||||
p.Period = gosettings.DefaultPointer(p.Period, defaultPeriod)
|
||||
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
|
||||
p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
|
||||
{Name: string(api.IPInfo)},
|
||||
{Name: string(api.Cloudflare)},
|
||||
{Name: string(api.IfConfigCo)},
|
||||
{Name: string(api.IP2Location)},
|
||||
})
|
||||
p.API = gosettings.DefaultComparable(p.API, "ipinfo")
|
||||
p.APIToken = gosettings.DefaultPointer(p.APIToken, "")
|
||||
}
|
||||
|
||||
func (p PublicIP) String() string {
|
||||
@@ -98,78 +98,41 @@ func (p PublicIP) String() string {
|
||||
}
|
||||
|
||||
func (p PublicIP) toLinesNode() (node *gotree.Node) {
|
||||
if !*p.Enabled {
|
||||
return gotree.New("Public IP settings: disabled")
|
||||
node = gotree.New("Public IP settings:")
|
||||
|
||||
if *p.Period == 0 {
|
||||
node.Appendf("Enabled: no")
|
||||
return node
|
||||
}
|
||||
|
||||
node = gotree.New("Public IP settings:")
|
||||
updatePeriod := "disabled"
|
||||
if *p.Period > 0 {
|
||||
updatePeriod = "every " + p.Period.String()
|
||||
}
|
||||
node.Appendf("Fetching: %s", updatePeriod)
|
||||
|
||||
if *p.IPFilepath != "" {
|
||||
node.Appendf("IP file path: %s", *p.IPFilepath)
|
||||
}
|
||||
|
||||
baseAPIString := "Public IP data base API: " + p.APIs[0].Name
|
||||
if p.APIs[0].Token != "" {
|
||||
baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
|
||||
}
|
||||
node.Append(baseAPIString)
|
||||
if len(p.APIs) > 1 {
|
||||
backupAPIsNode := node.Append("Public IP data backup APIs:")
|
||||
for i := 1; i < len(p.APIs); i++ {
|
||||
message := p.APIs[i].Name
|
||||
if p.APIs[i].Token != "" {
|
||||
message += " (token " + gosettings.ObfuscateKey(p.APIs[i].Token) + ")"
|
||||
}
|
||||
backupAPIsNode.Append(message)
|
||||
}
|
||||
node.Appendf("Public IP data API: %s", p.API)
|
||||
|
||||
if *p.APIToken != "" {
|
||||
node.Appendf("API token: %s", gosettings.ObfuscateKey(*p.APIToken))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
|
||||
p.Enabled, err = readPublicIPEnabled(r, warner)
|
||||
func (p *PublicIP) read(r *reader.Reader) (err error) {
|
||||
p.Period, err = r.DurationPtr("PUBLICIP_PERIOD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.IPFilepath = r.Get("PUBLICIP_FILE",
|
||||
reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))
|
||||
|
||||
apiNames := r.CSV("PUBLICIP_API")
|
||||
if len(apiNames) > 0 {
|
||||
apiTokens := r.CSV("PUBLICIP_API_TOKEN")
|
||||
p.APIs = make([]PublicIPAPI, len(apiNames))
|
||||
for i := range apiNames {
|
||||
p.APIs[i].Name = apiNames[i]
|
||||
var token string
|
||||
if i < len(apiTokens) { // only set token if it exists
|
||||
token = apiTokens[i]
|
||||
}
|
||||
p.APIs[i].Token = token
|
||||
}
|
||||
}
|
||||
|
||||
p.API = r.String("PUBLICIP_API")
|
||||
p.APIToken = r.Get("PUBLICIP_API_TOKEN")
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPublicIPEnabled(r *reader.Reader, warner Warner) (
|
||||
enabled *bool, err error,
|
||||
) {
|
||||
periodPtr, err := r.DurationPtr("PUBLICIP_PERIOD") // Retro-compatibility
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if periodPtr == nil {
|
||||
return r.BoolPtr("PUBLICIP_ENABLED")
|
||||
}
|
||||
|
||||
if *periodPtr == 0 {
|
||||
warner.Warn("please replace PUBLICIP_PERIOD=0 with PUBLICIP_ENABLED=no")
|
||||
return ptrTo(false), nil
|
||||
}
|
||||
|
||||
warner.Warn("PUBLICIP_PERIOD is no longer used. " +
|
||||
"It is assumed from its non-zero value you want PUBLICIP_ENABLED=yes. " +
|
||||
"Please migrate to use PUBLICIP_ENABLED only in the future.")
|
||||
return ptrTo(true), nil
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_PublicIP_read(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
makeReader func(ctrl *gomock.Controller) *reader.Reader
|
||||
makeWarner func(ctrl *gomock.Controller) Warner
|
||||
settings PublicIP
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"nothing_read": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
},
|
||||
"single_api_no_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo"},
|
||||
{key: "PUBLICIP_API_TOKEN"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"single_api_with_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo"},
|
||||
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo", Token: "xyz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple_apis_no_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||
{key: "PUBLICIP_API_TOKEN"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo"},
|
||||
{Name: "ip2location"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple_apis_with_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||
{key: "PUBLICIP_API_TOKEN", value: "xyz,abc"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo", Token: "xyz"},
|
||||
{Name: "ip2location", Token: "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple_apis_with_and_without_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo", Token: "xyz"},
|
||||
{Name: "ip2location"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
reader := testCase.makeReader(ctrl)
|
||||
var warner Warner
|
||||
if testCase.makeWarner != nil {
|
||||
warner = testCase.makeWarner(ctrl)
|
||||
}
|
||||
|
||||
var settings PublicIP
|
||||
err := settings.read(reader, warner)
|
||||
|
||||
assert.Equal(t, testCase.settings, settings)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -27,9 +24,6 @@ type ControlServer struct {
|
||||
// It cannot be empty in the internal state and defaults to
|
||||
// /gluetun/auth/config.toml.
|
||||
AuthFilePath string
|
||||
// AuthDefaultRole is a JSON encoded object defining the default role
|
||||
// that applies to all routes without a previously user-defined role assigned to.
|
||||
AuthDefaultRole string
|
||||
}
|
||||
|
||||
func (c ControlServer) validate() (err error) {
|
||||
@@ -50,30 +44,14 @@ func (c ControlServer) validate() (err error) {
|
||||
ErrControlServerPrivilegedPort, port, uid)
|
||||
}
|
||||
|
||||
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
|
||||
jsonDecoder.DisallowUnknownFields()
|
||||
var role auth.Role
|
||||
err = jsonDecoder.Decode(&role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
|
||||
}
|
||||
|
||||
if role.Auth != "" {
|
||||
err = role.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("default authentication role is not valid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ControlServer) copy() (copied ControlServer) {
|
||||
return ControlServer{
|
||||
Address: gosettings.CopyPointer(c.Address),
|
||||
Log: gosettings.CopyPointer(c.Log),
|
||||
AuthFilePath: c.AuthFilePath,
|
||||
AuthDefaultRole: c.AuthDefaultRole,
|
||||
Address: gosettings.CopyPointer(c.Address),
|
||||
Log: gosettings.CopyPointer(c.Log),
|
||||
AuthFilePath: c.AuthFilePath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,21 +62,12 @@ func (c *ControlServer) overrideWith(other ControlServer) {
|
||||
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
||||
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
||||
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
|
||||
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
|
||||
}
|
||||
|
||||
func (c *ControlServer) setDefaults() {
|
||||
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
||||
c.Log = gosettings.DefaultPointer(c.Log, true)
|
||||
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
|
||||
c.AuthDefaultRole = gosettings.DefaultComparable(c.AuthDefaultRole, "{}")
|
||||
if c.AuthDefaultRole != "{}" {
|
||||
var role auth.Role
|
||||
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||
role.Name = "default"
|
||||
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
|
||||
c.AuthDefaultRole = string(roleBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (c ControlServer) String() string {
|
||||
@@ -110,11 +79,6 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("Listening address: %s", *c.Address)
|
||||
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
||||
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
||||
if c.AuthDefaultRole != "{}" {
|
||||
var role auth.Role
|
||||
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||
node.AppendNode(role.ToLinesNode())
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -127,7 +91,6 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
|
||||
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
||||
|
||||
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
|
||||
c.AuthDefaultRole = r.String("HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type ServerSelection struct {
|
||||
type ServerSelection struct { //nolint:maligned
|
||||
// VPN is the VPN type which can be 'openvpn'
|
||||
// or 'wireguard'. It cannot be the empty string
|
||||
// in the internal state.
|
||||
@@ -91,15 +91,14 @@ var (
|
||||
)
|
||||
|
||||
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
filterChoicesGetter FilterChoicesGetter, warner Warner,
|
||||
) (err error) {
|
||||
storage Storage) (err error) {
|
||||
switch ss.VPN {
|
||||
case vpn.OpenVPN, vpn.Wireguard:
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
||||
}
|
||||
|
||||
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
|
||||
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, storage)
|
||||
if err != nil {
|
||||
return err // already wrapped error
|
||||
}
|
||||
@@ -112,7 +111,7 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
*ss = surfsharkRetroRegion(*ss)
|
||||
}
|
||||
|
||||
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)
|
||||
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
|
||||
}
|
||||
@@ -143,20 +142,19 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
}
|
||||
|
||||
func getLocationFilterChoices(vpnServiceProvider string,
|
||||
ss *ServerSelection, filterChoicesGetter FilterChoicesGetter, warner Warner) (
|
||||
filterChoices models.FilterChoices, err error,
|
||||
) {
|
||||
filterChoices = filterChoicesGetter.GetFilterChoices(vpnServiceProvider)
|
||||
ss *ServerSelection, storage Storage) (filterChoices models.FilterChoices,
|
||||
err error) {
|
||||
filterChoices = storage.GetFilterChoices(vpnServiceProvider)
|
||||
|
||||
if vpnServiceProvider == providers.Surfshark {
|
||||
// // Retro compatibility
|
||||
// TODO v4 remove
|
||||
newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic
|
||||
err := atLeastOneIsOneOfCaseInsensitive(ss.Regions, newAndRetroRegions, warner)
|
||||
err := validate.AreAllOneOfCaseInsensitive(ss.Regions, newAndRetroRegions)
|
||||
if err != nil {
|
||||
// Only return error comparing with newer regions, we don't want to confuse the user
|
||||
// with the retro regions in the error message.
|
||||
err = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner)
|
||||
err = validate.AreAllOneOfCaseInsensitive(ss.Regions, filterChoices.Regions)
|
||||
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||
}
|
||||
}
|
||||
@@ -167,29 +165,28 @@ func getLocationFilterChoices(vpnServiceProvider string,
|
||||
// validateServerFilters validates filters against the choices given as arguments.
|
||||
// Set an argument to nil to pass the check for a particular filter.
|
||||
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices,
|
||||
vpnServiceProvider string, warner Warner,
|
||||
) (err error) {
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
|
||||
vpnServiceProvider string) (err error) {
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Countries, filterChoices.Countries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
|
||||
}
|
||||
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Regions, filterChoices.Regions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||
}
|
||||
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Cities, filterChoices.Cities)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
|
||||
}
|
||||
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
|
||||
}
|
||||
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
|
||||
}
|
||||
@@ -208,12 +205,12 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
|
||||
ErrNameNotValid, len(settings.Names))
|
||||
}
|
||||
}
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner)
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Names, filterChoices.Names)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
|
||||
}
|
||||
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Categories, filterChoices.Categories)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
|
||||
}
|
||||
@@ -221,43 +218,6 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
|
||||
return nil
|
||||
}
|
||||
|
||||
func atLeastOneIsOneOfCaseInsensitive(values, choices []string,
|
||||
warner Warner,
|
||||
) (err error) {
|
||||
if len(values) > 0 && len(choices) == 0 {
|
||||
return fmt.Errorf("%w", validate.ErrNoChoice)
|
||||
}
|
||||
|
||||
set := make(map[string]struct{}, len(choices))
|
||||
for _, choice := range choices {
|
||||
lowercaseChoice := strings.ToLower(choice)
|
||||
set[lowercaseChoice] = struct{}{}
|
||||
}
|
||||
|
||||
invalidValues := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
lowercaseValue := strings.ToLower(value)
|
||||
_, ok := set[lowercaseValue]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
invalidValues = append(invalidValues, value)
|
||||
}
|
||||
|
||||
switch len(invalidValues) {
|
||||
case 0:
|
||||
return nil
|
||||
case len(values):
|
||||
return fmt.Errorf("%w: none of %s is one of the choices available %s",
|
||||
validate.ErrValueNotOneOf, strings.Join(values, ", "), strings.Join(choices, ", "))
|
||||
default:
|
||||
warner.Warn(fmt.Sprintf("values %s are not in choices %s",
|
||||
strings.Join(invalidValues, ", "), strings.Join(choices, ", ")))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvider string) error {
|
||||
switch {
|
||||
case *settings.FreeOnly &&
|
||||
@@ -354,8 +314,11 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled
|
||||
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
||||
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
||||
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
||||
defaultPortForwardOnly := portForwardingEnabled &&
|
||||
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
|
||||
defaultPortForwardOnly := false
|
||||
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
|
||||
providers.PrivateInternetAccess, providers.Protonvpn) {
|
||||
defaultPortForwardOnly = true
|
||||
}
|
||||
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
||||
ss.OpenVPN.setDefaults(vpnProvider)
|
||||
ss.Wireguard.setDefaults()
|
||||
@@ -457,8 +420,7 @@ func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
|
||||
}
|
||||
|
||||
func (ss *ServerSelection) read(r *reader.Reader,
|
||||
vpnProvider, vpnType string,
|
||||
) (err error) {
|
||||
vpnProvider, vpnType string) (err error) {
|
||||
ss.VPN = vpnType
|
||||
|
||||
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||
|
||||
@@ -2,6 +2,7 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
@@ -21,7 +22,6 @@ type Settings struct {
|
||||
Log Log
|
||||
PublicIP PublicIP
|
||||
Shadowsocks Shadowsocks
|
||||
Storage Storage
|
||||
System System
|
||||
Updater Updater
|
||||
Version Version
|
||||
@@ -29,16 +29,14 @@ type Settings struct {
|
||||
Pprof pprof.Settings
|
||||
}
|
||||
|
||||
type FilterChoicesGetter interface {
|
||||
type Storage interface {
|
||||
GetFilterChoices(provider string) models.FilterChoices
|
||||
}
|
||||
|
||||
// Validate validates all the settings and returns an error
|
||||
// if one of them is not valid.
|
||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||
func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool,
|
||||
warner Warner,
|
||||
) (err error) {
|
||||
func (s *Settings) Validate(storage Storage, ipv6Supported bool) (err error) {
|
||||
nameToValidation := map[string]func() error{
|
||||
"control server": s.ControlServer.validate,
|
||||
"dns": s.DNS.validate,
|
||||
@@ -48,13 +46,12 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
|
||||
"log": s.Log.validate,
|
||||
"public ip check": s.PublicIP.validate,
|
||||
"shadowsocks": s.Shadowsocks.validate,
|
||||
"storage": s.Storage.validate,
|
||||
"system": s.System.validate,
|
||||
"updater": s.Updater.Validate,
|
||||
"version": s.Version.validate,
|
||||
// Pprof validation done in pprof constructor
|
||||
"VPN": func() error {
|
||||
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||
return s.VPN.Validate(storage, ipv6Supported)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -78,7 +75,6 @@ func (s *Settings) copy() (copied Settings) {
|
||||
Log: s.Log.copy(),
|
||||
PublicIP: s.PublicIP.copy(),
|
||||
Shadowsocks: s.Shadowsocks.copy(),
|
||||
Storage: s.Storage.copy(),
|
||||
System: s.System.copy(),
|
||||
Updater: s.Updater.copy(),
|
||||
Version: s.Version.copy(),
|
||||
@@ -88,8 +84,7 @@ func (s *Settings) copy() (copied Settings) {
|
||||
}
|
||||
|
||||
func (s *Settings) OverrideWith(other Settings,
|
||||
filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner,
|
||||
) (err error) {
|
||||
storage Storage, ipv6Supported bool) (err error) {
|
||||
patchedSettings := s.copy()
|
||||
patchedSettings.ControlServer.overrideWith(other.ControlServer)
|
||||
patchedSettings.DNS.overrideWith(other.DNS)
|
||||
@@ -99,13 +94,12 @@ func (s *Settings) OverrideWith(other Settings,
|
||||
patchedSettings.Log.overrideWith(other.Log)
|
||||
patchedSettings.PublicIP.overrideWith(other.PublicIP)
|
||||
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
|
||||
patchedSettings.Storage.overrideWith(other.Storage)
|
||||
patchedSettings.System.overrideWith(other.System)
|
||||
patchedSettings.Updater.overrideWith(other.Updater)
|
||||
patchedSettings.Version.overrideWith(other.Version)
|
||||
patchedSettings.VPN.OverrideWith(other.VPN)
|
||||
patchedSettings.Pprof.OverrideWith(other.Pprof)
|
||||
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||
err = patchedSettings.Validate(storage, ipv6Supported)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -122,7 +116,6 @@ func (s *Settings) SetDefaults() {
|
||||
s.Log.setDefaults()
|
||||
s.PublicIP.setDefaults()
|
||||
s.Shadowsocks.setDefaults()
|
||||
s.Storage.setDefaults()
|
||||
s.System.setDefaults()
|
||||
s.Version.setDefaults()
|
||||
s.VPN.setDefaults()
|
||||
@@ -145,7 +138,6 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
|
||||
node.AppendNode(s.Shadowsocks.toLinesNode())
|
||||
node.AppendNode(s.HTTPProxy.toLinesNode())
|
||||
node.AppendNode(s.ControlServer.toLinesNode())
|
||||
node.AppendNode(s.Storage.toLinesNode())
|
||||
node.AppendNode(s.System.toLinesNode())
|
||||
node.AppendNode(s.PublicIP.toLinesNode())
|
||||
node.AppendNode(s.Updater.toLinesNode())
|
||||
@@ -173,22 +165,19 @@ func (s Settings) Warnings() (warnings []string) {
|
||||
"by creating an issue, attaching the new certificate and we will update Gluetun.")
|
||||
}
|
||||
|
||||
for _, upstreamAddress := range s.DNS.UpstreamPlainAddresses {
|
||||
if upstreamAddress.Addr().IsPrivate() {
|
||||
warnings = append(warnings, "DNS upstream address "+upstreamAddress.String()+" is private: "+
|
||||
"DNS traffic might leak out of the VPN tunnel to that address.")
|
||||
}
|
||||
// TODO remove in v4
|
||||
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
||||
" so the DNS over TLS (DoT) server will not be used."+
|
||||
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
|
||||
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||
" corresponding to the first DoT provider chosen is used.")
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
|
||||
warnings := readObsolete(r)
|
||||
for _, warning := range warnings {
|
||||
warner.Warn(warning)
|
||||
}
|
||||
|
||||
func (s *Settings) Read(r *reader.Reader) (err error) {
|
||||
readFunctions := map[string]func(r *reader.Reader) error{
|
||||
"control server": s.ControlServer.read,
|
||||
"DNS": s.DNS.read,
|
||||
@@ -196,16 +185,13 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
|
||||
"health": s.Health.Read,
|
||||
"http proxy": s.HTTPProxy.read,
|
||||
"log": s.Log.read,
|
||||
"public ip": func(r *reader.Reader) error {
|
||||
return s.PublicIP.read(r, warner)
|
||||
},
|
||||
"shadowsocks": s.Shadowsocks.read,
|
||||
"storage": s.Storage.read,
|
||||
"system": s.System.read,
|
||||
"updater": s.Updater.read,
|
||||
"version": s.Version.read,
|
||||
"VPN": s.VPN.read,
|
||||
"profiling": s.Pprof.Read,
|
||||
"public ip": s.PublicIP.read,
|
||||
"shadowsocks": s.Shadowsocks.read,
|
||||
"system": s.System.read,
|
||||
"updater": s.Updater.read,
|
||||
"version": s.Version.read,
|
||||
"VPN": s.VPN.read,
|
||||
"profiling": s.Pprof.Read,
|
||||
}
|
||||
|
||||
for name, read := range readFunctions {
|
||||
|
||||
@@ -38,16 +38,27 @@ func Test_Settings_String(t *testing.T) {
|
||||
| ├── Run OpenVPN as: root
|
||||
| └── Verbosity level: 1
|
||||
├── DNS settings:
|
||||
| ├── Upstream resolver type: dot
|
||||
| ├── Upstream resolvers:
|
||||
| | └── Cloudflare
|
||||
| ├── Caching: yes
|
||||
| ├── IPv6: no
|
||||
| ├── Update period: every 24h0m0s
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
| ├── Keep existing nameserver(s): no
|
||||
| ├── DNS server address to use: 127.0.0.1
|
||||
| └── DNS over TLS settings:
|
||||
| ├── Enabled: yes
|
||||
| ├── Update period: every 24h0m0s
|
||||
| ├── Unbound settings:
|
||||
| | ├── Authoritative servers:
|
||||
| | | └── Cloudflare
|
||||
| | ├── Caching: yes
|
||||
| | ├── IPv6: no
|
||||
| | ├── Verbosity level: 1
|
||||
| | ├── Verbosity details level: 0
|
||||
| | ├── Validation log level: 0
|
||||
| | ├── System user: root
|
||||
| | └── Allowed networks:
|
||||
| | ├── 0.0.0.0/0
|
||||
| | └── ::/0
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
├── Firewall settings:
|
||||
| └── Enabled: yes
|
||||
├── Log settings:
|
||||
@@ -55,8 +66,12 @@ func Test_Settings_String(t *testing.T) {
|
||||
├── Health settings:
|
||||
| ├── Server listening address: 127.0.0.1:9999
|
||||
| ├── Target address: cloudflare.com:443
|
||||
| ├── ICMP target IP: VPN server IP
|
||||
| └── Restart VPN on healthcheck failure: yes
|
||||
| ├── Duration to wait after success: 5s
|
||||
| ├── Read header timeout: 100ms
|
||||
| ├── Read timeout: 500ms
|
||||
| └── VPN wait durations:
|
||||
| ├── Initial duration: 6s
|
||||
| └── Additional duration: 5s
|
||||
├── Shadowsocks server settings:
|
||||
| └── Enabled: no
|
||||
├── HTTP proxy settings:
|
||||
@@ -65,24 +80,20 @@ func Test_Settings_String(t *testing.T) {
|
||||
| ├── Listening address: :8000
|
||||
| ├── Logging: yes
|
||||
| └── Authentication file path: /gluetun/auth/config.toml
|
||||
├── Storage settings:
|
||||
| └── Filepath: /gluetun/servers.json
|
||||
├── OS Alpine settings:
|
||||
| ├── Process UID: 1000
|
||||
| └── Process GID: 1000
|
||||
├── Public IP settings:
|
||||
| ├── Fetching: every 12h0m0s
|
||||
| ├── IP file path: /tmp/gluetun/ip
|
||||
| ├── Public IP data base API: ipinfo
|
||||
| └── Public IP data backup APIs:
|
||||
| ├── cloudflare
|
||||
| ├── ifconfigco
|
||||
| └── ip2location
|
||||
| └── Public IP data API: ipinfo
|
||||
└── Version settings:
|
||||
└── Enabled: yes`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type Shadowsocks struct {
|
||||
// It defaults to false, and cannot be nil in the internal state.
|
||||
Enabled *bool
|
||||
// Settings are settings for the TCP+UDP server.
|
||||
Settings tcpudp.Settings
|
||||
tcpudp.Settings
|
||||
}
|
||||
|
||||
func (s Shadowsocks) validate() (err error) {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Storage contains settings to configure the storage.
|
||||
type Storage struct {
|
||||
// Filepath is the path to the servers.json file. An empty string disables on-disk storage.
|
||||
Filepath *string
|
||||
}
|
||||
|
||||
func (s Storage) validate() (err error) {
|
||||
if *s.Filepath != "" { // optional
|
||||
_, err := filepath.Abs(*s.Filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("filepath is not valid: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) copy() (copied Storage) {
|
||||
return Storage{
|
||||
Filepath: gosettings.CopyPointer(s.Filepath),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) overrideWith(other Storage) {
|
||||
s.Filepath = gosettings.OverrideWithPointer(s.Filepath, other.Filepath)
|
||||
}
|
||||
|
||||
func (s *Storage) setDefaults() {
|
||||
const defaultFilepath = "/gluetun/servers.json"
|
||||
s.Filepath = gosettings.DefaultPointer(s.Filepath, defaultFilepath)
|
||||
}
|
||||
|
||||
func (s Storage) String() string {
|
||||
return s.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (s Storage) toLinesNode() (node *gotree.Node) {
|
||||
if *s.Filepath == "" {
|
||||
return gotree.New("Storage settings: disabled")
|
||||
}
|
||||
node = gotree.New("Storage settings:")
|
||||
node.Appendf("Filepath: %s", *s.Filepath)
|
||||
return node
|
||||
}
|
||||
|
||||
func (s *Storage) read(r *reader.Reader) (err error) {
|
||||
s.Filepath = r.Get("STORAGE_FILEPATH", reader.AcceptEmpty(true))
|
||||
return nil
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func surfsharkRetroRegion(selection ServerSelection) (
|
||||
updatedSelection ServerSelection,
|
||||
) {
|
||||
updatedSelection ServerSelection) {
|
||||
locationData := servers.LocationData()
|
||||
|
||||
retroToLocation := make(map[string]servers.ServerLocation, len(locationData))
|
||||
|
||||
223
internal/configuration/settings/unbound.go
Normal file
223
internal/configuration/settings/unbound.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/pkg/provider"
|
||||
"github.com/qdm12/dns/pkg/unbound"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Unbound is settings for the Unbound program.
|
||||
type Unbound struct {
|
||||
Providers []string `json:"providers"`
|
||||
Caching *bool `json:"caching"`
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
VerbosityLevel *uint8 `json:"verbosity_level"`
|
||||
VerbosityDetailsLevel *uint8 `json:"verbosity_details_level"`
|
||||
ValidationLogLevel *uint8 `json:"validation_log_level"`
|
||||
Username string `json:"username"`
|
||||
Allowed []netip.Prefix `json:"allowed"`
|
||||
}
|
||||
|
||||
func (u *Unbound) setDefaults() {
|
||||
if len(u.Providers) == 0 {
|
||||
u.Providers = []string{
|
||||
provider.Cloudflare().String(),
|
||||
}
|
||||
}
|
||||
|
||||
u.Caching = gosettings.DefaultPointer(u.Caching, true)
|
||||
u.IPv6 = gosettings.DefaultPointer(u.IPv6, false)
|
||||
|
||||
const defaultVerbosityLevel = 1
|
||||
u.VerbosityLevel = gosettings.DefaultPointer(u.VerbosityLevel, defaultVerbosityLevel)
|
||||
|
||||
const defaultVerbosityDetailsLevel = 0
|
||||
u.VerbosityDetailsLevel = gosettings.DefaultPointer(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel)
|
||||
|
||||
const defaultValidationLogLevel = 0
|
||||
u.ValidationLogLevel = gosettings.DefaultPointer(u.ValidationLogLevel, defaultValidationLogLevel)
|
||||
|
||||
if u.Allowed == nil {
|
||||
u.Allowed = []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
|
||||
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
|
||||
}
|
||||
}
|
||||
|
||||
u.Username = gosettings.DefaultComparable(u.Username, "root")
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnboundVerbosityLevelNotValid = errors.New("Unbound verbosity level is not valid")
|
||||
ErrUnboundVerbosityDetailsLevelNotValid = errors.New("Unbound verbosity details level is not valid")
|
||||
ErrUnboundValidationLogLevelNotValid = errors.New("Unbound validation log level is not valid")
|
||||
)
|
||||
|
||||
func (u Unbound) validate() (err error) {
|
||||
for _, s := range u.Providers {
|
||||
_, err := provider.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
const maxVerbosityLevel = 5
|
||||
if *u.VerbosityLevel > maxVerbosityLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundVerbosityLevelNotValid,
|
||||
*u.VerbosityLevel,
|
||||
maxVerbosityLevel)
|
||||
}
|
||||
|
||||
const maxVerbosityDetailsLevel = 4
|
||||
if *u.VerbosityDetailsLevel > maxVerbosityDetailsLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundVerbosityDetailsLevelNotValid,
|
||||
*u.VerbosityDetailsLevel,
|
||||
maxVerbosityDetailsLevel)
|
||||
}
|
||||
|
||||
const maxValidationLogLevel = 2
|
||||
if *u.ValidationLogLevel > maxValidationLogLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundValidationLogLevelNotValid,
|
||||
*u.ValidationLogLevel, maxValidationLogLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u Unbound) copy() (copied Unbound) {
|
||||
return Unbound{
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
Caching: gosettings.CopyPointer(u.Caching),
|
||||
IPv6: gosettings.CopyPointer(u.IPv6),
|
||||
VerbosityLevel: gosettings.CopyPointer(u.VerbosityLevel),
|
||||
VerbosityDetailsLevel: gosettings.CopyPointer(u.VerbosityDetailsLevel),
|
||||
ValidationLogLevel: gosettings.CopyPointer(u.ValidationLogLevel),
|
||||
Username: u.Username,
|
||||
Allowed: gosettings.CopySlice(u.Allowed),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Unbound) overrideWith(other Unbound) {
|
||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||
u.Caching = gosettings.OverrideWithPointer(u.Caching, other.Caching)
|
||||
u.IPv6 = gosettings.OverrideWithPointer(u.IPv6, other.IPv6)
|
||||
u.VerbosityLevel = gosettings.OverrideWithPointer(u.VerbosityLevel, other.VerbosityLevel)
|
||||
u.VerbosityDetailsLevel = gosettings.OverrideWithPointer(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
|
||||
u.ValidationLogLevel = gosettings.OverrideWithPointer(u.ValidationLogLevel, other.ValidationLogLevel)
|
||||
u.Username = gosettings.OverrideWithComparable(u.Username, other.Username)
|
||||
u.Allowed = gosettings.OverrideWithSlice(u.Allowed, other.Allowed)
|
||||
}
|
||||
|
||||
func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) {
|
||||
providers := make([]provider.Provider, len(u.Providers))
|
||||
for i := range providers {
|
||||
providers[i], err = provider.Parse(u.Providers[i])
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
}
|
||||
|
||||
const port = 53
|
||||
|
||||
return unbound.Settings{
|
||||
ListeningPort: port,
|
||||
IPv4: true,
|
||||
Providers: providers,
|
||||
Caching: *u.Caching,
|
||||
IPv6: *u.IPv6,
|
||||
VerbosityLevel: *u.VerbosityLevel,
|
||||
VerbosityDetailsLevel: *u.VerbosityDetailsLevel,
|
||||
ValidationLogLevel: *u.ValidationLogLevel,
|
||||
AccessControl: unbound.AccessControlSettings{
|
||||
Allowed: netipPrefixesToNetaddrIPPrefixes(u.Allowed),
|
||||
},
|
||||
Username: u.Username,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrConvertingNetip = errors.New("converting net.IP to netip.Addr failed")
|
||||
)
|
||||
|
||||
func (u Unbound) GetFirstPlaintextIPv4() (ipv4 netip.Addr, err error) {
|
||||
s := u.Providers[0]
|
||||
provider, err := provider.Parse(s)
|
||||
if err != nil {
|
||||
return ipv4, err
|
||||
}
|
||||
|
||||
ip := provider.DNS().IPv4[0]
|
||||
ipv4, ok := netip.AddrFromSlice(ip)
|
||||
if !ok {
|
||||
return ipv4, fmt.Errorf("%w: for ip %s (%#v)",
|
||||
ErrConvertingNetip, ip, ip)
|
||||
}
|
||||
return ipv4.Unmap(), nil
|
||||
}
|
||||
|
||||
func (u Unbound) String() string {
|
||||
return u.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (u Unbound) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Unbound settings:")
|
||||
|
||||
authServers := node.Appendf("Authoritative servers:")
|
||||
for _, provider := range u.Providers {
|
||||
authServers.Appendf(provider)
|
||||
}
|
||||
|
||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(u.Caching))
|
||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(u.IPv6))
|
||||
node.Appendf("Verbosity level: %d", *u.VerbosityLevel)
|
||||
node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel)
|
||||
node.Appendf("Validation log level: %d", *u.ValidationLogLevel)
|
||||
node.Appendf("System user: %s", u.Username)
|
||||
|
||||
allowedNetworks := node.Appendf("Allowed networks:")
|
||||
for _, network := range u.Allowed {
|
||||
allowedNetworks.Appendf(network.String())
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (u *Unbound) read(reader *reader.Reader) (err error) {
|
||||
u.Providers = reader.CSV("DOT_PROVIDERS")
|
||||
|
||||
u.Caching, err = reader.BoolPtr("DOT_CACHING")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.VerbosityLevel, err = reader.Uint8Ptr("DOT_VERBOSITY")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.VerbosityDetailsLevel, err = reader.Uint8Ptr("DOT_VERBOSITY_DETAILS")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.ValidationLogLevel, err = reader.Uint8Ptr("DOT_VALIDATION_LOGLEVEL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
43
internal/configuration/settings/unbound_test.go
Normal file
43
internal/configuration/settings/unbound_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Unbound_JSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := Unbound{
|
||||
Providers: []string{"cloudflare"},
|
||||
Caching: boolPtr(true),
|
||||
IPv6: boolPtr(false),
|
||||
VerbosityLevel: uint8Ptr(1),
|
||||
VerbosityDetailsLevel: nil,
|
||||
ValidationLogLevel: uint8Ptr(0),
|
||||
Username: "user",
|
||||
Allowed: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
|
||||
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
const expected = `{"providers":["cloudflare"],"caching":true,"ipv6":false,` +
|
||||
`"verbosity_level":1,"verbosity_details_level":null,"validation_log_level":0,` +
|
||||
`"username":"user","allowed":["0.0.0.0/0","::/0"]}`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
|
||||
var resultSettings Unbound
|
||||
err = json.Unmarshal(b, &resultSettings)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, settings, resultSettings)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,10 +31,6 @@ type Updater struct {
|
||||
// Providers is the list of VPN service providers
|
||||
// to update server information for.
|
||||
Providers []string
|
||||
// ProtonUsername is the username to authenticate with the Proton API.
|
||||
ProtonUsername *string
|
||||
// ProtonPassword is the password to authenticate with the Proton API.
|
||||
ProtonPassword *string
|
||||
}
|
||||
|
||||
func (u Updater) Validate() (err error) {
|
||||
@@ -56,18 +51,6 @@ func (u Updater) Validate() (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
||||
}
|
||||
|
||||
if provider == providers.Protonvpn {
|
||||
authenticatedAPI := *u.ProtonUsername != "" || *u.ProtonPassword != ""
|
||||
if authenticatedAPI {
|
||||
switch {
|
||||
case *u.ProtonUsername == "":
|
||||
return fmt.Errorf("%w", ErrUpdaterProtonUsernameMissing)
|
||||
case *u.ProtonPassword == "":
|
||||
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -75,12 +58,10 @@ func (u Updater) Validate() (err error) {
|
||||
|
||||
func (u *Updater) copy() (copied Updater) {
|
||||
return Updater{
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
ProtonUsername: gosettings.CopyPointer(u.ProtonUsername),
|
||||
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,8 +73,6 @@ func (u *Updater) overrideWith(other Updater) {
|
||||
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
||||
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||
u.ProtonUsername = gosettings.OverrideWithPointer(u.ProtonUsername, other.ProtonUsername)
|
||||
u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword)
|
||||
}
|
||||
|
||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
@@ -108,10 +87,6 @@ func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
||||
u.Providers = []string{vpnProvider}
|
||||
}
|
||||
|
||||
// Set these to empty strings to avoid nil pointer panics
|
||||
u.ProtonUsername = gosettings.DefaultPointer(u.ProtonUsername, "")
|
||||
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
|
||||
}
|
||||
|
||||
func (u Updater) String() string {
|
||||
@@ -128,10 +103,6 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||
if slices.Contains(u.Providers, providers.Protonvpn) {
|
||||
node.Appendf("Proton API username: %s", *u.ProtonUsername)
|
||||
node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
@@ -154,14 +125,6 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
||||
|
||||
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
||||
|
||||
u.ProtonUsername = r.Get("UPDATER_PROTONVPN_USERNAME")
|
||||
if u.ProtonUsername != nil {
|
||||
// Enforce to use the username not the email address
|
||||
*u.ProtonUsername = strings.TrimSuffix(*u.ProtonUsername, "@protonmail.com")
|
||||
*u.ProtonUsername = strings.TrimSuffix(*u.ProtonUsername, "@proton.me")
|
||||
}
|
||||
u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ type VPN struct {
|
||||
}
|
||||
|
||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||
func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) {
|
||||
func (v *VPN) Validate(storage Storage, ipv6Supported bool) (err error) {
|
||||
// Validate Type
|
||||
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
|
||||
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err)
|
||||
}
|
||||
|
||||
err = v.Provider.validate(v.Type, filterChoicesGetter, warner)
|
||||
err = v.Provider.validate(v.Type, storage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("provider settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -39,12 +39,9 @@ type Wireguard struct {
|
||||
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
|
||||
// Maximum Transmission Unit (MTU) of the Wireguard interface.
|
||||
// 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,
|
||||
// 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.
|
||||
MTU uint16 `json:"mtu"`
|
||||
// Implementation is the Wireguard implementation to use.
|
||||
// It can be "auto", "userspace" or "kernelspace".
|
||||
@@ -194,7 +191,7 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
|
||||
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
|
||||
w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
|
||||
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
|
||||
const defaultMTU = 1320
|
||||
const defaultMTU = 1400
|
||||
w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
|
||||
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
|
||||
}
|
||||
@@ -218,12 +215,12 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
|
||||
|
||||
addressesNode := node.Appendf("Interface addresses:")
|
||||
for _, address := range w.Addresses {
|
||||
addressesNode.Append(address.String())
|
||||
addressesNode.Appendf(address.String())
|
||||
}
|
||||
|
||||
allowedIPsNode := node.Appendf("Allowed IPs:")
|
||||
for _, allowedIP := range w.AllowedIPs {
|
||||
allowedIPsNode.Append(allowedIP.String())
|
||||
allowedIPsNode.Appendf(allowedIP.String())
|
||||
}
|
||||
|
||||
if *w.PersistentKeepaliveInterval > 0 {
|
||||
|
||||
@@ -155,8 +155,7 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
|
||||
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
|
||||
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w - note this MUST be an IP address, "+
|
||||
"see https://github.com/qdm12/gluetun/issues/788", err)
|
||||
return err
|
||||
}
|
||||
|
||||
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
|
||||
|
||||
@@ -34,7 +34,9 @@ type WireguardConfig struct {
|
||||
EndpointPort *string
|
||||
}
|
||||
|
||||
var regexINISectionNotExist = regexp.MustCompile(`^section ".+" does not exist$`)
|
||||
var (
|
||||
regexINISectionNotExist = regexp.MustCompile(`^section ".+" does not exist$`)
|
||||
)
|
||||
|
||||
func ParseWireguardConf(path string) (config WireguardConfig, err error) {
|
||||
iniFile, err := ini.InsensitiveLoad(path)
|
||||
@@ -66,18 +68,18 @@ func ParseWireguardConf(path string) (config WireguardConfig, err error) {
|
||||
}
|
||||
|
||||
func parseWireguardInterfaceSection(interfaceSection *ini.Section) (
|
||||
privateKey, addresses *string,
|
||||
) {
|
||||
privateKey, addresses *string) {
|
||||
privateKey = getINIKeyFromSection(interfaceSection, "PrivateKey")
|
||||
addresses = getINIKeyFromSection(interfaceSection, "Address")
|
||||
return privateKey, addresses
|
||||
}
|
||||
|
||||
var ErrEndpointHostNotIP = errors.New("endpoint host is not an IP")
|
||||
var (
|
||||
ErrEndpointHostNotIP = errors.New("endpoint host is not an IP")
|
||||
)
|
||||
|
||||
func parseWireguardPeerSection(peerSection *ini.Section) (
|
||||
preSharedKey, publicKey, endpointIP, endpointPort *string,
|
||||
) {
|
||||
preSharedKey, publicKey, endpointIP, endpointPort *string) {
|
||||
preSharedKey = getINIKeyFromSection(peerSection, "PresharedKey")
|
||||
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
|
||||
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
|
||||
@@ -94,7 +96,9 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
|
||||
return preSharedKey, publicKey, endpointIP, endpointPort
|
||||
}
|
||||
|
||||
var regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`)
|
||||
var (
|
||||
regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`)
|
||||
)
|
||||
|
||||
func getINIKeyFromSection(section *ini.Section, key string) (value *string) {
|
||||
iniKey, err := section.GetKey(key)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -73,12 +72,12 @@ PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configFile := filepath.Join(t.TempDir(), "wg.conf")
|
||||
const permission = fs.FileMode(0o600)
|
||||
err := os.WriteFile(configFile, []byte(testCase.fileContent), permission)
|
||||
err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
wireguard, err := ParseWireguardConf(configFile)
|
||||
@@ -122,6 +121,7 @@ Address = 10.38.22.35/32
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -182,6 +182,7 @@ Endpoint = 1.2.3.4:51820`,
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -39,8 +38,7 @@ func Test_Source_Get(t *testing.T) {
|
||||
"empty_secret_file": {
|
||||
makeSource: func(tempDir string) (source *Source, err error) {
|
||||
secretFilepath := filepath.Join(tempDir, "test_file")
|
||||
const permission = fs.FileMode(0o600)
|
||||
err = os.WriteFile(secretFilepath, nil, permission)
|
||||
err = os.WriteFile(secretFilepath, nil, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -55,8 +53,7 @@ func Test_Source_Get(t *testing.T) {
|
||||
"default_secret_file": {
|
||||
makeSource: func(tempDir string) (source *Source, err error) {
|
||||
secretFilepath := filepath.Join(tempDir, "test_file")
|
||||
const permission = fs.FileMode(0o600)
|
||||
err = os.WriteFile(secretFilepath, []byte{'A'}, permission)
|
||||
err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,8 +69,7 @@ func Test_Source_Get(t *testing.T) {
|
||||
"env_specified_secret_file": {
|
||||
makeSource: func(tempDir string) (source *Source, err error) {
|
||||
secretFilepath := filepath.Join(tempDir, "test_file_custom")
|
||||
const permission = fs.FileMode(0o600)
|
||||
err = os.WriteFile(secretFilepath, []byte{'A'}, permission)
|
||||
err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,6 +87,7 @@ func Test_Source_Get(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ package constants
|
||||
|
||||
import "github.com/fatih/color"
|
||||
|
||||
func ColorUnbound() *color.Color {
|
||||
return color.New(color.FgCyan)
|
||||
}
|
||||
|
||||
func ColorOpenvpn() *color.Color {
|
||||
return color.New(color.FgHiMagenta)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ const (
|
||||
Example = "example"
|
||||
Expressvpn = "expressvpn"
|
||||
Fastestvpn = "fastestvpn"
|
||||
Giganews = "giganews"
|
||||
HideMyAss = "hidemyass"
|
||||
Ipvanish = "ipvanish"
|
||||
Ivpn = "ivpn"
|
||||
@@ -38,7 +37,6 @@ func All() []string {
|
||||
Cyberghost,
|
||||
Expressvpn,
|
||||
Fastestvpn,
|
||||
Giganews,
|
||||
HideMyAss,
|
||||
Ipvanish,
|
||||
Ivpn,
|
||||
|
||||
15
internal/dns/interfaces.go
Normal file
15
internal/dns/interfaces.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdm12/dns/pkg/unbound"
|
||||
)
|
||||
|
||||
type Configurator interface {
|
||||
SetupFiles(ctx context.Context) error
|
||||
MakeUnboundConf(settings unbound.Settings) (err error)
|
||||
Start(ctx context.Context, verbosityDetailsLevel uint8) (
|
||||
stdoutLines, stderrLines chan string, waitError chan error, err error)
|
||||
Version(ctx context.Context) (version string, err error)
|
||||
}
|
||||
75
internal/dns/logs.go
Normal file
75
internal/dns/logs.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
)
|
||||
|
||||
type logLevel uint8
|
||||
|
||||
const (
|
||||
levelDebug logLevel = iota
|
||||
levelInfo
|
||||
levelWarn
|
||||
levelError
|
||||
)
|
||||
|
||||
func (l *Loop) collectLines(ctx context.Context, done chan<- struct{},
|
||||
stdout, stderr chan string) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Context should only be canceled after stdout and stderr are done
|
||||
// being written to.
|
||||
close(stdout)
|
||||
close(stderr)
|
||||
return
|
||||
case line = <-stderr:
|
||||
case line = <-stdout:
|
||||
}
|
||||
|
||||
line, level := processLogLine(line)
|
||||
switch level {
|
||||
case levelDebug:
|
||||
l.logger.Debug(line)
|
||||
case levelInfo:
|
||||
l.logger.Info(line)
|
||||
case levelWarn:
|
||||
l.logger.Warn(line)
|
||||
case levelError:
|
||||
l.logger.Error(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unboundPrefix = regexp.MustCompile(`\[[0-9]{10}\] unbound\[[0-9]+:[0|1]\] `)
|
||||
|
||||
func processLogLine(s string) (filtered string, level logLevel) {
|
||||
prefix := unboundPrefix.FindString(s)
|
||||
filtered = s[len(prefix):]
|
||||
switch {
|
||||
case strings.HasPrefix(filtered, "notice: "):
|
||||
filtered = strings.TrimPrefix(filtered, "notice: ")
|
||||
level = levelInfo
|
||||
case strings.HasPrefix(filtered, "info: "):
|
||||
filtered = strings.TrimPrefix(filtered, "info: ")
|
||||
level = levelInfo
|
||||
case strings.HasPrefix(filtered, "warn: "):
|
||||
filtered = strings.TrimPrefix(filtered, "warn: ")
|
||||
level = levelWarn
|
||||
case strings.HasPrefix(filtered, "error: "):
|
||||
filtered = strings.TrimPrefix(filtered, "error: ")
|
||||
level = levelError
|
||||
default:
|
||||
level = levelInfo
|
||||
}
|
||||
filtered = constants.ColorUnbound().Sprintf(filtered)
|
||||
return filtered, level
|
||||
}
|
||||
48
internal/dns/logs_test.go
Normal file
48
internal/dns/logs_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_processLogLine(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
s string
|
||||
filtered string
|
||||
level logLevel
|
||||
}{
|
||||
"empty string": {"", "", levelInfo},
|
||||
"random string": {"asdasqdb", "asdasqdb", levelInfo},
|
||||
"unbound notice": {
|
||||
"[1594595249] unbound[75:0] notice: init module 0: validator",
|
||||
"init module 0: validator",
|
||||
levelInfo},
|
||||
"unbound info": {
|
||||
"[1594595249] unbound[75:0] info: init module 0: validator",
|
||||
"init module 0: validator",
|
||||
levelInfo},
|
||||
"unbound warn": {
|
||||
"[1594595249] unbound[75:0] warn: init module 0: validator",
|
||||
"init module 0: validator",
|
||||
levelWarn},
|
||||
"unbound error": {
|
||||
"[1594595249] unbound[75:0] error: init module 0: validator",
|
||||
"init module 0: validator",
|
||||
levelError},
|
||||
"unbound unknown": {
|
||||
"[1594595249] unbound[75:0] BLA: init module 0: validator",
|
||||
"BLA: init module 0: validator",
|
||||
levelInfo},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filtered, level := processLogLine(tc.s)
|
||||
assert.Equal(t, tc.filtered, filtered)
|
||||
assert.Equal(t, tc.level, level)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,10 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||
"github.com/qdm12/dns/v2/pkg/server"
|
||||
"github.com/qdm12/dns/pkg/blacklist"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/dns/state"
|
||||
@@ -17,30 +14,28 @@ import (
|
||||
)
|
||||
|
||||
type Loop struct {
|
||||
statusManager *loopstate.State
|
||||
state *state.State
|
||||
server *server.Server
|
||||
filter *mapfilter.Filter
|
||||
localResolvers []netip.Addr
|
||||
resolvConf string
|
||||
client *http.Client
|
||||
logger Logger
|
||||
userTrigger bool
|
||||
start <-chan struct{}
|
||||
running chan<- models.LoopStatus
|
||||
stop <-chan struct{}
|
||||
stopped chan<- struct{}
|
||||
updateTicker <-chan struct{}
|
||||
backoffTime time.Duration
|
||||
timeNow func() time.Time
|
||||
timeSince func(time.Time) time.Duration
|
||||
statusManager *loopstate.State
|
||||
state *state.State
|
||||
conf Configurator
|
||||
resolvConf string
|
||||
blockBuilder blacklist.Builder
|
||||
client *http.Client
|
||||
logger Logger
|
||||
userTrigger bool
|
||||
start <-chan struct{}
|
||||
running chan<- models.LoopStatus
|
||||
stop <-chan struct{}
|
||||
stopped chan<- struct{}
|
||||
updateTicker <-chan struct{}
|
||||
backoffTime time.Duration
|
||||
timeNow func() time.Time
|
||||
timeSince func(time.Time) time.Duration
|
||||
}
|
||||
|
||||
const defaultBackoffTime = 10 * time.Second
|
||||
|
||||
func NewLoop(settings settings.DNS,
|
||||
client *http.Client, logger Logger,
|
||||
) (loop *Loop, err error) {
|
||||
func NewLoop(conf Configurator, settings settings.DNS,
|
||||
client *http.Client, logger Logger) *Loop {
|
||||
start := make(chan struct{})
|
||||
running := make(chan models.LoopStatus)
|
||||
stop := make(chan struct{})
|
||||
@@ -50,19 +45,12 @@ func NewLoop(settings settings.DNS,
|
||||
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
||||
state := state.New(statusManager, settings, updateTicker)
|
||||
|
||||
filter, err := mapfilter.New(mapfilter.Settings{
|
||||
Logger: buildFilterLogger(logger),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating map filter: %w", err)
|
||||
}
|
||||
|
||||
return &Loop{
|
||||
statusManager: statusManager,
|
||||
state: state,
|
||||
server: nil,
|
||||
filter: filter,
|
||||
conf: conf,
|
||||
resolvConf: "/etc/resolv.conf",
|
||||
blockBuilder: blacklist.NewBuilder(client),
|
||||
client: client,
|
||||
logger: logger,
|
||||
userTrigger: true,
|
||||
@@ -74,7 +62,7 @@ func NewLoop(settings settings.DNS,
|
||||
backoffTime: defaultBackoffTime,
|
||||
timeNow: time.Now,
|
||||
timeSince: time.Since,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loop) logAndWait(ctx context.Context, err error) {
|
||||
@@ -104,15 +92,3 @@ func (l *Loop) signalOrSetStatus(status models.LoopStatus) {
|
||||
l.statusManager.SetStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
type filterLogger struct {
|
||||
logger Logger
|
||||
}
|
||||
|
||||
func (l *filterLogger) Log(msg string) {
|
||||
l.logger.Info(msg)
|
||||
}
|
||||
|
||||
func buildFilterLogger(logger Logger) *filterLogger {
|
||||
return &filterLogger{logger: logger}
|
||||
}
|
||||
|
||||
@@ -2,35 +2,46 @@ package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
"github.com/qdm12/dns/pkg/nameserver"
|
||||
)
|
||||
|
||||
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
settings := l.GetSettings()
|
||||
|
||||
targetIP := settings.GetFirstPlaintextIPv4()
|
||||
// Try with user provided plaintext ip address
|
||||
// if it's not 127.0.0.1 (default for DoT)
|
||||
targetIP := settings.ServerAddress
|
||||
if targetIP.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
if fallback {
|
||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||
} else {
|
||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||
}
|
||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
||||
const keepNameserver = false
|
||||
err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use first plaintext DNS IPv4 address
|
||||
targetIP, err := settings.DoT.Unbound.GetFirstPlaintextIPv4()
|
||||
if err != nil {
|
||||
// Unbound should always have a default provider
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if fallback {
|
||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||
} else {
|
||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||
}
|
||||
|
||||
const dialTimeout = 3 * time.Second
|
||||
const defaultDNSPort = 53
|
||||
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
||||
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
|
||||
Timeout: dialTimeout,
|
||||
}
|
||||
nameserver.UseDNSInternally(settingsInternalDNS)
|
||||
|
||||
settingsSystemWide := nameserver.SettingsSystemDNS{
|
||||
IPs: []netip.Addr{targetIP},
|
||||
ResolvPath: l.resolvConf,
|
||||
}
|
||||
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
||||
const keepNameserver = false
|
||||
err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
@@ -4,23 +4,21 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
)
|
||||
|
||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
var err error
|
||||
l.localResolvers, err = nameserver.GetPrivateDNSServers()
|
||||
if err != nil {
|
||||
l.logger.Error("getting private DNS servers: " + err.Error())
|
||||
return
|
||||
if *l.GetSettings().KeepNameserver {
|
||||
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
|
||||
"this will likely leak DNS traffic outside the VPN " +
|
||||
"and go through your container network DNS outside the VPN tunnel!")
|
||||
} else {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
|
||||
select {
|
||||
case <-l.start:
|
||||
case <-ctx.Done():
|
||||
@@ -28,16 +26,20 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
// Upper scope variables for the DNS forwarder server only
|
||||
// Upper scope variables for Unbound only
|
||||
// Their values are to be used if DOT=off
|
||||
var runError <-chan error
|
||||
waitError := make(chan error)
|
||||
unboundCancel := func() { waitError <- nil }
|
||||
closeStreams := func() {}
|
||||
|
||||
for {
|
||||
settings := l.GetSettings()
|
||||
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
||||
var err error
|
||||
runError, err = l.setupServer(ctx)
|
||||
unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx)
|
||||
if err == nil {
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.logger.Info("ready")
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -47,57 +49,55 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
return
|
||||
}
|
||||
|
||||
if !errors.Is(err, errUpdateBlockLists) {
|
||||
if !errors.Is(err, errUpdateFiles) {
|
||||
const fallback = true
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
l.logAndWait(ctx, err)
|
||||
}
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
settings = l.GetSettings()
|
||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
|
||||
l.userTrigger = false
|
||||
|
||||
exitLoop := l.runWait(ctx, runError)
|
||||
if exitLoop {
|
||||
return
|
||||
stayHere := true
|
||||
for stayHere {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
unboundCancel()
|
||||
<-waitError
|
||||
close(waitError)
|
||||
closeStreams()
|
||||
return
|
||||
case <-l.stop:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("stopping")
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
unboundCancel()
|
||||
<-waitError
|
||||
// do not close waitError or the waitError
|
||||
// select case will trigger
|
||||
closeStreams()
|
||||
l.stopped <- struct{}{}
|
||||
case <-l.start:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("starting")
|
||||
stayHere = false
|
||||
case err := <-waitError: // unexpected error
|
||||
closeStreams()
|
||||
|
||||
unboundCancel()
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
const fallback = true
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.logAndWait(ctx, err)
|
||||
stayHere = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop bool) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
l.stopServer()
|
||||
// TODO revert OS and Go nameserver when exiting
|
||||
return true
|
||||
case <-l.stop:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("stopping")
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.stopServer()
|
||||
l.stopped <- struct{}{}
|
||||
case <-l.start:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("starting")
|
||||
return false
|
||||
case err := <-runError: // unexpected error
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
const fallback = true
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.logAndWait(ctx, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loop) stopServer() {
|
||||
stopErr := l.server.Stop()
|
||||
if stopErr != nil {
|
||||
l.logger.Error("stopping server: " + stopErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,161 +2,13 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
"github.com/qdm12/dns/v2/pkg/dot"
|
||||
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
|
||||
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/localdns"
|
||||
"github.com/qdm12/dns/v2/pkg/plain"
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/dns/v2/pkg/server"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
)
|
||||
|
||||
func (l *Loop) GetSettings() (settings settings.DNS) { return l.state.GetSettings() }
|
||||
|
||||
func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
outcome string,
|
||||
) {
|
||||
outcome string) {
|
||||
return l.state.SetSettings(ctx, settings)
|
||||
}
|
||||
|
||||
func buildServerSettings(userSettings settings.DNS,
|
||||
filter *mapfilter.Filter, localResolvers []netip.Addr,
|
||||
logger Logger) (
|
||||
serverSettings server.Settings, err error,
|
||||
) {
|
||||
serverSettings.Logger = logger
|
||||
|
||||
upstreamResolvers := buildProviders(userSettings)
|
||||
|
||||
ipVersion := "ipv4"
|
||||
if *userSettings.IPv6 {
|
||||
ipVersion = "ipv6"
|
||||
}
|
||||
|
||||
var dialer server.Dialer
|
||||
switch userSettings.UpstreamType {
|
||||
case settings.DNSUpstreamTypeDot:
|
||||
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 settings.DNSUpstreamTypeDoh:
|
||||
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 settings.DNSUpstreamTypePlain:
|
||||
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: " + userSettings.UpstreamType)
|
||||
}
|
||||
serverSettings.Dialer = dialer
|
||||
|
||||
if *userSettings.Caching {
|
||||
lruCache, err := lru.New(lru.Settings{})
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
|
||||
}
|
||||
cacheMiddleware, err := cachemiddleware.New(cachemiddleware.Settings{
|
||||
Cache: lruCache,
|
||||
})
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating cache middleware: %w", err)
|
||||
}
|
||||
serverSettings.Middlewares = append(serverSettings.Middlewares, cacheMiddleware)
|
||||
}
|
||||
|
||||
filterMiddleware, err := filtermiddleware.New(filtermiddleware.Settings{
|
||||
Filter: filter,
|
||||
})
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating filter middleware: %w", err)
|
||||
}
|
||||
serverSettings.Middlewares = append(serverSettings.Middlewares, filterMiddleware)
|
||||
|
||||
localResolversAddrPorts := make([]netip.AddrPort, len(localResolvers))
|
||||
const defaultDNSPort = 53
|
||||
for i, addr := range localResolvers {
|
||||
localResolversAddrPorts[i] = netip.AddrPortFrom(addr, defaultDNSPort)
|
||||
}
|
||||
localDNSMiddleware, err := localdns.New(localdns.Settings{
|
||||
Resolvers: localResolversAddrPorts, // auto-detected at container start only
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
return server.Settings{}, fmt.Errorf("creating local DNS middleware: %w", err)
|
||||
}
|
||||
// Place after cache middleware, since we want to avoid caching for local
|
||||
// hostnames that may change regularly.
|
||||
// Place after filter middleware to avoid conflicts with the rebinding protection.
|
||||
serverSettings.Middlewares = append(serverSettings.Middlewares, localDNSMiddleware)
|
||||
|
||||
return serverSettings, nil
|
||||
}
|
||||
|
||||
func buildProviders(userSettings settings.DNS) []provider.Provider {
|
||||
if userSettings.UpstreamType == settings.DNSUpstreamTypePlain &&
|
||||
len(userSettings.UpstreamPlainAddresses) > 0 {
|
||||
providers := make([]provider.Provider, len(userSettings.UpstreamPlainAddresses))
|
||||
for i, addrPort := range userSettings.UpstreamPlainAddresses {
|
||||
providers[i] = provider.Provider{
|
||||
Name: addrPort.String(),
|
||||
}
|
||||
if addrPort.Addr().Is4() {
|
||||
providers[i].Plain.IPv4 = []netip.AddrPort{addrPort}
|
||||
} else {
|
||||
providers[i].Plain.IPv6 = []netip.AddrPort{addrPort}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
providersData := provider.NewProviders()
|
||||
providers := make([]provider.Provider, 0, len(userSettings.Providers)+len(userSettings.UpstreamPlainAddresses))
|
||||
for _, providerName := range userSettings.Providers {
|
||||
provider, err := providersData.Get(providerName)
|
||||
if err != nil {
|
||||
panic(err) // this should already had been checked
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
if userSettings.UpstreamType != settings.DNSUpstreamTypePlain {
|
||||
return providers
|
||||
}
|
||||
|
||||
for _, addrPort := range userSettings.UpstreamPlainAddresses {
|
||||
newProvider := provider.Provider{
|
||||
Name: addrPort.String(),
|
||||
}
|
||||
if addrPort.Addr().Is4() {
|
||||
newProvider.Plain.IPv4 = []netip.AddrPort{addrPort}
|
||||
} else {
|
||||
newProvider.Plain.IPv6 = []netip.AddrPort{addrPort}
|
||||
}
|
||||
providers = append(providers, newProvider)
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -4,52 +4,59 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/check"
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
"github.com/qdm12/dns/v2/pkg/server"
|
||||
"github.com/qdm12/dns/pkg/check"
|
||||
"github.com/qdm12/dns/pkg/nameserver"
|
||||
)
|
||||
|
||||
var errUpdateBlockLists = errors.New("cannot update filter block lists")
|
||||
var errUpdateFiles = errors.New("cannot update files")
|
||||
|
||||
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) {
|
||||
// Returning cancel == nil signals we want to re-run setupUnbound
|
||||
// Returning err == errUpdateFiles signals we should not fall back
|
||||
// on the plaintext DNS as DOT is still up and running.
|
||||
func (l *Loop) setupUnbound(ctx context.Context) (
|
||||
cancel context.CancelFunc, waitError chan error, closeStreams func(), err error) {
|
||||
err = l.updateFiles(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errUpdateBlockLists, err)
|
||||
return nil, nil, nil,
|
||||
fmt.Errorf("%w: %s", errUpdateFiles, err)
|
||||
}
|
||||
|
||||
settings := l.GetSettings()
|
||||
|
||||
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
|
||||
unboundCtx, cancel := context.WithCancel(context.Background())
|
||||
stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx,
|
||||
*settings.DoT.Unbound.VerbosityDetailsLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("building server settings: %w", err)
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
server, err := server.New(serverSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating server: %w", err)
|
||||
linesCollectionCtx, linesCollectionCancel := context.WithCancel(context.Background())
|
||||
lineCollectionDone := make(chan struct{})
|
||||
go l.collectLines(linesCollectionCtx, lineCollectionDone,
|
||||
stdoutLines, stderrLines)
|
||||
closeStreams = func() {
|
||||
linesCollectionCancel()
|
||||
<-lineCollectionDone
|
||||
}
|
||||
|
||||
runError, err = server.Start(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("starting server: %w", err)
|
||||
}
|
||||
l.server = server
|
||||
|
||||
// use internal DNS server
|
||||
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{})
|
||||
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
||||
ResolvPath: l.resolvConf,
|
||||
})
|
||||
// use Unbound
|
||||
nameserver.UseDNSInternally(settings.ServerAddress.AsSlice())
|
||||
err = nameserver.UseDNSSystemWide(l.resolvConf, settings.ServerAddress.AsSlice(),
|
||||
*settings.KeepNameserver)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
err = check.WaitForDNS(ctx, check.Settings{})
|
||||
if err != nil {
|
||||
l.stopServer()
|
||||
return nil, err
|
||||
if err := check.WaitForDNS(ctx, net.DefaultResolver); err != nil {
|
||||
cancel()
|
||||
<-waitError
|
||||
close(waitError)
|
||||
closeStreams()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return runError, nil
|
||||
return cancel, waitError, closeStreams, nil
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ func (s *State) GetSettings() (settings settings.DNS) {
|
||||
}
|
||||
|
||||
func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
outcome string,
|
||||
) {
|
||||
outcome string) {
|
||||
s.settingsMu.Lock()
|
||||
|
||||
settingsUnchanged := reflect.DeepEqual(s.settings, settings)
|
||||
@@ -27,7 +26,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
|
||||
// Check for only update period change
|
||||
tempSettings := s.settings.Copy()
|
||||
*tempSettings.UpdatePeriod = *settings.UpdatePeriod
|
||||
*tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod
|
||||
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
||||
|
||||
s.settings = settings
|
||||
@@ -40,6 +39,8 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
|
||||
// Restart
|
||||
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
||||
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||
if *settings.DoT.Enabled {
|
||||
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import (
|
||||
|
||||
func New(statusApplier StatusApplier,
|
||||
settings settings.DNS,
|
||||
updateTicker chan<- struct{},
|
||||
) *State {
|
||||
updateTicker chan<- struct{}) *State {
|
||||
return &State{
|
||||
statusApplier: statusApplier,
|
||||
settings: settings,
|
||||
|
||||
@@ -11,7 +11,6 @@ func (l *Loop) GetStatus() (status models.LoopStatus) {
|
||||
}
|
||||
|
||||
func (l *Loop) ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
||||
outcome string, err error,
|
||||
) {
|
||||
outcome string, err error) {
|
||||
return l.statusManager.ApplyStatus(ctx, status)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user