Compare commits

..

3 Commits

Author SHA1 Message Date
Quentin McGaw
c04cd621b0 fix(docker): openvpn 2.4.12-r0 install 2022-03-31 20:53:28 +00:00
Quentin McGaw
69c3f4e872 fix(env): OPENVPN_FLAGS functionality 2022-03-31 20:47:39 +00:00
Quentin McGaw
e1c20595b1 fix(health): use TCP dialing instead of ping
- `HEALTH_TARGET_ADDRESS` to replace `HEALTH_ADDRESS_TO_PING`
- Remove `github.com/go-ping/ping` dependency
- Dial TCP the target address, appending `:443` if port is not set
2022-03-21 20:38:55 +00:00
907 changed files with 100516 additions and 312287 deletions

View File

@@ -1,4 +1,5 @@
.dockerignore .dockerignore
devcontainer.json devcontainer.json
docker-compose.yml
Dockerfile Dockerfile
README.md README.md

View File

@@ -1,2 +1,2 @@
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine FROM qmcgaw/godevcontainer
RUN apk add wireguard-tools htop openssl RUN apk add wireguard-tools

View File

@@ -2,47 +2,68 @@
Development container that can be used with VSCode. Development container that can be used with VSCode.
It works on Linux, Windows (WSL2) and OSX. It works on Linux, Windows and OSX.
## Requirements ## Requirements
- [VS code](https://code.visualstudio.com/download) installed - [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](https://www.docker.com/products/docker-desktop) installed and running
- If you don't use Linux or WSL 2, share your home directory `~/` and the directory of your project with Docker Desktop
- [Docker Compose](https://docs.docker.com/compose/install/) installed
- Ensure your host has the following and that they are accessible by Docker:
- `~/.ssh` directory
- `~/.gitconfig` file (can be empty)
## Setup ## Setup
1. Create the following files and directory 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.
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P). 1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory. 1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
1. For Docker running on Windows HyperV, if you want to use SSH keys, bind mount them at `/tmp/.ssh` by changing the `volumes` section in the [docker-compose.yml](docker-compose.yml).
## Customization ## 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) You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
2. Select `Dev-Containers: Rebuild Container`
Changes you can make are notably: ```Dockerfile
FROM qmcgaw/godevcontainer
USER root
RUN apk add curl
USER vscode
```
- Changes to the Docker image in [Dockerfile](Dockerfile) Note that you may need to use `USER root` to build as root, and then change back to `USER vscode`.
- 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:
```json To rebuild the image, either:
// Welcome script
{ - With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
"source": "/yourpath/.welcome.sh", - With a terminal, go to this directory and `docker-compose build`
"target": "/root/.welcome.sh",
"type": "bind" ### 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 `/home/vscode/.welcome.sh` to replace the [current welcome script](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).
### 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.

View File

@@ -1,111 +1,81 @@
{ {
"name": "gluetun-dev", "name": "gluetun-dev",
// User defined settings "dockerComposeFile": [
"containerEnv": { "docker-compose.yml"
"TZ": "" ],
}, "service": "vscode",
// Fixed settings "runServices": [
"build": { "vscode"
"dockerfile": "./Dockerfile" ],
}, "shutdownAction": "stopCompose",
"postCreateCommand": "~/.windows.sh && go mod download", "postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
"capAdd": [ "workspaceFolder": "/workspace",
"NET_ADMIN", // Gluetun specific "extensions": [
"SYS_PTRACE" // for dlv Go debugging "golang.go",
], "eamodio.gitlens", // IDE Git information
"securityOpt": [ "davidanson.vscode-markdownlint",
"seccomp=unconfined" // for dlv Go debugging "ms-azuretools.vscode-docker", // Docker integration and linting
], "shardulm94.trailing-spaces", // Show trailing spaces
"mounts": [ "Gruntfuggly.todo-tree", // Highlights TODO comments
// Zsh commands history persistence "bierner.emojisense", // Emoji sense for markdown
{ "stkb.rewrap", // rewrap comments after n characters on one line
"source": "${localEnv:HOME}/.zsh_history", "vscode-icons-team.vscode-icons", // Better file extension icons
"target": "/root/.zsh_history", "github.vscode-pull-request-github", // Github interaction
"type": "bind" "redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
}, "bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
// Git configuration file "IBM.output-colorizer", // Colorize your output/test logs
{ "mohsen1.prettify-json", // Prettify JSON data
"source": "${localEnv:HOME}/.gitconfig", ],
"target": "/root/.gitconfig", "settings": {
"type": "bind" "files.eol": "\n",
}, "remote.extensionKind": {
// SSH directory for Linux, OSX and WSL "ms-azuretools.vscode-docker": "workspace"
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is },
// created in the container. On Windows, files are copied "editor.codeActionsOnSaveTimeout": 3000,
// from /mnt/ssh to ~/.ssh to fix permissions. "go.useLanguageServer": true,
{ "[go]": {
"source": "${localEnv:HOME}/.ssh", "editor.formatOnSave": true,
"target": "/mnt/ssh", "editor.codeActionsOnSave": {
"type": "bind" "source.organizeImports": true,
}, },
// Docker socket to access the host Docker server // Optional: Disable snippets, as they conflict with completion ranking.
{ "editor.snippetSuggestions": "none"
"source": "/var/run/docker.sock", },
"target": "/var/run/docker.sock", "[go.mod]": {
"type": "bind" "editor.formatOnSave": true,
} "editor.codeActionsOnSave": {
], "source.organizeImports": true,
"customizations": { },
"vscode": { },
"extensions": [ "gopls": {
"golang.go", "usePlaceholders": false,
"eamodio.gitlens", // IDE Git information "staticcheck": true
"davidanson.vscode-markdownlint", },
"ms-azuretools.vscode-docker", // Docker integration and linting "go.autocompleteUnimportedPackages": true,
"shardulm94.trailing-spaces", // Show trailing spaces "go.gotoSymbol.includeImports": true,
"Gruntfuggly.todo-tree", // Highlights TODO comments "go.gotoSymbol.includeGoroot": true,
"bierner.emojisense", // Emoji sense for markdown "go.lintTool": "golangci-lint",
"stkb.rewrap", // rewrap comments after n characters on one line "go.buildOnSave": "workspace",
"vscode-icons-team.vscode-icons", // Better file extension icons "go.lintOnSave": "workspace",
"github.vscode-pull-request-github", // Github interaction "go.vetOnSave": "workspace",
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting "editor.formatOnSave": true,
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked "go.toolsEnvVars": {
"IBM.output-colorizer", // Colorize your output/test logs "GOFLAGS": "-tags=",
"github.copilot" // AI code completion // "CGO_ENABLED": 1 // for the race detector
], },
"settings": { "gopls.env": {
"files.eol": "\n", "GOFLAGS": "-tags="
"remote.extensionKind": { },
"ms-azuretools.vscode-docker": "workspace" "go.testEnvVars": {
}, "": ""
"go.useLanguageServer": true, },
"[go]": { "go.testFlags": [
"editor.codeActionsOnSave": { "-v",
"source.organizeImports": "explicit" // "-race"
} ],
}, "go.testTimeout": "10s",
"[go.mod]": { "go.coverOnSingleTest": true,
"editor.codeActionsOnSave": { "go.coverOnSingleTestFile": true,
"source.organizeImports": "explicit" "go.coverOnTestPackage": true
} }
},
"gopls": {
"usePlaceholders": false,
"staticcheck": true,
"ui.diagnostic.analyses": {
"ST1000": false
},
"formatting.gofumpt": true,
},
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
"editor.formatOnSave": true,
"go.buildTags": "linux",
"go.toolsEnvVars": {
"CGO_ENABLED": "0"
},
"go.testEnvVars": {
"CGO_ENABLED": "1"
},
"go.testFlags": [
"-v",
"-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true
}
}
}
} }

View File

@@ -0,0 +1,33 @@
version: "3.7"
services:
vscode:
build: .
image: godevcontainer
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ../:/workspace
# Docker socket to access Docker server
- /var/run/docker.sock:/var/run/docker.sock
# Docker configuration
- ~/.docker:/root/.docker:z
# SSH directory for Linux, OSX and WSL
- ~/.ssh:/root/.ssh:z
# For Windows without WSL, a copy will be made
# from /tmp/.ssh to ~/.ssh to fix permissions
#- ~/.ssh:/tmp/.ssh:ro
# Shell history persistence
- ~/.zsh_history:/root/.zsh_history:z
# Git config
- ~/.gitconfig:/root/.gitconfig:z
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"

View File

@@ -13,6 +13,6 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
## Resources ## Resources
- [Gluetun guide on development](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) - [Gluetun guide on development](https://github.com/qdm12/gluetun/wiki/Development)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)

View File

@@ -7,18 +7,13 @@ body:
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
⚠️ Your issue will be instantly closed as not planned WITHOUT explanation if:
- you do not fill out **the title of the issue** ☝️
- you do not provide the **Gluetun version** as requested below
- you provide **less than 10 lines of logs** as requested below
- type: dropdown - type: dropdown
id: urgent id: urgent
attributes: attributes:
label: Is this urgent? label: Is this urgent?
description: | description: |
Is this a critical bug, or do you need this fixed urgently? Is this a critical bug, or do you need this fixed urgently?
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md) if that can help. If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) if that can help.
options: options:
- "No" - "No"
- "Yes" - "Yes"
@@ -45,12 +40,10 @@ body:
attributes: attributes:
label: VPN service provider label: VPN service provider
options: options:
- AirVPN
- Custom - Custom
- Cyberghost - Cyberghost
- ExpressVPN - ExpressVPN
- FastestVPN - FastestVPN
- Giganews
- HideMyAss - HideMyAss
- IPVanish - IPVanish
- IVPN - IVPN
@@ -61,10 +54,8 @@ body:
- PrivateVPN - PrivateVPN
- ProtonVPN - ProtonVPN
- PureVPN - PureVPN
- SlickVPN
- Surfshark - Surfshark
- TorGuard - TorGuard
- VPNSecure.me
- VPNUnlimited - VPNUnlimited
- VyprVPN - VyprVPN
- WeVPN - WeVPN
@@ -81,7 +72,6 @@ body:
- Portainer - Portainer
- Kubernetes - Kubernetes
- Podman - Podman
- Unraid
- Other - Other
validations: validations:
required: true required: true
@@ -91,7 +81,7 @@ body:
label: What is the version of Gluetun label: What is the version of Gluetun
description: | description: |
Copy paste the version line at the top of your logs. Copy paste the version line at the top of your logs.
It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`. It should be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -104,9 +94,9 @@ body:
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
label: Share your logs (at least 10 lines) label: Share your logs
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`. description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
render: plain text render: log
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -1,10 +1,6 @@
blank_issues_enabled: false
contact_links: contact_links:
- name: Report a Wiki issue
url: https://github.com/qdm12/gluetun-wiki/issues/new/choose
about: Please create an issue on the gluetun-wiki repository.
- name: Configuration help? - name: Configuration help?
url: https://github.com/qdm12/gluetun/discussions/new/choose url: https://github.com/qdm12/gluetun/discussions/new
about: Please create a Github discussion. about: Please create a Github discussion.
- name: Unraid template issue - name: Unraid template issue
url: https://github.com/qdm12/gluetun/discussions/550 url: https://github.com/qdm12/gluetun/discussions/550

View File

@@ -6,35 +6,12 @@ labels: ":bulb: New provider"
--- ---
Important notes: One of the following is required:
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible - Publicly accessible URL to a zip file containing the Openvpn configuration files
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately - Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
## For Wireguard
Wireguard can be natively supported ONLY if:
- the `PrivateKey` field value is the same across all servers for one user account
- the `Address` field value is:
- can be found in a structured (JSON etc.) list of servers publicly available; OR
- the same across all servers for one user account
- the `PublicKey` field value is:
- can be found in a structured (JSON etc.) list of servers publicly available; OR
- the same across all servers for one user account
- the `Endpoint` field value:
- can be found in a structured (JSON etc.) list of servers publicly available
- can be determined using a pattern, for example using country codes in hostnames
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
If **all** of these conditions are met, please provide an answer for each of them.
## For OpenVPN
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP - Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
If the list of servers requires to login **or** is hidden behind an interactive configurator,
you can only use a custom Openvpn configuration file.
[The Wiki](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to do so.

18
.github/ISSUE_TEMPLATE/wiki issue.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Wiki issue
description: Report a Wiki issue
title: "Wiki issue: "
labels: ["📄 Wiki issue"]
body:
- type: input
id: url
attributes:
label: "URL to the Wiki page"
placeholder: "https://github.com/qdm12/gluetun/wiki/OpenVPN-options"
validations:
required: true
- type: textarea
id: description
attributes:
label: "What's the issue?"
validations:
required: true

198
.github/labels.yml vendored
View File

@@ -1,152 +1,110 @@
- name: "Status: 🗯️ Waiting for feedback" - name: "Bug :bug:"
color: "f7d692" color: "b60205"
- name: "Status: 🔴 Blocked" description: ""
color: "f7d692" - name: "Feature request :bulb:"
description: "Blocked by another issue or pull request" color: "0e8a16"
- name: "Status: 📌 Before next release" description: ""
color: "f7d692" - name: "Help wanted :pray:"
description: "Has to be done before the next release" color: "4caf50"
- name: "Status: 🔒 After next release" description: ""
color: "f7d692" - name: "Documentation :memo:"
description: "Will be done after the next release" color: "c5def5"
- name: "Status: 🟡 Nearly resolved" description: ""
color: "f7d692" - name: "Needs more info :thinking:"
description: "This might be resolved or is about to be resolved" color: "795548"
description: ""
- name: "Closed: ⚰️ Inactive" # Priority
color: "959a9c" - name: "🚨 Urgent"
description: "No answer was received for weeks" color: "d5232f"
- name: "Closed: 👥 Duplicate" description: ""
color: "959a9c" - name: "💤 Low priority"
description: "Issue duplicates an existing issue" color: "4285f4"
- name: "Closed: 🗑️ Bad issue" description: ""
color: "959a9c"
- name: "Closed: ☠️ cannot be done"
color: "959a9c"
- name: "Priority: 🚨 Urgent"
color: "03adfc"
- name: "Priority: 💤 Low priority"
color: "03adfc"
- name: "Complexity: ☣️ Hard to do"
color: "ff9efc"
- name: "Complexity: 🟩 Easy to do"
color: "ff9efc"
- name: "Popularity: ❤️‍🔥 extreme"
color: "ffc7ea"
- name: "Popularity: ❤️ high"
color: "ffc7ea"
# VPN providers # VPN providers
- name: "☁️ AirVPN" - name: ":cloud: Cyberghost"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ Custom" description: ""
- name: ":cloud: HideMyAss"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ Cyberghost" description: ""
- name: ":cloud: IPVanish"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ Giganews" description: ""
- name: ":cloud: IVPN"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ HideMyAss" description: ""
- name: ":cloud: ExpressVPN"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ IPVanish" description: ""
- name: ":cloud: FastestVPN"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ IVPN" description: ""
- name: ":cloud: Mullvad"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ ExpressVPN" description: ""
- name: ":cloud: NordVPN"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ FastestVPN" description: ""
- name: ":cloud: Perfect Privacy"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ Mullvad" description: ""
- name: ":cloud: PIA"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ NordVPN" description: ""
- name: ":cloud: Privado"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ Perfect Privacy" description: ""
- name: ":cloud: PrivateVPN"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ PIA" description: ""
- name: ":cloud: ProtonVPN"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ Privado" - name: ":cloud: PureVPN"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ PrivateVPN" description: ""
- name: ":cloud: Surfshark"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ ProtonVPN" description: ""
- name: ":cloud: Torguard"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ PureVPN" description: ""
- name: ":cloud: VPNUnlimited"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ SlickVPN" description: ""
- name: ":cloud: Vyprvpn"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ Surfshark" description: ""
- name: ":cloud: WeVPN"
color: "cfe8d4" color: "cfe8d4"
- name: "☁️ Torguard" description: ""
color: "cfe8d4" - name: ":cloud: Windscribe"
- name: "☁️ VPNSecure.me"
color: "cfe8d4"
- name: "☁️ VPNUnlimited"
color: "cfe8d4"
- name: "☁️ Vyprvpn"
color: "cfe8d4"
- name: "☁️ WeVPN"
color: "cfe8d4"
- name: "☁️ Windscribe"
color: "cfe8d4" color: "cfe8d4"
description: ""
- name: "Category: User error 🤦" # Problem category
from_name: "Category: Config problem 📝" - name: "Openvpn"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Healthcheck 🩺" description: ""
- name: "Wireguard"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Documentation ✒️" description: ""
description: "A problem with the readme or a code comment." - name: "Unbound (DNS over TLS)"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Maintenance ⛓️" description: ""
description: "Anything related to code or other maintenance" - name: "Firewall"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Logs 📚" description: ""
description: "Something to change in logs" - name: "HTTP proxy"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Good idea 🎯" description: ""
description: "This is a good idea, judged by the maintainers" - name: "Shadowsocks"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Motivated! 🙌" description: ""
description: "Your pumpness makes me pumped! The issue or PR shows great motivation!" - name: "Healthcheck server"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Foolproof settings 👼" description: ""
color: "ffc7ea" - name: "Control server"
- name: "Category: Label missing ❗"
color: "ffc7ea"
- name: "Category: updater ♻️"
color: "ffc7ea"
description: "Concerns the code to update servers data"
- name: "Category: New provider 🆕"
color: "ffc7ea"
- name: "Category: OpenVPN 🔐"
color: "ffc7ea"
- name: "Category: Wireguard 🔐"
color: "ffc7ea"
- name: "Category: DNS 📠"
color: "ffc7ea"
- name: "Category: Firewall ⛓️"
color: "ffc7ea"
- name: "Category: Routing 🛤️"
color: "ffc7ea"
- name: "Category: IPv6 🛰️"
color: "ffc7ea"
- name: "Category: VPN port forwarding 📥"
color: "ffc7ea"
- name: "Category: HTTP proxy 🔁"
color: "ffc7ea"
- name: "Category: Shadowsocks 🔁"
color: "ffc7ea"
- name: "Category: control server ⚙️"
color: "ffc7ea"
- name: "Category: kernel 🧠"
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" color: "ffc7ea"
description: ""

View File

@@ -1,12 +0,0 @@
# Description
<!-- Please describe the reason for the changes being proposed. -->
# Issue
<!-- Please link to the issue(s) this change relates to. -->
# Assertions
* [ ] I am aware that we do not accept manual changes to the servers.json file <!-- If this is your goal, please consult https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-using-the-command-line -->
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)

View File

@@ -1,35 +0,0 @@
name: No trigger file paths
on:
push:
branches:
- master
paths-ignore:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
pull_request:
paths-ignore:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: No trigger path triggered for required verify workflow.
run: exit 0

View File

@@ -17,6 +17,8 @@ on:
- go.mod - go.mod
- go.sum - go.sum
pull_request: pull_request:
branches:
- master
paths: paths:
- .github/workflows/ci.yml - .github/workflows/ci.yml
- cmd/** - cmd/**
@@ -30,28 +32,28 @@ on:
jobs: jobs:
verify: verify:
# Only run if it's a push event or if it's a PR from this repository, and it is not dependabot.
if: |
github.actor != 'dependabot[bot]' &&
(github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
actions: read
contents: read
env: env:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v2.4.0
- uses: reviewdog/action-misspell@v1 - uses: reviewdog/action-misspell@v1
with: with:
locale: "US" locale: "US"
level: error level: error
exclude: |
./internal/storage/servers.json
*.md
- name: Linting - name: Linting
run: docker build --target lint . run: docker build --target lint .
- name: Mocks check - name: Go mod tidy check
run: docker build --target mocks . run: docker build --target tidy .
- name: Build test image - name: Build test image
run: docker build --target test -t test-container . run: docker build --target test -t test-container .
@@ -59,116 +61,69 @@ jobs:
- name: Run tests in test container - name: Run tests in test container
run: | run: |
touch coverage.txt touch coverage.txt
docker run --rm --device /dev/net/tun \ docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \ -v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container test-container
- name: Code security analysis
uses: snyk/actions/golang@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Build final image - name: Build final image
run: docker build -t final-image . run: docker build -t final-image .
verify-private: # - name: Image security analysis
if: | # uses: snyk/actions/docker@master
github.repository == 'qdm12/gluetun' && # env:
( # SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
github.event_name == 'push' || # with:
github.event_name == 'release' || # image: final-image
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify]
runs-on: ubuntu-latest
environment: secrets
steps:
- uses: actions/checkout@v5
- run: docker build -t qmcgaw/gluetun .
- name: Setup Go for CI utility
uses: actions/setup-go@v6
with:
go-version-file: ci/go.mod
- name: Build utility
run: go build -C ./ci -o runner ./cmd/main.go
- name: Run Gluetun container with Mullvad configuration
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{ secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
- name: Run Gluetun container with ProtonVPN configuration
run: echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner protonvpn
codeql:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: github/codeql-action/init@v4
with:
languages: go
- uses: github/codeql-action/autobuild@v4
- uses: github/codeql-action/analyze@v4
publish: publish:
# Only run if it's a push event or if it's a PR from this repository
if: | if: |
github.repository == 'qdm12/gluetun' && github.event_name == 'push' ||
( github.event_name == 'release' ||
github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
github.event_name == 'release' || needs: [verify]
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify, verify-private, codeql]
permissions:
actions: read
contents: read
packages: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v2.4.0
# extract metadata (tags, labels) for Docker # extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v3
with: with:
flavor: | flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
images: | images: |
ghcr.io/qdm12/gluetun
qmcgaw/gluetun qmcgaw/gluetun
qmcgaw/private-internet-access qmcgaw/private-internet-access
tags: | tags: |
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}
type=ref,event=pr type=ref,event=pr
type=semver,pattern=v{{major}}.{{minor}}.{{patch}} type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}} type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- uses: docker/setup-qemu-action@v3 - uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v3 - uses: docker/login-action@v1
with: with:
username: qmcgaw username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: qdm12
password: ${{ github.token }}
- name: Short commit - name: Short commit
id: shortcommit id: shortcommit
run: echo "::set-output name=value::$(git rev-parse --short HEAD)" run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
- name: Build and push final image - name: Build and push final image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v2.8.0
with: with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,21 +0,0 @@
name: Closed issue
on:
issues:
types: [closed]
jobs:
comment:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
body: |
Closed issues are **NOT** monitored, so commenting here is likely to be not seen.
If you think this is *still unresolved* and have **more information** to bring, please create another issue.
This is an automated comment setup because @qdm12 is the sole maintainer of this project
which became too popular to monitor issues closed.

View File

@@ -1,14 +0,0 @@
{
"ignorePatterns": [
{
"pattern": "^https://console.substack.com/p/console-72$"
}
],
"timeout": "20s",
"retryOn429": false,
"fallbackRetryDelay": "30s",
"aliveStatusCodes": [
200,
429
]
}

37
.github/workflows/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Dependabot
on:
pull_request:
branches:
- master
paths:
- .github/workflows/dependabot.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
if: ${{ github.actor == 'dependabot[bot]' }}
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2.4.0
- name: Build test image
run: docker build --target test -t test-container .
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Build final image
run: docker build -t final-image .

View File

@@ -0,0 +1,21 @@
name: Docker Hub description
on:
push:
branches: [master]
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.4.0
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: qmcgaw/gluetun
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
readme-filepath: README.md

40
.github/workflows/fork.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Fork
on:
pull_request:
branches:
- master
paths:
- .github/workflows/fork.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2.4.0
- name: Linting
run: docker build --target lint .
- name: Build test image
run: docker build --target test -t test-container .
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Build final image
run: docker build -t final-image .

View File

@@ -7,11 +7,9 @@ on:
- .github/workflows/labels.yml - .github/workflows/labels.yml
jobs: jobs:
labeler: labeler:
permissions:
issues: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v2.4.0
- uses: crazy-max/ghaction-github-labeler@v5 - uses: crazy-max/ghaction-github-labeler@v3
with: with:
yaml-file: .github/labels.yml yaml-file: .github/labels.yml

View File

@@ -1,21 +0,0 @@
name: Markdown
on:
push:
branches:
- master
paths-ignore:
- "**.md"
- .github/workflows/markdown.yml
pull_request:
paths-ignore:
- "**.md"
- .github/workflows/markdown.yml
jobs:
markdown:
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: No trigger path triggered for required markdown workflow.
run: exit 0

View File

@@ -1,47 +0,0 @@
name: Markdown
on:
push:
branches:
- master
paths:
- "**.md"
- .github/workflows/markdown.yml
pull_request:
paths:
- "**.md"
- .github/workflows/markdown.yml
jobs:
markdown:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@v5
- uses: DavidAnson/markdownlint-cli2-action@v21
with:
globs: "**.md"
config: .markdownlint-cli2.jsonc
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
pattern: |
*.md
- uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: yes
config-file: .github/workflows/configs/mlc-config.json
- uses: peter-evans/dockerhub-description@v4
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: qmcgaw/gluetun
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
readme-filepath: README.md

View File

@@ -1,22 +0,0 @@
name: Opened issue
on:
issues:
types: [opened]
jobs:
comment:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
body: |
@qdm12 is more or less the only maintainer of this project and works on it in his free time.
Please:
- **do not** ask for updates, be patient
- :+1: the issue to show your support instead of commenting
@qdm12 usually checks issues at least once a week, if this is a new urgent bug,
[revert to an older tagged container image](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md)

View File

@@ -1,94 +1,66 @@
version: "2" linters-settings:
misspell:
locale: US
formatters: issues:
enable: exclude-rules:
- gci - path: _test\.go
- gofumpt linters:
- goimports - dupl
exclusions: - maligned
generated: lax - goerr113
paths: - containedctx
- third_party$ - path: internal/server/
- builtin$ linters:
- examples$ - dupl
- path: internal/configuration/
linters:
- dupl
- path: internal/constants/
linters:
- dupl
- text: "exported: exported var Err*"
linters:
- revive
- text: "mnd: Magic number: 0644*"
linters:
- gomnd
- text: "mnd: Magic number: 0400*"
linters:
- gomnd
- text: "variable 'mssFix' is only used in the if-statement*"
path: "openvpnconf.go"
linters:
- ifshort
- text: "variable 'auth' is only used in the if-statement*"
path: "openvpnconf.go"
linters:
- ifshort
- linters:
- lll
source: "^//go:generate "
linters: linters:
settings:
misspell:
locale: US
goconst:
ignore-string-values:
# commonly used settings strings
- "^disabled$"
# Firewall and routing strings
- "^(ACCEPT|DROP)$"
- "^--delete$"
- "^all$"
- "^(tcp|udp)$"
# Server route strings
- "^/status$"
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- containedctx
- dupl
- err113
- maintidx
path: _test\.go
- linters:
- dupl
path: internal\/server\/.+\.go
- linters:
- ireturn
text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
- linters:
- ireturn
path: internal\/openvpn\/pkcs8\/descbc\.go
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
- linters:
- revive
path: internal\/provider\/(common|utils)\/.+\.go
text: "var-naming: avoid (bad|meaningless) package names"
- linters:
- lll
source: "^// https://.+$"
- linters:
- err113
- mnd
path: ci\/.+\.go
paths:
- third_party$
- builtin$
- examples$
enable: enable:
# - cyclop # - cyclop
# - errorlint # - errorlint
- asasalint # - ireturn
# - varnamelen
# - wrapcheck
- asciicheck - asciicheck
- bidichk - bidichk
- bodyclose - bodyclose
- containedctx - containedctx
- copyloopvar
- decorder - decorder
- dogsled - dogsled
- dupl - dupl
- dupword
- durationcheck - durationcheck
- err113
- errchkjson - errchkjson
- errname - errname
- exhaustive - exhaustive
- fatcontext - exportloopref
- forcetypeassert - forcetypeassert
- gocheckcompilerdirectives - gci
- gochecknoglobals - gochecknoglobals
- gochecknoinits - gochecknoinits
- gocognit - gocognit
@@ -96,44 +68,43 @@ linters:
- gocritic - gocritic
- gocyclo - gocyclo
- godot - godot
- goerr113
- goheader - goheader
- goimports
- gomnd
- gomoddirectives - gomoddirectives
- goprintffuncname - goprintffuncname
- gosec - gosec
- gosmopolitan
- grouper - grouper
- ifshort
- importas - importas
- interfacebloat
- intrange
- ireturn
- lll - lll
- maintidx - maintidx
- makezero - makezero
- mirror
- misspell - misspell
- mnd
- musttag
- nakedret - nakedret
- nestif - nestif
- nilerr - nilerr
- nilnil - nilnil
- noctx - noctx
- nolintlint - nolintlint
- nosprintfhostport
- paralleltest
- prealloc - prealloc
- predeclared - predeclared
- predeclared
- promlinter - promlinter
- reassign
- revive - revive
- rowserrcheck - rowserrcheck
- sqlclosecheck - sqlclosecheck
- tagalign - tenv
- thelper - thelper
- tparallel - tparallel
- unconvert - unconvert
- unparam - unparam
- usestdlibvars
- wastedassign - wastedassign
- whitespace - whitespace
- zerologlint
run:
skip-dirs:
- .devcontainer
- .github
- doc

View File

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

View File

@@ -1,8 +0,0 @@
{
// This list should be kept to the strict minimum
// to develop this project.
"recommendations": [
"golang.go",
"davidanson.vscode-markdownlint",
],
}

29
.vscode/settings.json vendored
View File

@@ -1,29 +0,0 @@
{
// The settings should be kept to the strict minimum
// to develop this project.
"files.eol": "\n",
"editor.formatOnSave": true,
"go.buildTags": "linux",
"go.toolsEnvVars": {
"CGO_ENABLED": "0"
},
"go.testEnvVars": {
"CGO_ENABLED": "1"
},
"go.testFlags": [
"-v",
"-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true,
"go.useLanguageServer": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package"
}

51
.vscode/tasks.json vendored
View File

@@ -1,51 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Update a VPN provider servers data",
"type": "shell",
"command": "go",
"args": [
"run",
"./cmd/gluetun/main.go",
"update",
"${input:updateMode}",
"-providers",
"${input:provider}"
],
},
{
"label": "Add a Gluetun Github Git remote",
"type": "shell",
"command": "git",
"args": [
"remote",
"add",
"${input:githubRemoteUsername}",
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
],
}
],
"inputs": [
{
"id": "provider",
"type": "promptString",
"description": "Please enter a provider (or comma separated list of providers)",
},
{
"id": "updateMode",
"type": "pickString",
"description": "Update mode to use",
"options": [
"-maintainer",
"-enduser"
],
"default": "-maintainer"
},
{
"id": "githubRemoteUsername",
"type": "promptString",
"description": "Please enter a Github username",
},
]
}

View File

@@ -1,22 +1,18 @@
ARG ALPINE_VERSION=3.22 ARG ALPINE_VERSION=3.15
ARG GO_ALPINE_VERSION=3.22 ARG GO_ALPINE_VERSION=3.15
ARG GO_VERSION=1.25 ARG GO_VERSION=1.17
ARG XCPUTRANSLATE_VERSION=v0.9.0 ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v2.4.0 ARG GOLANGCI_LINT_VERSION=v1.44.2
ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64 ARG BUILDPLATFORM=linux/amd64
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
# Note: findutils needed to have xargs support `-d` flag for mocks stage. RUN apk --update add git g++
RUN apk --update add git g++ findutils
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
COPY --from=golangci-lint /bin /go/bin/golangci-lint COPY --from=golangci-lint /bin /go/bin/golangci-lint
COPY --from=mockgen /bin /go/bin/mockgen
WORKDIR /tmp/gobuild WORKDIR /tmp/gobuild
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
@@ -32,19 +28,16 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
FROM --platform=${BUILDPLATFORM} base AS lint FROM --platform=${BUILDPLATFORM} base AS lint
COPY .golangci.yml ./ COPY .golangci.yml ./
RUN golangci-lint run RUN golangci-lint run --timeout=10m
FROM --platform=${BUILDPLATFORM} base AS mocks FROM --platform=${BUILDPLATFORM} base AS tidy
RUN git init && \ RUN git init && \
git config user.email ci@localhost && \ git config user.email ci@localhost && \
git config user.name ci && \ git config user.name ci && \
git config core.fileMode false && \ git add -A && git commit -m ci && \
git add -A && \ sed -i '/\/\/ indirect/d' go.mod && \
git commit -m "snapshot" && \ go mod tidy && \
grep -lr -E '^// Code generated by MockGen\. DO NOT EDIT\.$' . | xargs -r -d '\n' rm && \ git diff --exit-code -- go.mod
go generate -run "mockgen" ./... && \
git diff --exit-code && \
rm -rf .git/
FROM --platform=${BUILDPLATFORM} base AS build FROM --platform=${BUILDPLATFORM} base AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
@@ -76,85 +69,53 @@ LABEL \
ENV VPN_SERVICE_PROVIDER=pia \ ENV VPN_SERVICE_PROVIDER=pia \
VPN_TYPE=openvpn \ VPN_TYPE=openvpn \
# Common VPN options # Common VPN options
VPN_ENDPOINT_IP= \
VPN_ENDPOINT_PORT= \
VPN_INTERFACE=tun0 \ VPN_INTERFACE=tun0 \
# OpenVPN # OpenVPN
OPENVPN_ENDPOINT_IP= \
OPENVPN_ENDPOINT_PORT= \
OPENVPN_PROTOCOL=udp \ OPENVPN_PROTOCOL=udp \
OPENVPN_USER= \ OPENVPN_USER= \
OPENVPN_PASSWORD= \ OPENVPN_PASSWORD= \
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \ OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \ OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
OPENVPN_VERSION=2.6 \ OPENVPN_VERSION=2.5 \
OPENVPN_VERBOSITY=1 \ OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \ OPENVPN_FLAGS= \
OPENVPN_CIPHERS= \ OPENVPN_CIPHERS= \
OPENVPN_AUTH= \ OPENVPN_AUTH= \
OPENVPN_PROCESS_USER=root \ OPENVPN_PROCESS_USER= \
OPENVPN_MSSFIX= \ OPENVPN_IPV6=off \
OPENVPN_CUSTOM_CONFIG= \ OPENVPN_CUSTOM_CONFIG= \
# Wireguard # Wireguard
WIREGUARD_ENDPOINT_IP= \
WIREGUARD_ENDPOINT_PORT= \
WIREGUARD_CONF_SECRETFILE=/run/secrets/wg0.conf \
WIREGUARD_PRIVATE_KEY= \ WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
WIREGUARD_PRESHARED_KEY= \ WIREGUARD_PRESHARED_KEY= \
WIREGUARD_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
WIREGUARD_PUBLIC_KEY= \ WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ALLOWED_IPS= \
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
WIREGUARD_ADDRESSES= \ WIREGUARD_ADDRESSES= \
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
WIREGUARD_MTU=1320 \
WIREGUARD_IMPLEMENTATION=auto \
# VPN server filtering # VPN server filtering
SERVER_REGIONS= \ SERVER_REGIONS= \
SERVER_COUNTRIES= \ SERVER_COUNTRIES= \
SERVER_CITIES= \ SERVER_CITIES= \
SERVER_HOSTNAMES= \ SERVER_HOSTNAMES= \
SERVER_CATEGORIES= \
# # Mullvad only: # # Mullvad only:
ISP= \ ISP= \
OWNED_ONLY=no \ OWNED_ONLY=no \
# # Private Internet Access only: # # Private Internet Access only:
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \ PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
VPN_PORT_FORWARDING=off \ PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING=off \
VPN_PORT_FORWARDING_LISTENING_PORT=0 \ PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
VPN_PORT_FORWARDING_PROVIDER= \
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: # # Cyberghost only:
OPENVPN_CERT= \
OPENVPN_KEY= \
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \ OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \ OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
# # VPNSecure only:
OPENVPN_ENCRYPTED_KEY= \
OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key \
OPENVPN_KEY_PASSPHRASE= \
OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase \
# # Nordvpn only: # # Nordvpn only:
SERVER_NUMBER= \ SERVER_NUMBER= \
# # PIA only: # # PIA and ProtonVPN only:
SERVER_NAMES= \ SERVER_NAMES= \
# # VPNUnlimited and ProtonVPN only:
STREAM_ONLY= \
FREE_ONLY= \
# # ProtonVPN only: # # ProtonVPN only:
SECURE_CORE_ONLY= \ FREE_ONLY= \
TOR_ONLY= \
# # Surfshark only: # # Surfshark only:
MULTIHOP_ONLY= \ MULTIHOP_ONLY= \
# # VPN Secure only:
PREMIUM_ONLY= \
# # PIA and ProtonVPN only:
PORT_FORWARD_ONLY= \
# Firewall # Firewall
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \ FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \ FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \ FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \ FIREWALL_OUTBOUND_SUBNETS= \
@@ -163,23 +124,22 @@ ENV VPN_SERVICE_PROVIDER=pia \
LOG_LEVEL=info \ LOG_LEVEL=info \
# Health # Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \ HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_TARGET_ADDRESSES=cloudflare.com:443,github.com:443 \ HEALTH_TARGET_ADDRESS=github.com:443 \
HEALTH_ICMP_TARGET_IPS=1.1.1.1,8.8.8.8 \ HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_SMALL_CHECK_TYPE=icmp \ HEALTH_VPN_DURATION_ADDITION=5s \
HEALTH_RESTART_VPN=on \ # DNS over TLS
# DNS DOT=on \
DNS_SERVER=on \ DOT_PROVIDERS=cloudflare \
DNS_UPSTREAM_RESOLVER_TYPE=DoT \ DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
DNS_UPSTREAM_RESOLVERS=cloudflare \ DOT_VERBOSITY=1 \
DNS_BLOCK_IPS= \ DOT_VERBOSITY_DETAILS=0 \
DNS_BLOCK_IP_PREFIXES= \ DOT_VALIDATION_LOGLEVEL=0 \
DNS_CACHING=on \ DOT_CACHING=on \
DNS_UPSTREAM_IPV6=off \ DOT_IPV6=off \
BLOCK_MALICIOUS=on \ BLOCK_MALICIOUS=on \
BLOCK_SURVEILLANCE=off \ BLOCK_SURVEILLANCE=off \
BLOCK_ADS=off \ BLOCK_ADS=off \
DNS_UNBLOCK_HOSTNAMES= \ UNBLOCK= \
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
DNS_UPDATE_PERIOD=24h \ DNS_UPDATE_PERIOD=24h \
DNS_ADDRESS=127.0.0.1 \ DNS_ADDRESS=127.0.0.1 \
DNS_KEEP_NAMESERVER=off \ DNS_KEEP_NAMESERVER=off \
@@ -187,7 +147,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
HTTPPROXY= \ HTTPPROXY= \
HTTPPROXY_LOG=off \ HTTPPROXY_LOG=off \
HTTPPROXY_LISTENING_ADDRESS=":8888" \ HTTPPROXY_LISTENING_ADDRESS=":8888" \
HTTPPROXY_STEALTH=off \
HTTPPROXY_USER= \ HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \ HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \ HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
@@ -200,23 +159,13 @@ ENV VPN_SERVICE_PROVIDER=pia \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \ SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \ SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
# Control server # Control server
HTTP_CONTROL_SERVER_LOG=on \
HTTP_CONTROL_SERVER_ADDRESS=":8000" \ HTTP_CONTROL_SERVER_ADDRESS=":8000" \
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
# Server data updater # Server data updater
UPDATER_PERIOD=0 \ UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \ UPDATER_VPN_SERVICE_PROVIDERS= \
UPDATER_PROTONVPN_EMAIL= \
UPDATER_PROTONVPN_PASSWORD= \
# Public IP # Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \ PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_ENABLED=on \ PUBLICIP_PERIOD=12h \
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
PUBLICIP_API_TOKEN= \
# Storage
STORAGE_FILEPATH=/gluetun/servers.json \
# Pprof # Pprof
PPROF_ENABLED=no \ PPROF_ENABLED=no \
PPROF_BLOCK_PROFILE_RATE=0 \ PPROF_BLOCK_PROFILE_RATE=0 \
@@ -225,19 +174,22 @@ ENV VPN_SERVICE_PROVIDER=pia \
# Extras # Extras
VERSION_INFORMATION=on \ VERSION_INFORMATION=on \
TZ= \ TZ= \
PUID=1000 \ PUID= \
PGID=1000 PGID=
ENTRYPOINT ["/gluetun-entrypoint"] ENTRYPOINT ["/gluetun-entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apk add --no-cache --update -l wget && \ RUN apk add --no-cache --update -l apk-tools && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \ apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.12-r0 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
apk del openvpn && \ apk del openvpn && \
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \ apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \ # Fix vulnerability issue
apk add --no-cache --update busybox && \
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 openvpn && \
deluser unbound && \
mkdir /gluetun mkdir /gluetun
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint

257
README.md
View File

@@ -1,130 +1,127 @@
# Gluetun VPN client # Gluetun VPN client
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️ *Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN,
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN,
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers
using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
**ANNOUNCEMENT**: Large settings refactor merged on 2022-06-01, please file issues if you find any problem!
[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
[![Docker pulls qmcgaw/gluetun](https://img.shields.io/docker/pulls/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker pulls qmcgaw/private-internet-access](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun) [![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
[![Docker stars qmcgaw/gluetun](https://img.shields.io/docker/stars/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun) [![Docker pulls qmcgaw/gluetun](https://img.shields.io/docker/pulls/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker stars qmcgaw/private-internet-access](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun) [![Docker pulls qmcgaw/private-internet-access](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
![Last release](https://img.shields.io/github/release/qdm12/gluetun?label=Last%20release) [![Docker stars qmcgaw/gluetun](https://img.shields.io/docker/stars/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
![Last Docker tag](https://img.shields.io/docker/v/qmcgaw/gluetun?sort=semver&label=Last%20Docker%20tag) [![Docker stars qmcgaw/private-internet-access](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Last release size](https://img.shields.io/docker/image-size/qmcgaw/gluetun?sort=semver&label=Last%20released%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
![GitHub last release date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date) ![Last release](https://img.shields.io/github/release/qdm12/gluetun?label=Last%20release)
![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver) ![Last Docker tag](https://img.shields.io/docker/v/qmcgaw/gluetun?sort=semver&label=Last%20Docker%20tag)
[![Last release size](https://img.shields.io/docker/image-size/qmcgaw/gluetun?sort=semver&label=Last%20released%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
[![Latest size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags) ![GitHub last release date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date)
![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits/master)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/graphs/contributors) [![Latest size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues) [![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits/master)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed) [![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/graphs/contributors)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun) [![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun) [![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/gluetun)
[![Lines of code](https://img.shields.io/tokei/lines/github/qdm12/gluetun)](https://github.com/qdm12/gluetun)
![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme) ![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun)
![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun)
## Quick links ![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/gluetun)
- [Setup](#setup) ![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme)
- [Features](#features)
- Problem? ## Quick links
- Check the Wiki [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
- [Start a discussion](https://github.com/qdm12/gluetun/discussions) - [Setup](#Setup)
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550) - [Features](#Features)
- Suggestion? - Problem?
- [Create an issue](https://github.com/qdm12/gluetun/issues) - [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
- Happy? - [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12) - [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw) - Suggestion?
- Drop me [an email](mailto:quentin.mcgaw@gmail.com) - [Create an issue](https://github.com/qdm12/gluetun/issues)
- **Want to add a VPN provider?** check [the development page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) and [add a provider page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/add-a-provider.md) - [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
- Video: - Happy?
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4) - Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
- [Substack Console interview](https://console.substack.com/p/console-72) - Video:
## Features [![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4)
- Based on Alpine 3.22 for a small Docker image of 41.1MB - [Substack Console interview](https://console.substack.com/p/console-72)
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
- Supports OpenVPN for all providers listed ## Features
- Supports Wireguard both kernelspace and userspace
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe** - Based on Alpine 3.15 for a small Docker image of 29MB
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - Supports OpenVPN for all providers listed
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134) - Supports Wireguard both kernelspace and userspace
- DNS over TLS baked in with service provider(s) of your choice - For **Mullvad**, **Ivpn** and **Windscribe**
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours - For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- Choose the vpn network protocol, `udp` or `tcp` - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices - More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP) - DNS over TLS baked in with service provider(s) of your choice
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP) - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md) - Choose the vpn network protocol, `udp` or `tcp`
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md) - Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆 - Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- 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) - Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers - [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun)
- Can work as a Kubernetes sidecar container, thanks @rorph - [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-a-LAN-device-to-gluetun)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
## Setup - [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible! - Unbound subprogram drops root privileges once launched
- Can work as a Kubernetes sidecar container, thanks @rorph
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
## Setup
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new/choose)
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
Here's a docker-compose.yml for the laziest:
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
```yml
--- [🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun/issues/new?assignees=&labels=%F0%9F%93%84+Wiki+issue&template=wiki+issue.yml&title=Wiki+issue%3A+)
services:
gluetun: Here's a docker-compose.yml for the laziest:
image: qmcgaw/gluetun
# container_name: gluetun ```yml
# line above must be uncommented to allow external containers to connect. version: "3"
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md#external-container-to-gluetun services:
cap_add: gluetun:
- NET_ADMIN image: qmcgaw/gluetun
devices: # container_name: gluetun
- /dev/net/tun:/dev/net/tun # line above must be uncommented to allow external containers to connect. See https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun#external-container-to-gluetun
ports: cap_add:
- 8888:8888/tcp # HTTP proxy - NET_ADMIN
- 8388:8388/tcp # Shadowsocks ports:
- 8388:8388/udp # Shadowsocks - 8888:8888/tcp # HTTP proxy
volumes: - 8388:8388/tcp # Shadowsocks
- /yourpath:/gluetun - 8388:8388/udp # Shadowsocks
environment: volumes:
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup - /yourpath:/gluetun
- VPN_SERVICE_PROVIDER=ivpn environment:
- VPN_TYPE=openvpn # See https://github.com/qdm12/gluetun/wiki
# OpenVPN: - VPN_SERVICE_PROVIDER=ivpn
- OPENVPN_USER= - VPN_TYPE=openvpn
- OPENVPN_PASSWORD= # OpenVPN:
# Wireguard: - OPENVPN_USER=
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU= - OPENVPN_PASSWORD=
# - WIREGUARD_ADDRESSES=10.64.222.21/32 # Wireguard:
# Timezone for accurate log times # - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
- TZ= # - WIREGUARD_ADDRESSES=10.64.222.21/32
# Server list updater # Timezone for accurate log times
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list - TZ=
- UPDATER_PERIOD= ```
```
## License
🆕 Image also available as `ghcr.io/qdm12/gluetun`
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/master/LICENSE)
## License
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/blob/master/LICENSE)

View File

@@ -1,35 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/qdm12/gluetun/ci/internal"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: " + os.Args[0] + " <command>")
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
var err error
switch os.Args[1] {
case "mullvad":
err = internal.MullvadTest(ctx)
case "protonvpn":
err = internal.ProtonVPNTest(ctx)
default:
err = fmt.Errorf("unknown command: %s", os.Args[1])
}
stop()
if err != nil {
fmt.Println("❌", err)
os.Exit(1)
}
fmt.Println("✅ Test completed successfully.")
}

View File

@@ -1,36 +0,0 @@
module github.com/qdm12/gluetun/ci
go 1.25.0
require (
github.com/docker/docker v28.5.1+incompatible
github.com/opencontainers/image-spec v1.1.1
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/time v0.14.0 // indirect
gotest.tools/v3 v3.5.2 // indirect
)

View File

@@ -1,97 +0,0 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=

View File

@@ -1,27 +0,0 @@
package internal
import (
"context"
"fmt"
)
func MullvadTest(ctx context.Context) error {
expectedSecrets := []string{
"Wireguard private key",
"Wireguard address",
}
secrets, err := readSecrets(ctx, expectedSecrets)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=mullvad",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=USA",
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
"WIREGUARD_ADDRESSES=" + secrets[1],
}
return simpleTest(ctx, env)
}

View File

@@ -1,25 +0,0 @@
package internal
import (
"context"
"fmt"
)
func ProtonVPNTest(ctx context.Context) error {
expectedSecrets := []string{
"Wireguard private key",
}
secrets, err := readSecrets(ctx, expectedSecrets)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=protonvpn",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=United States",
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
}
return simpleTest(ctx, env)
}

View File

@@ -1,42 +0,0 @@
package internal
import (
"bufio"
"context"
"fmt"
"os"
"strings"
)
func readSecrets(ctx context.Context, expectedSecrets []string) (lines []string, err error) {
scanner := bufio.NewScanner(os.Stdin)
lines = make([]string, 0, len(expectedSecrets))
for i := range expectedSecrets {
fmt.Println("🤫 reading", expectedSecrets[i], "from Stdin...")
if !scanner.Scan() {
break
}
lines = append(lines, strings.TrimSpace(scanner.Text()))
fmt.Println("🤫 "+expectedSecrets[i], "secret read successfully")
if ctx.Err() != nil {
return nil, ctx.Err()
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("reading secrets from stdin: %w", err)
}
if len(lines) < len(expectedSecrets) {
return nil, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
len(expectedSecrets), len(lines))
}
for i, line := range lines {
if line == "" {
return nil, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
}
}
return lines, nil
}

View File

@@ -1,134 +0,0 @@
package internal
import (
"bufio"
"context"
"fmt"
"io"
"regexp"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func ptrTo[T any](v T) *T { return &v }
func simpleTest(ctx context.Context, env []string) error {
const timeout = 30 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("creating Docker client: %w", err)
}
defer client.Close()
config := &container.Config{
Image: "qmcgaw/gluetun",
StopTimeout: ptrTo(3),
Env: env,
}
hostConfig := &container.HostConfig{
AutoRemove: true,
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
}
networkConfig := (*network.NetworkingConfig)(nil)
platform := (*v1.Platform)(nil)
const containerName = "" // auto-generated name
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
if err != nil {
return fmt.Errorf("creating container: %w", err)
}
for _, warning := range response.Warnings {
fmt.Println("Warning during container creation:", warning)
}
containerID := response.ID
defer stopContainer(client, containerID)
beforeStartTime := time.Now()
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
if err != nil {
return fmt.Errorf("starting container: %w", err)
}
return waitForLogLine(ctx, client, containerID, beforeStartTime)
}
func stopContainer(client *client.Client, containerID string) {
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
defer stopCancel()
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
if err != nil {
fmt.Println("failed to stop container:", err)
}
}
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
beforeStartTime time.Time,
) error {
logOptions := container.LogsOptions{
ShowStdout: true,
Follow: true,
Since: beforeStartTime.Format(time.RFC3339Nano),
}
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
if err != nil {
return fmt.Errorf("error getting container logs: %w", err)
}
defer reader.Close()
var linesSeen []string
scanner := bufio.NewScanner(reader)
for ctx.Err() == nil {
if scanner.Scan() {
line := scanner.Text()
if len(line) > 8 { // remove Docker log prefix
line = line[8:]
}
linesSeen = append(linesSeen, line)
if successRegexp.MatchString(line) {
fmt.Println("✅ Success line logged")
return nil
}
continue
}
err := scanner.Err()
if err != nil && err != io.EOF {
logSeenLines(linesSeen)
return fmt.Errorf("reading log stream: %w", err)
}
// The scanner is either done or cannot read because of EOF
fmt.Println("The log scanner stopped")
logSeenLines(linesSeen)
// Check if the container is still running
inspect, err := client.ContainerInspect(ctx, containerID)
if err != nil {
return fmt.Errorf("inspecting container: %w", err)
}
if !inspect.State.Running {
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
}
}
return ctx.Err()
}
func logSeenLines(lines []string) {
fmt.Println("Logs seen so far:")
for _, line := range lines {
fmt.Println(" " + line)
}
}

View File

@@ -4,10 +4,8 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"strings" "strings"
"syscall" "syscall"
@@ -15,11 +13,13 @@ import (
_ "time/tzdata" _ "time/tzdata"
_ "github.com/breml/rootcerts" _ "github.com/breml/rootcerts"
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/gluetun/internal/alpine" "github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/cli" "github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/command" "github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/sources/env"
"github.com/qdm12/gluetun/internal/configuration/sources/files" "github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/mux"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets" "github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/dns" "github.com/qdm12/gluetun/internal/dns"
@@ -29,28 +29,24 @@ import (
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/portforward" "github.com/qdm12/gluetun/internal/portforward"
"github.com/qdm12/gluetun/internal/pprof" "github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip" "github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/routing" "github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun" "github.com/qdm12/gluetun/internal/tun"
updater "github.com/qdm12/gluetun/internal/updater/loop" "github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
"github.com/qdm12/gluetun/internal/vpn" "github.com/qdm12/gluetun/internal/vpn"
"github.com/qdm12/gosettings/reader" "github.com/qdm12/golibs/command"
"github.com/qdm12/gosettings/reader/sources/env" "github.com/qdm12/golibs/logging"
"github.com/qdm12/goshutdown" "github.com/qdm12/goshutdown"
"github.com/qdm12/goshutdown/goroutine" "github.com/qdm12/goshutdown/goroutine"
"github.com/qdm12/goshutdown/group" "github.com/qdm12/goshutdown/group"
"github.com/qdm12/goshutdown/order" "github.com/qdm12/goshutdown/order"
"github.com/qdm12/gosplash" "github.com/qdm12/gosplash"
"github.com/qdm12/log" "github.com/qdm12/updated/pkg/dnscrypto"
) )
//nolint:gochecknoglobals //nolint:gochecknoglobals
@@ -68,45 +64,37 @@ func main() {
} }
background := context.Background() background := context.Background()
signalCh := make(chan os.Signal, 1) signalCtx, stop := signal.NotifyContext(background, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(background) ctx, cancel := context.WithCancel(background)
logger := log.New(log.SetLevel(log.LevelInfo)) logger := logging.New(logging.Settings{
Level: logging.LevelInfo,
})
args := os.Args args := os.Args
tun := tun.New() tun := tun.New()
netLinkDebugLogger := logger.New(log.SetComponent("netlink")) netLinker := netlink.New()
netLinker := netlink.New(netLinkDebugLogger)
cli := cli.New() cli := cli.New()
cmder := command.New() cmder := command.NewCmder()
reader := reader.New(reader.Settings{ envReader := env.New(logger)
Sources: []reader.Source{ filesReader := files.New()
secrets.New(logger), secretsReader := secrets.New()
files.New(logger), muxReader := mux.New(envReader, filesReader, secretsReader)
env.New(env.Settings{}),
},
HandleDeprecatedKey: func(source, deprecatedKey, currentKey string) {
logger.Warn("You are using the old " + source + " " + deprecatedKey +
", please consider changing it to " + currentKey)
},
})
errorCh := make(chan error) errorCh := make(chan error)
go func() { go func() {
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli) errorCh <- _main(ctx, buildInfo, args, logger, muxReader, tun, netLinker, cmder, cli)
}() }()
// Wait for OS signal or run error
var err error
select { select {
case receivedSignal := <-signalCh: case <-signalCtx.Done():
signal.Stop(signalCh) stop()
fmt.Println("") fmt.Println("")
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down") logger.Warn("Caught OS signal, shutting down")
cancel() cancel()
case err = <-errorCh: case err := <-errorCh:
stop()
close(errorCh) close(errorCh)
if err == nil { // expected exit such as healthcheck if err == nil { // expected exit such as healthcheck
os.Exit(0) os.Exit(0)
@@ -115,58 +103,48 @@ func main() {
cancel() cancel()
} }
// Shutdown timed sequence, and force exit on second OS signal
const shutdownGracePeriod = 5 * time.Second const shutdownGracePeriod = 5 * time.Second
timer := time.NewTimer(shutdownGracePeriod) timer := time.NewTimer(shutdownGracePeriod)
select { select {
case shutdownErr := <-errorCh: case <-errorCh:
timer.Stop() if !timer.Stop() {
if shutdownErr != nil { <-timer.C
logger.Warnf("Shutdown failed: %s", shutdownErr)
os.Exit(1)
} }
logger.Info("Shutdown successful") logger.Info("Shutdown successful")
if err != nil {
os.Exit(1)
}
os.Exit(0)
case <-timer.C: case <-timer.C:
logger.Warn("Shutdown timed out") logger.Warn("Shutdown timed out")
os.Exit(1)
} }
os.Exit(1)
} }
var errCommandUnknown = errors.New("command is unknown") var (
errCommandUnknown = errors.New("command is unknown")
)
//nolint:gocognit,gocyclo,maintidx //nolint:gocognit,gocyclo,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation, func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger log.LoggerInterface, reader *reader.Reader, args []string, logger logging.ParentLogger, source sources.Source,
tun Tun, netLinker netLinker, cmder RunStarter, tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter,
cli clier, cli cli.CLIer) error {
) error {
if len(args) > 1 { // cli operation if len(args) > 1 { // cli operation
switch args[1] { switch args[1] {
case "healthcheck": case "healthcheck":
return cli.HealthCheck(ctx, reader, logger) return cli.HealthCheck(ctx, source, logger)
case "clientkey": case "clientkey":
return cli.ClientKey(args[2:]) return cli.ClientKey(args[2:])
case "openvpnconfig": case "openvpnconfig":
return cli.OpenvpnConfig(logger, reader, netLinker) return cli.OpenvpnConfig(logger, source)
case "update": case "update":
return cli.Update(ctx, args[2:], logger) return cli.Update(ctx, args[2:], logger)
case "format-servers": case "format-servers":
return cli.FormatServers(args[2:]) return cli.FormatServers(args[2:])
case "genkey":
return cli.GenKey(args[2:])
default: default:
return fmt.Errorf("%w: %s", errCommandUnknown, args[1]) return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
} }
} }
defer fmt.Println(gluetunLogo) announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
if err != nil { if err != nil {
return err return err
} }
@@ -176,8 +154,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
Emails: []string{"quentin.mcgaw@gmail.com"}, Emails: []string{"quentin.mcgaw@gmail.com"},
Version: buildInfo.Version, Version: buildInfo.Version,
Commit: buildInfo.Commit, Commit: buildInfo.Commit,
Created: buildInfo.Created, BuildDate: buildInfo.Created,
Announcement: "All control server routes will become private by default after the v3.41.0 release", Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!",
AnnounceExp: announcementExp, AnnounceExp: announcementExp,
// Sponsor information // Sponsor information
PaypalUser: "qmcgaw", PaypalUser: "qmcgaw",
@@ -187,30 +165,26 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
fmt.Println(line) fmt.Println(line)
} }
var allSettings settings.Settings allSettings, err := source.Read()
err = allSettings.Read(reader, logger)
if err != nil { if err != nil {
return err return err
} }
allSettings.SetDefaults()
// Note: no need to validate minimal settings for the firewall: // Note: no need to validate minimal settings for the firewall:
// - global log level is parsed below // - global log level is parsed from source
// - firewall Debug and Enabled are booleans parsed from source // - firewall Debug and Enabled are booleans parsed from source
logLevel, err := log.ParseLevel(allSettings.Log.Level)
if err != nil {
return fmt.Errorf("log level: %w", err)
}
logger.Patch(log.SetLevel(logLevel))
netLinker.PatchLoggerLevel(logLevel)
routingLogger := logger.New(log.SetComponent("routing")) logger.PatchLevel(*allSettings.Log.Level)
routingLogger := logger.NewChild(logging.Settings{
Prefix: "routing: ",
})
if *allSettings.Firewall.Debug { // To remove in v4 if *allSettings.Firewall.Debug { // To remove in v4
routingLogger.Patch(log.SetLevel(log.LevelDebug)) routingLogger.PatchLevel(logging.LevelDebug)
} }
routingConf := routing.New(netLinker, routingLogger) routingConf := routing.New(netLinker, routingLogger)
defaultRoutes, err := routingConf.DefaultRoutes() defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
if err != nil { if err != nil {
return err return err
} }
@@ -220,16 +194,19 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
firewallLogger := logger.New(log.SetComponent("firewall")) defaultIP, err := routingConf.DefaultIP()
if *allSettings.Firewall.Debug { // To remove in v4
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
}
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
defaultRoutes, localNetworks)
if err != nil { if err != nil {
return err return err
} }
firewallLogger := logger.NewChild(logging.Settings{
Prefix: "firewall: ",
})
if *allSettings.Firewall.Debug { // To remove in v4
firewallLogger.PatchLevel(logging.LevelDebug)
}
firewallConf := firewall.NewConfig(firewallLogger, cmder,
defaultInterface, defaultGateway, localNetworks, defaultIP)
if *allSettings.Firewall.Enabled { if *allSettings.Firewall.Enabled {
err = firewallConf.SetEnabled(ctx, true) err = firewallConf.SetEnabled(ctx, true)
if err != nil { if err != nil {
@@ -238,26 +215,23 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
} }
// TODO run this in a loop or in openvpn to reload from file without restarting // TODO run this in a loop or in openvpn to reload from file without restarting
storageLogger := logger.New(log.SetComponent("storage")) storageLogger := logger.NewChild(logging.Settings{Prefix: "storage: "})
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath) storage, err := storage.New(storageLogger, constants.ServersData)
if err != nil { if err != nil {
return err return err
} }
ipv6Supported, err := netLinker.IsIPv6Supported() allServers := storage.GetServers()
if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err)
}
err = allSettings.Validate(storage, ipv6Supported, logger) err = allSettings.Validate(allServers)
if err != nil { if err != nil {
return err return err
} }
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof")) allSettings.Pprof.HTTPServer.Logger = logger
pprofServer, err := pprof.New(allSettings.Pprof) pprofServer, err := pprof.New(allSettings.Pprof)
if err != nil { if err != nil {
return fmt.Errorf("creating Pprof server: %w", err) return fmt.Errorf("cannot create Pprof server: %w", err)
} }
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID) puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
@@ -267,14 +241,21 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
// Create configurators // Create configurators
alpineConf := alpine.New() alpineConf := alpine.New()
ovpnConf := openvpn.New( ovpnConf := openvpn.New(
logger.New(log.SetComponent("openvpn configurator")), logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}),
cmder, puid, pgid) 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{ err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version}, {name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25}, {name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26}, {name: "Unbound", getVersion: dnsConf.Version},
{name: "IPtables", getVersion: firewallConf.Version}, {name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) {
return firewall.Version(ctx, cmder)
}},
}) })
if err != nil { if err != nil {
return err return err
@@ -282,40 +263,40 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.Info(allSettings.String()) logger.Info(allSettings.String())
for _, warning := range allSettings.Warnings() { if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
logger.Warn(warning)
}
const permission = fs.FileMode(0o644)
err = os.MkdirAll("/tmp/gluetun", permission)
if err != nil {
return err return err
} }
err = os.MkdirAll("/gluetun", permission) if err := os.MkdirAll("/gluetun", 0644); err != nil {
if err != nil {
return err return err
} }
const defaultUsername = "nonrootuser" const defaultUsername = "nonrootuser"
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid) nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
if err != nil { if err != nil {
return fmt.Errorf("creating user: %w", err) return fmt.Errorf("cannot create user: %w", err)
} }
if nonRootUsername != defaultUsername { if nonRootUsername != defaultUsername {
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid)) 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 allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
return err
}
if err := routingConf.Setup(); err != nil { if err := routingConf.Setup(); err != nil {
if strings.Contains(err.Error(), "operation not permitted") { if strings.Contains(err.Error(), "operation not permitted") {
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?") logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
} }
return fmt.Errorf("setting up routing: %w", err) return fmt.Errorf("cannot setup routing: %w", err)
} }
defer func() { defer func() {
routingLogger.Info("routing cleanup...") logger.Info("routing cleanup...")
if err := routingConf.TearDown(); err != nil { if err := routingConf.TearDown(); err != nil {
routingLogger.Error("cannot teardown routing: " + err.Error()) logger.Error("cannot teardown routing: " + err.Error())
} }
}() }()
@@ -326,30 +307,19 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
err = routingConf.AddLocalRules(localNetworks)
if err != nil {
return fmt.Errorf("adding local rules: %w", err)
}
const tunDevice = "/dev/net/tun" const tunDevice = "/dev/net/tun"
err = tun.Check(tunDevice) if err := tun.Check(tunDevice); err != nil {
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
}
logger.Info(err.Error() + "; creating it...") logger.Info(err.Error() + "; creating it...")
err = tun.Create(tunDevice) err = tun.Create(tunDevice)
if err != nil { if err != nil {
return fmt.Errorf("creating tun device: %w", err) return err
} }
} }
for _, port := range allSettings.Firewall.InputPorts { for _, port := range allSettings.Firewall.InputPorts {
for _, defaultRoute := range defaultRoutes { err = firewallConf.SetAllowedPort(ctx, port, defaultInterface)
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface) if err != nil {
if err != nil { return err
return err
}
} }
} // TODO move inside firewall? } // TODO move inside firewall?
@@ -364,85 +334,64 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
} }
defaultGroupOptions := []group.Option{ defaultGroupOptions := []group.Option{
group.OptionTimeout(defaultShutdownTimeout), group.OptionTimeout(defaultShutdownTimeout),
group.OptionOnSuccess(defaultShutdownOnSuccess), group.OptionOnSuccess(defaultShutdownOnSuccess)}
}
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...) controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...) tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...) otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
if *allSettings.Pprof.Enabled { pprofReady := make(chan struct{})
// TODO run in run loop so this can be patched at runtime pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
pprofReady := make(chan struct{}) go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server") otherGroupHandler.Add(pprofHandler)
go pprofServer.Run(pprofCtx, pprofReady, pprofDone) <-pprofReady
otherGroupHandler.Add(pprofHandler)
<-pprofReady
}
portForwardLogger := logger.New(log.SetComponent("port forwarding")) portForwardLogger := logger.NewChild(logging.Settings{Prefix: "port forwarding: "})
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding, portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid) httpClient, firewallConf, portForwardLogger)
portForwardRunError, err := portForwardLooper.Start(ctx) portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
if err != nil { "port forwarding", goroutine.OptionTimeout(time.Second))
return fmt.Errorf("starting port forwarding loop: %w", err) go portForwardLooper.Run(portForwardCtx, portForwardDone)
}
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.NewChild(logging.Settings{Prefix: "dns over tls: "})
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
unboundLogger)
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler( dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
"dns", goroutine.OptionTimeout(defaultShutdownTimeout)) "unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker // wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
go dnsLooper.Run(dnsCtx, dnsDone) go unboundLooper.Run(dnsCtx, dnsDone)
otherGroupHandler.Add(dnsHandler) otherGroupHandler.Add(dnsHandler)
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler( dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout)) "dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone) go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
controlGroupHandler.Add(dnsTickerHandler) controlGroupHandler.Add(dnsTickerHandler)
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient, publicIPLooper := publicip.NewLoop(httpClient,
logger.New(log.SetComponent("ip getter"))) logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
if err != nil { allSettings.PublicIP, puid, pgid)
return fmt.Errorf("creating public ip loop: %w", err) pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
} "public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
publicIPRunError, err := publicIPLooper.Start(ctx) go publicIPLooper.Run(pubIPCtx, pubIPDone)
if err != nil { otherGroupHandler.Add(pubIPHandler)
return fmt.Errorf("starting public ip loop: %w", err)
}
healthLogger := logger.New(log.SetComponent("healthcheck")) pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger) "public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler( go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout)) tickersGroupHandler.Add(pubIPTickerHandler)
go healthcheckServer.Run(healthServerCtx, healthServerDone)
healthChecker := healthcheck.NewChecker(healthLogger)
updaterLogger := logger.New(log.SetComponent("updater")) vpnLogger := logger.NewChild(logging.Settings{Prefix: "vpn: "})
vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
unzipper := unzip.New(httpClient) allServers, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress) cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
openvpnFileExtractor, allSettings.Updater)
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,
buildInfo, *allSettings.Version.Enabled) buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler( vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second)) "vpn", goroutine.OptionTimeout(time.Second))
go vpnLooper.Run(vpnCtx, vpnDone) go vpnLooper.Run(vpnCtx, vpnDone)
updaterLooper := updater.NewLoop(allSettings.Updater, updaterLooper := updater.NewLooper(allSettings.Updater,
providers, storage, httpClient, updaterLogger) allServers, storage, vpnLooper.SetServers, httpClient,
logger.NewChild(logging.Settings{Prefix: "updater: "}))
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler( updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
"updater", goroutine.OptionTimeout(defaultShutdownTimeout)) "updater", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker // wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
@@ -455,64 +404,48 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
controlGroupHandler.Add(updaterTickerHandler) controlGroupHandler.Add(updaterTickerHandler)
httpProxyLooper := httpproxy.NewLoop( httpProxyLooper := httpproxy.NewLoop(
logger.New(log.SetComponent("http proxy")), logger.NewChild(logging.Settings{Prefix: "http proxy: "}),
allSettings.HTTPProxy) allSettings.HTTPProxy)
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler( httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout)) "http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone) go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
otherGroupHandler.Add(httpProxyHandler) otherGroupHandler.Add(httpProxyHandler)
shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks, shadowsocksLooper := shadowsocks.NewLooper(allSettings.Shadowsocks,
logger.New(log.SetComponent("shadowsocks"))) logger.NewChild(logging.Settings{Prefix: "shadowsocks: "}))
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler( shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout)) "shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone) go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler) otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := *allSettings.ControlServer.Address
controlServerLogging := *allSettings.ControlServer.Log
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler( httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout)) "http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer, httpServer := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")), logger.NewChild(logging.Settings{Prefix: "http server: "}),
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper, buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper)
storage, ipv6Supported) go httpServer.Run(httpServerCtx, httpServerDone)
if err != nil {
return fmt.Errorf("setting up control server: %w", err)
}
httpServerReady := make(chan struct{})
go httpServer.Run(httpServerCtx, httpServerReady, httpServerDone)
<-httpServerReady
controlGroupHandler.Add(httpServerHandler) controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "})
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
orderHandler := goshutdown.NewOrderHandler("gluetun", orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout), order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess), order.OptionOnSuccess(defaultShutdownOnSuccess),
order.OptionOnFailure(defaultShutdownOnFailure)) order.OptionOnFailure(defaultShutdownOnFailure))
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler, orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
vpnHandler, otherGroupHandler) vpnHandler, portForwardHandler, otherGroupHandler)
// Start VPN for the first time in a blocking call // Start VPN for the first time in a blocking call
// until the VPN is launched // until the VPN is launched
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable _, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
select { <-ctx.Done()
case <-ctx.Done():
stoppers := []interface {
String() string
Stop() error
}{
portForwardLooper, publicIPLooper,
}
for _, stopper := range stoppers {
err := stopper.Stop()
if err != nil {
logger.Error(fmt.Sprintf("stopping %s: %s", stopper, err))
}
}
case err := <-portForwardRunError:
logger.Errorf("port forwarding loop crashed: %s", err)
case err := <-publicIPRunError:
logger.Errorf("public IP loop crashed: %s", err)
}
return orderHandler.Shutdown(context.Background()) return orderHandler.Shutdown(context.Background())
} }
@@ -527,8 +460,7 @@ type infoer interface {
} }
func printVersions(ctx context.Context, logger infoer, func printVersions(ctx context.Context, logger infoer,
elements []printVersionElement, elements []printVersionElement) (err error) {
) (err error) {
const timeout = 5 * time.Second const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout) ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() defer cancel()
@@ -536,100 +468,10 @@ func printVersions(ctx context.Context, logger infoer,
for _, element := range elements { for _, element := range elements {
version, err := element.getVersion(ctx) version, err := element.getVersion(ctx)
if err != nil { if err != nil {
return fmt.Errorf("getting %s version: %w", element.name, err) return err
} }
logger.Info(element.name + " version: " + version) logger.Info(element.name + " version: " + version)
} }
return nil return nil
} }
type netLinker interface {
Addresser
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
IsIPv6Supported() (ok bool, err error)
PatchLoggerLevel(level log.Level)
}
type Addresser interface {
AddrList(link netlink.Link, family int) (
addresses []netlink.Addr, err error)
AddrReplace(link netlink.Link, addr netlink.Addr) error
}
type Router interface {
RouteList(family int) (routes []netlink.Route, err error)
RouteAdd(route netlink.Route) error
RouteDel(route netlink.Route) error
RouteReplace(route netlink.Route) error
}
type Ruler interface {
RuleList(family int) (rules []netlink.Rule, err error)
RuleAdd(rule netlink.Rule) error
RuleDel(rule netlink.Rule) error
}
type Linker interface {
LinkList() (links []netlink.Link, err error)
LinkByName(name string) (link netlink.Link, err error)
LinkByIndex(index int) (link netlink.Link, err error)
LinkAdd(link netlink.Link) (linkIndex int, err error)
LinkDel(link netlink.Link) (err error)
LinkSetUp(link netlink.Link) (linkIndex int, err error)
LinkSetDown(link netlink.Link) (err error)
}
type clier interface {
ClientKey(args []string) error
FormatServers(args []string) error
OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
GenKey(args []string) error
}
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)
}
const gluetunLogo = ` @@@
@@@@
@@@@@@
@@@@.@@ @@@@@@@@@@
@@@@.@@@ @@@@@@@@==@@@@
@@@.@..@@ @@@@@@@=@..==@@@@
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
@@@.--@..@@@@.-@@@@@@@==============@@@@
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
@@@@.@--@@@@@@@@@@================@@@@@@@
@@@..--@@*@@@@@@================@@@@+*@@
@@@.---@@.@@@@=================@@@@--@@
@@@-.---@@@@@@================@@@@*--@@@
@@@.:-#@@@@@@===============*@@@@.---@@
@@@.-------.@@@============@@@@@@.--@@@
@@@..--------:@@@=========@@@@@@@@.--@@@
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
@@@@@@=============@@@@@ @@@...------------.*@@@
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
@@@@@@@@ @@@@ @@@@
`

86
go.mod
View File

@@ -1,69 +1,43 @@
module github.com/qdm12/gluetun module github.com/qdm12/gluetun
go 1.25.0 go 1.17
require ( require (
github.com/ProtonMail/go-srp v0.0.7 github.com/breml/rootcerts v0.2.2
github.com/breml/rootcerts v0.3.3 github.com/fatih/color v1.13.0
github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.18.1 github.com/qdm12/dns v1.11.0
github.com/klauspost/pgzip v1.2.6 github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88
github.com/qdm12/gosettings v0.4.4
github.com/qdm12/goshutdown v0.3.0 github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.2.0 github.com/qdm12/gosplash v0.1.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/govalid v0.1.0
github.com/qdm12/ss-server v0.6.0 github.com/qdm12/ss-server v0.4.0
github.com/stretchr/testify v1.11.1 github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
github.com/ulikunitz/xz v0.5.15 github.com/stretchr/testify v1.7.0
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
golang.org/x/net v0.47.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
golang.org/x/sys v0.38.0 inet.af/netaddr v0.0.0-20210718074554-06ca8145d722
golang.org/x/text v0.31.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
) )
require ( 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/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.5.5 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.0.0 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.4.0 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/miekg/dns v1.1.40 // indirect
github.com/miekg/dns v1.1.62 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_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/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
golang.org/x/crypto v0.44.0 // indirect go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
golang.org/x/mod v0.29.0 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
golang.org/x/tools v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // 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
) )

367
go.sum
View File

@@ -1,192 +1,251 @@
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= github.com/breml/rootcerts v0.2.2 h1:hkHEpbTdYaNvDoYeq+mwRvCeg/YTTl23DjQ1Tnj71Zs=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= github.com/breml/rootcerts v0.2.2/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 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.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
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 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 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.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
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 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
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.20251123213823-54e987293e88 h1:GJ5FALvJ3UmHjVaNYebrfV5zF5You4dq8HfRWZy2loM=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88/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/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM= github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM= github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY= github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo= github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw=
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI= github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw= 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/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8=
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI= github.com/qdm12/govalid v0.1.0/go.mod h1:CyS/OEQdOvunBgrtIsW93fjd4jBkwZPBjGSpxq3NwA4=
github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE= github.com/qdm12/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU=
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78= github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY=
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 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= 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/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.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 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
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 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/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-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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 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.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/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/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-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.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-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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/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-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-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-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.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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19 h1:ab2jcw2W91Rz07eHAb8Lic7sFQKO0NhBftjv6m/gL/0=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
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 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-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI= inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 h1:Qws2rZnQudC58cIagVucPQDLmMi3kAXgxscsgD0v6DU=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=

View File

@@ -1,9 +1,17 @@
// Package alpine defines a configurator to interact with the Alpine operating system.
package alpine package alpine
import ( import (
"os/user" "os/user"
) )
var _ Alpiner = (*Alpine)(nil)
type Alpiner interface {
UserCreater
VersionGetter
}
type Alpine struct { type Alpine struct {
alpineReleasePath string alpineReleasePath string
passwdPath string passwdPath string

View File

@@ -3,13 +3,18 @@ package alpine
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/fs"
"os" "os"
"os/user" "os/user"
"strconv" "strconv"
) )
var ErrUserAlreadyExists = errors.New("user already exists") var (
ErrUserAlreadyExists = errors.New("user already exists")
)
type UserCreater interface {
CreateUser(username string, uid int) (createdUsername string, err error)
}
// CreateUser creates a user in Alpine with the given UID. // CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) { func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
@@ -38,8 +43,7 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
ErrUserAlreadyExists, username, u.Uid, uid) ErrUserAlreadyExists, username, u.Uid, uid)
} }
const permission = fs.FileMode(0o644) file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -7,7 +7,11 @@ import (
"strings" "strings"
) )
func (a *Alpine) Version(context.Context) (version string, err error) { type VersionGetter interface {
Version(ctx context.Context) (version string, err error)
}
func (a *Alpine) Version(ctx context.Context) (version string, err error) {
file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0) file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -2,6 +2,6 @@ package cli
import "context" import "context"
func (c *CLI) CI(context.Context) error { func (c *CLI) CI(context context.Context) error {
return nil return nil
} }

View File

@@ -1,5 +1,16 @@
// Package cli defines an interface CLI to run command line operations.
package cli package cli
var _ CLIer = (*CLI)(nil)
type CLIer interface {
ClientKeyFormatter
HealthChecker
OpenvpnConfigMaker
Updater
ServersFormatter
}
type CLI struct { type CLI struct {
repoServersPath string repoServersPath string
} }

View File

@@ -6,12 +6,17 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
) )
type ClientKeyFormatter interface {
ClientKey(args []string) error
}
func (c *CLI) ClientKey(args []string) error { func (c *CLI) ClientKey(args []string) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError) flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
const openVPNClientKeyPath = "/gluetun/client.key" // TODO deduplicate? filepath := flagSet.String("path", files.OpenVPNClientKeyPath, "file path to the client.key file")
filepath := flagSet.String("path", openVPNClientKeyPath, "file path to the client.key file")
if err := flagSet.Parse(args); err != nil { if err := flagSet.Parse(args); err != nil {
return err return err
} }
@@ -27,6 +32,9 @@ func (c *CLI) ClientKey(args []string) error {
if err := file.Close(); err != nil { if err := file.Close(); err != nil {
return err return err
} }
if err != nil {
return err
}
s := string(data) s := string(data)
s = strings.ReplaceAll(s, "\n", "") s = strings.ReplaceAll(s, "\n", "")
s = strings.ReplaceAll(s, "\r", "") s = strings.ReplaceAll(s, "\r", "")

View File

@@ -4,109 +4,126 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
type ServersFormatter interface {
FormatServers(args []string) error
}
var ( var (
ErrProviderUnspecified = errors.New("VPN provider to format was not specified") ErrFormatNotRecognized = errors.New("format is not recognized")
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified") ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
) )
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
provider string, titleCaser cases.Caser,
) {
boolPtr, ok := providerToFormat[provider]
if !ok {
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
}
flagSet.BoolVar(boolPtr, provider, false, "Format "+titleCaser.String(provider)+" servers")
}
func (c *CLI) FormatServers(args []string) error { func (c *CLI) FormatServers(args []string) error {
var format, output string var format, output string
allProviders := providers.All() var cyberghost, expressvpn, fastestvpn, hideMyAss, ipvanish, ivpn, mullvad,
allProviderFlags := make([]string, len(allProviders)) nordvpn, perfectPrivacy, pia, privado, privatevpn, protonvpn, purevpn, surfshark,
for i, provider := range allProviders { torguard, vpnUnlimited, vyprvpn, wevpn, windscribe bool
allProviderFlags[i] = strings.ReplaceAll(provider, " ", "-") flagSet := flag.NewFlagSet("markdown", flag.ExitOnError)
} flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
providersToFormat := make(map[string]*bool, len(allProviders))
for _, provider := range allProviderFlags {
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(&output, "output", "/dev/stdout", "Output file to write the formatted data to") flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
titleCaser := cases.Title(language.English) flagSet.BoolVar(&cyberghost, "cyberghost", false, "Format Cyberghost servers")
for _, provider := range allProviderFlags { flagSet.BoolVar(&expressvpn, "expressvpn", false, "Format ExpressVPN servers")
addProviderFlag(flagSet, providersToFormat, provider, titleCaser) flagSet.BoolVar(&fastestvpn, "fastestvpn", false, "Format FastestVPN servers")
} flagSet.BoolVar(&hideMyAss, "hidemyass", false, "Format HideMyAss servers")
flagSet.BoolVar(&ipvanish, "ipvanish", false, "Format IpVanish servers")
flagSet.BoolVar(&ivpn, "ivpn", false, "Format IVPN servers")
flagSet.BoolVar(&mullvad, "mullvad", false, "Format Mullvad servers")
flagSet.BoolVar(&nordvpn, "nordvpn", false, "Format Nordvpn servers")
flagSet.BoolVar(&perfectPrivacy, "perfectprivacy", false, "Format Perfect Privacy servers")
flagSet.BoolVar(&pia, "pia", false, "Format Private Internet Access servers")
flagSet.BoolVar(&privado, "privado", false, "Format Privado servers")
flagSet.BoolVar(&privatevpn, "privatevpn", false, "Format Private VPN servers")
flagSet.BoolVar(&protonvpn, "protonvpn", false, "Format Protonvpn servers")
flagSet.BoolVar(&purevpn, "purevpn", false, "Format Purevpn servers")
flagSet.BoolVar(&surfshark, "surfshark", false, "Format Surfshark servers")
flagSet.BoolVar(&torguard, "torguard", false, "Format Torguard servers")
flagSet.BoolVar(&vpnUnlimited, "vpnunlimited", false, "Format VPN Unlimited servers")
flagSet.BoolVar(&vyprvpn, "vyprvpn", false, "Format Vyprvpn servers")
flagSet.BoolVar(&wevpn, "wevpn", false, "Format WeVPN servers")
flagSet.BoolVar(&windscribe, "windscribe", false, "Format Windscribe servers")
if err := flagSet.Parse(args); err != nil { if err := flagSet.Parse(args); err != nil {
return err 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
for provider, formatPtr := range providersToFormat {
if *formatPtr {
providers = append(providers, provider)
}
}
switch len(providers) {
case 0:
return fmt.Errorf("%w", ErrProviderUnspecified)
case 1:
default:
return fmt.Errorf("%w: %d specified: %s",
ErrMultipleProvidersToFormat, len(providers),
strings.Join(providers, ", "))
}
var providerToFormat string
for _, providerToFormat = range allProviders {
if strings.ReplaceAll(providerToFormat, " ", "-") == providers[0] {
break
}
} }
logger := newNoopLogger() logger := newNoopLogger()
storage, err := storage.New(logger, constants.ServersData) storage, err := storage.New(logger, constants.ServersData)
if err != nil { if err != nil {
return fmt.Errorf("creating servers storage: %w", err) return fmt.Errorf("cannot create servers storage: %w", err)
} }
currentServers := storage.GetServers()
formatted, err := storage.Format(providerToFormat, format) var formatted string
if err != nil { switch {
return fmt.Errorf("formatting servers: %w", err) case cyberghost:
formatted = currentServers.Cyberghost.ToMarkdown()
case expressvpn:
formatted = currentServers.Expressvpn.ToMarkdown()
case fastestvpn:
formatted = currentServers.Fastestvpn.ToMarkdown()
case hideMyAss:
formatted = currentServers.HideMyAss.ToMarkdown()
case ipvanish:
formatted = currentServers.Ipvanish.ToMarkdown()
case ivpn:
formatted = currentServers.Ivpn.ToMarkdown()
case mullvad:
formatted = currentServers.Mullvad.ToMarkdown()
case nordvpn:
formatted = currentServers.Nordvpn.ToMarkdown()
case perfectPrivacy:
formatted = currentServers.Perfectprivacy.ToMarkdown()
case pia:
formatted = currentServers.Pia.ToMarkdown()
case privado:
formatted = currentServers.Privado.ToMarkdown()
case privatevpn:
formatted = currentServers.Privatevpn.ToMarkdown()
case protonvpn:
formatted = currentServers.Protonvpn.ToMarkdown()
case purevpn:
formatted = currentServers.Purevpn.ToMarkdown()
case surfshark:
formatted = currentServers.Surfshark.ToMarkdown()
case torguard:
formatted = currentServers.Torguard.ToMarkdown()
case vpnUnlimited:
formatted = currentServers.VPNUnlimited.ToMarkdown()
case vyprvpn:
formatted = currentServers.Vyprvpn.ToMarkdown()
case wevpn:
formatted = currentServers.Wevpn.ToMarkdown()
case windscribe:
formatted = currentServers.Windscribe.ToMarkdown()
default:
return ErrProviderUnspecified
} }
output = filepath.Clean(output) output = filepath.Clean(output)
const permission = fs.FileMode(0o644) file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
if err != nil { if err != nil {
return fmt.Errorf("opening output file: %w", err) return fmt.Errorf("cannot open output file: %w", err)
} }
_, err = fmt.Fprint(file, formatted) _, err = fmt.Fprint(file, formatted)
if err != nil { if err != nil {
_ = file.Close() _ = file.Close()
return fmt.Errorf("writing to output file: %w", err) return fmt.Errorf("cannot write to output file: %w", err)
} }
err = file.Close() err = file.Close()
if err != nil { if err != nil {
return fmt.Errorf("closing output file: %w", err) return fmt.Errorf("cannot close output file: %w", err)
} }
return nil return nil

View File

@@ -1,66 +0,0 @@
package cli
import (
"crypto/rand"
"flag"
"fmt"
)
func (c *CLI) GenKey(args []string) (err error) {
flagSet := flag.NewFlagSet("genkey", flag.ExitOnError)
err = flagSet.Parse(args)
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
const keyLength = 128 / 8
keyBytes := make([]byte, keyLength)
_, _ = rand.Read(keyBytes)
key := base58Encode(keyBytes)
fmt.Println(key)
return nil
}
func base58Encode(data []byte) string {
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
const radix = 58
zcount := 0
for zcount < len(data) && data[zcount] == 0 {
zcount++
}
// integer simplification of ceil(log(256)/log(58))
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:mnd
size := zcount + ceilLog256Div58
output := make([]byte, size)
high := size - 1
for _, b := range data {
i := size - 1
for carry := uint32(b); i > high || carry != 0; i-- {
carry += 256 * uint32(output[i]) //nolint:mnd
output[i] = byte(carry % radix)
carry /= radix
}
high = i
}
// Determine the additional "zero-gap" in the output buffer
additionalZeroGapEnd := zcount
for additionalZeroGapEnd < size && output[additionalZeroGapEnd] == 0 {
additionalZeroGapEnd++
}
val := output[additionalZeroGapEnd-zcount:]
size = len(val)
for i := range val {
output[i] = alphabet[val[i]]
}
return string(output[:size])
}

View File

@@ -6,21 +6,21 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/healthcheck" "github.com/qdm12/gluetun/internal/healthcheck"
"github.com/qdm12/gosettings/reader"
) )
func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) { type HealthChecker interface {
HealthCheck(ctx context.Context, source sources.Source, warner Warner) error
}
func (c *CLI) HealthCheck(ctx context.Context, source sources.Source, warner Warner) error {
// Extract the health server port from the configuration. // Extract the health server port from the configuration.
var config settings.Health config, err := source.ReadHealth()
err = config.Read(reader)
if err != nil { if err != nil {
return err return err
} }
config.SetDefaults()
err = config.Validate() err = config.Validate()
if err != nil { if err != nil {
return err return err

View File

@@ -1,10 +1,16 @@
package cli package cli
import "github.com/qdm12/golibs/logging"
type noopLogger struct{} type noopLogger struct{}
func newNoopLogger() *noopLogger { func newNoopLogger() *noopLogger {
return new(noopLogger) return new(noopLogger)
} }
func (l *noopLogger) Info(string) {} func (l *noopLogger) Debug(s string) {}
func (l *noopLogger) Warn(string) {} func (l *noopLogger) Info(s string) {}
func (l *noopLogger) Warn(s string) {}
func (l *noopLogger) Error(s string) {}
func (l *noopLogger) PatchLevel(level logging.Level) {}
func (l *noopLogger) PatchPrefix(prefix string) {}

View File

@@ -1,91 +1,50 @@
package cli package cli
import ( import (
"context"
"fmt" "fmt"
"net/http"
"net/netip"
"strings" "strings"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gosettings/reader"
) )
type OpenvpnConfigMaker interface {
OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error
}
type OpenvpnConfigLogger interface { type OpenvpnConfigLogger interface {
Info(s string) Info(s string)
Warn(s string) Warn(s string)
} }
type Unzipper interface { func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error {
FetchAndExtract(ctx context.Context, url string) (
contents map[string][]byte, err error)
}
type ParallelResolver interface {
Resolve(ctx context.Context, settings resolver.ParallelSettings) (
hostToIPs map[string][]netip.Addr, warnings []string, err error)
}
type IPFetcher interface {
String() string
CanFetchAnyIP() bool
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
}
type IPv6Checker interface {
IsIPv6Supported() (supported bool, err error)
}
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
ipv6Checker IPv6Checker,
) error {
storage, err := storage.New(logger, constants.ServersData) storage, err := storage.New(logger, constants.ServersData)
if err != nil { if err != nil {
return err return err
} }
allServers := storage.GetServers()
var allSettings settings.Settings allSettings, err := source.Read()
err = allSettings.Read(reader, logger)
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 {
return fmt.Errorf("validating settings: %w", err)
}
// Unused by this CLI command
unzipper := (Unzipper)(nil)
client := (*http.Client)(nil)
warner := (Warner)(nil)
parallelResolver := (ParallelResolver)(nil)
ipFetcher := (IPFetcher)(nil)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, warner, client,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
if err != nil { if err != nil {
return err return err
} }
lines := providerConf.OpenVPNConfig(connection, if err = allSettings.Validate(allServers); err != nil {
allSettings.VPN.OpenVPN, ipv6Supported) return err
}
providerConf := provider.New(*allSettings.VPN.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection)
if err != nil {
return err
}
lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
if err != nil {
return err
}
fmt.Println(strings.Join(lines, "\n")) fmt.Println(strings.Join(lines, "\n"))
return nil return nil

View File

@@ -2,85 +2,79 @@ package cli
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"net"
"net/http" "net/http"
"slices" "os"
"strings" "strings"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater" "github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
) )
var ( var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified") ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrDNSAddress = errors.New("DNS address is not valid")
ErrNoProviderSpecified = errors.New("no provider was 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 Updater interface {
Update(ctx context.Context, args []string, logger UpdaterLogger) error
}
type UpdaterLogger interface { type UpdaterLogger interface {
Info(s string) Info(s string)
Warn(s string) Warn(s string)
Error(s string) Error(s string)
} }
func boolPtr(b bool) *bool { return &b }
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error { func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{} options := settings.Updater{CLI: boolPtr(true)}
var endUserMode, maintainerMode, updateAll bool var endUserMode, maintainerMode, updateAll bool
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string var dnsAddress, csvProviders string
flagSet := flag.NewFlagSet("update", flag.ExitOnError) flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)") flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false, flagSet.BoolVar(&maintainerMode, "maintainer", false,
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)") "Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use") flagSet.StringVar(&dnsAddress, "dns", "8.8.8.8", "DNS resolver address to use")
const defaultMinRatio = 0.8
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
"Minimum ratio of servers to find for the update to succeed")
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers") 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(&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", "",
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
flagSet.StringVar(&protonEmail, "proton-email", "", "Email 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 { if err := flagSet.Parse(args); err != nil {
return err return err
} }
if !endUserMode && !maintainerMode { if !endUserMode && !maintainerMode {
return fmt.Errorf("%w", ErrModeUnspecified) return ErrModeUnspecified
}
options.DNSAddress = net.ParseIP(dnsAddress)
if options.DNSAddress == nil {
return fmt.Errorf("%w: %s", ErrDNSAddress, dnsAddress)
} }
if updateAll { if updateAll {
options.Providers = providers.All() for _, provider := range constants.AllProviders() {
if provider == constants.Custom {
continue
}
options.Providers = append(options.Providers, provider)
}
} else { } else {
if csvProviders == "" { if csvProviders == "" {
return fmt.Errorf("%w", ErrNoProviderSpecified) return ErrNoProviderSpecified
} }
options.Providers = strings.Split(csvProviders, ",") options.Providers = strings.Split(csvProviders, ",")
} }
if slices.Contains(options.Providers, providers.Protonvpn) {
if protonEmail == "" && protonUsername != "" {
protonEmail = protonUsername + "@protonmail.com"
logger.Warn("use -proton-email instead of -proton-username in the future. " +
"This assumes the email is " + protonEmail + " and may not work.")
}
options.ProtonEmail = &protonEmail
options.ProtonPassword = &protonPassword
}
options.SetDefaults(options.Providers[0]) options.SetDefaults(options.Providers[0])
err := options.Validate() err := options.Validate()
@@ -88,47 +82,48 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return fmt.Errorf("options validation failed: %w", err) return fmt.Errorf("options validation failed: %w", err)
} }
serversDataPath := constants.ServersData
if maintainerMode {
serversDataPath = ""
}
storage, err := storage.New(logger, serversDataPath)
if err != nil {
return fmt.Errorf("creating servers storage: %w", err)
}
const clientTimeout = 10 * time.Second const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout} httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(options.DNSAddress) storage, err := storage.New(logger, constants.ServersData)
nameTokenPairs := []api.NameToken{
{Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)},
{Name: string(api.IfConfigCo)},
}
fetchers, err := api.New(nameTokenPairs, httpClient)
if err != nil { if err != nil {
return fmt.Errorf("creating public IP fetchers: %w", err) return fmt.Errorf("cannot create servers storage: %w", err)
} }
ipFetcher := api.NewResilient(fetchers, logger) currentServers := storage.GetServers()
openvpnFileExtractor := extract.New() updater := updater.New(options, httpClient, currentServers, logger)
allServers, err := updater.UpdateServers(ctx)
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
updater := updater.New(httpClient, storage, providers, logger)
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
if err != nil { if err != nil {
return fmt.Errorf("updating server information: %w", err) return fmt.Errorf("cannot update server information: %w", err)
}
if endUserMode {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("cannot write updated information to file: %w", err)
}
} }
if maintainerMode { if maintainerMode {
err := storage.FlushToFile(c.repoServersPath) if err := writeToEmbeddedJSON(c.repoServersPath, allServers); err != nil {
if err != nil { return fmt.Errorf("cannot write updated information to file: %w", err)
return fmt.Errorf("writing servers data to embedded JSON file: %w", err)
} }
} }
return nil return nil
} }
func writeToEmbeddedJSON(repoServersPath string,
allServers models.AllServers) error {
const perms = 0600
f, err := os.OpenFile(repoServersPath,
os.O_TRUNC|os.O_WRONLY|os.O_CREATE, perms)
if err != nil {
return err
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
return encoder.Encode(allServers)
}

View File

@@ -1,8 +0,0 @@
package command
// Cmder handles running subprograms synchronously and asynchronously.
type Cmder struct{}
func New() *Cmder {
return &Cmder{}
}

View File

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

View File

@@ -1,3 +0,0 @@
package command
//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go

View File

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

View File

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

View File

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

View File

@@ -1,150 +0,0 @@
package command
import (
"bytes"
"errors"
"fmt"
"strings"
"unicode/utf8"
)
var (
ErrCommandEmpty = errors.New("command is empty")
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
)
// Split splits a command string into a slice of arguments.
// This is especially important for commands such as:
// /bin/sh -c "echo hello"
// which should be split into: ["/bin/sh", "-c", "echo hello"]
// It supports backslash-escapes, single-quotes and double-quotes.
// It does not support:
// - the $" quoting style.
// - expansion (brace, shell or pathname).
func Split(command string) (words []string, err error) {
if command == "" {
return nil, fmt.Errorf("%w", ErrCommandEmpty)
}
const bufferSize = 1024
buffer := bytes.NewBuffer(make([]byte, bufferSize))
startIndex := 0
for startIndex < len(command) {
// skip any split characters at the start
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
switch {
case strings.ContainsRune(" \n\t", character):
startIndex += runeSize
case character == '\\':
// Look ahead to eventually skip an escaped newline
if command[startIndex+runeSize:] == "" {
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
}
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
if character == '\n' {
startIndex += runeSize + runeSize // backslash and newline
}
default:
var word string
buffer.Reset()
word, startIndex, err = splitWord(command, startIndex, buffer)
if err != nil {
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
}
words = append(words, word)
}
}
return words, nil
}
// WARNING: buffer must be cleared before calling this function.
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
cursor := startIndex
for cursor < len(input) {
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
cursor += runeLength
if character == '"' ||
character == '\'' ||
character == '\\' ||
character == ' ' ||
character == '\n' ||
character == '\t' {
buffer.WriteString(input[startIndex : cursor-runeLength])
}
switch {
case strings.ContainsRune(" \n\t", character): // spacing character
return buffer.String(), cursor, nil
case character == '"':
return handleDoubleQuoted(input, cursor, buffer)
case character == '\'':
return handleSingleQuoted(input, cursor, buffer)
case character == '\\':
return handleEscaped(input, cursor, buffer)
}
}
buffer.WriteString(input[startIndex:])
return buffer.String(), len(input), nil
}
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
cursor := startIndex
for cursor < len(input) {
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
cursor += nextRuneLength
switch nextCharacter {
case '"': // end of the double quoted string
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
return splitWord(input, cursor, buffer)
case '\\': // escaped character
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
cursor += escapedRuneLength
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
break
}
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
if escapedCharacter != '\n' {
// skip backslash entirely for the newline character
buffer.WriteRune(escapedCharacter)
}
startIndex = cursor
}
}
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
}
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
if closingQuoteIndex == -1 {
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
}
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
const singleQuoteRuneLength = 1
startIndex += closingQuoteIndex + singleQuoteRuneLength
return splitWord(input, startIndex, buffer)
}
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
if input[startIndex:] == "" {
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
}
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
if character != '\n' { // backslash-escaped newline is ignored
buffer.WriteString(input[startIndex : startIndex+runeLength])
}
startIndex += runeLength
return splitWord(input, startIndex, buffer)
}

View File

@@ -1,110 +0,0 @@
package command
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Split(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
command string
words []string
errWrapped error
errMessage string
}{
"empty": {
command: "",
errWrapped: ErrCommandEmpty,
errMessage: "command is empty",
},
"concrete_sh_command": {
command: `/bin/sh -c "echo 123"`,
words: []string{"/bin/sh", "-c", "echo 123"},
},
"single_word": {
command: "word1",
words: []string{"word1"},
},
"two_words_single_space": {
command: "word1 word2",
words: []string{"word1", "word2"},
},
"two_words_multiple_space": {
command: "word1 word2",
words: []string{"word1", "word2"},
},
"two_words_no_expansion": {
command: "word1* word2?",
words: []string{"word1*", "word2?"},
},
"escaped_single quote": {
command: "ain\\'t good",
words: []string{"ain't", "good"},
},
"escaped_single_quote_all_single_quoted": {
command: "'ain'\\''t good'",
words: []string{"ain't good"},
},
"empty_single_quoted": {
command: "word1 '' word2",
words: []string{"word1", "", "word2"},
},
"escaped_newline": {
command: "word1\\\nword2",
words: []string{"word1word2"},
},
"quoted_newline": {
command: "text \"with\na\" quoted newline",
words: []string{"text", "with\na", "quoted", "newline"},
},
"quoted_escaped_newline": {
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
words: []string{"word1\\d\\\" word2word3 word4"},
},
"escaped_separated_newline": {
command: "word1 \\\n word2",
words: []string{"word1", "word2"},
},
"double_quotes_no_spacing": {
command: "word1\"word2\"word3",
words: []string{"word1word2word3"},
},
"unterminated_single_quote": {
command: "'abc'\\''def",
errWrapped: ErrSingleQuoteUnterminated,
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
},
"unterminated_double_quote": {
command: "\"abc'def",
errWrapped: ErrDoubleQuoteUnterminated,
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
},
"unterminated_escape": {
command: "abc\\",
errWrapped: ErrEscapeUnterminated,
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
},
"unterminated_escape_only": {
command: " \\",
errWrapped: ErrEscapeUnterminated,
errMessage: `unterminated backslash-escape: " \\"`,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
words, err := Split(testCase.command)
assert.Equal(t, testCase.words, words)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}

View File

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

View File

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

View File

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

View File

@@ -1,87 +1,37 @@
package settings package settings
import ( import (
"errors"
"fmt" "fmt"
"net/netip" "net"
"time"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// DNS contains settings to configure DNS. // DNS contains settings to configure DNS.
type DNS struct { type DNS struct {
// ServerEnabled is true if the server should be running
// and used. It defaults to true, and cannot be nil
// in the internal state.
ServerEnabled *bool
// UpstreamType can be dot or plain, and defaults to dot.
UpstreamType string `json:"upstream_type"`
// UpdatePeriod is the period to update DNS block lists.
// It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in
// the internal state.
UpdatePeriod *time.Duration
// Providers is a list of DNS providers
Providers []string `json:"providers"`
// Caching is true if the server should cache
// DNS responses.
Caching *bool `json:"caching"`
// IPv6 is true if the server should connect over IPv6.
IPv6 *bool `json:"ipv6"`
// Blacklist contains settings to configure the filter
// block lists.
Blacklist DNSBlacklist
// ServerAddress is the DNS server to use inside // ServerAddress is the DNS server to use inside
// the Go program and for the system. // the Go program and for the system.
// It defaults to '127.0.0.1' to be used with the // It defaults to '127.0.0.1' to be used with the
// local server. It cannot be the zero value in the internal // DoT server. It cannot be nil in the internal
// state. // state.
ServerAddress netip.Addr ServerAddress net.IP
// KeepNameserver is true if the existing DNS server // KeepNameserver is true if the Docker DNS server
// found in /etc/resolv.conf should be used // found in /etc/resolv.conf should be kept.
// Note setting this to true will likely DNS traffic // Note settings this to true will go around the
// outside the VPN tunnel since it would go through // DoT server blocking.
// the local DNS server of your Docker/Kubernetes
// configuration, which is likely not going through the tunnel.
// This will also disable the DNS forwarder server and the
// `ServerAddress` field will be ignored.
// It defaults to false and cannot be nil in the // It defaults to false and cannot be nil in the
// internal state. // internal state.
KeepNameserver *bool KeepNameserver *bool
// DOT contains settings to configure the DoT
// server.
DoT DoT
} }
var (
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
)
func (d DNS) validate() (err error) { func (d DNS) validate() (err error) {
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") { err = d.DoT.validate()
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
}
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("%w: %s must be bigger than %s",
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
}
providers := provider.NewProviders()
for _, providerName := range d.Providers {
_, err := providers.Get(providerName)
if err != nil {
return err
}
}
err = d.Blacklist.validate()
if err != nil { if err != nil {
return err return fmt.Errorf("failed validating DoT settings: %w", err)
} }
return nil return nil
@@ -89,64 +39,34 @@ func (d DNS) validate() (err error) {
func (d *DNS) Copy() (copied DNS) { func (d *DNS) Copy() (copied DNS) {
return DNS{ return DNS{
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled), ServerAddress: helpers.CopyIP(d.ServerAddress),
UpstreamType: d.UpstreamType, KeepNameserver: helpers.CopyBoolPtr(d.KeepNameserver),
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod), DoT: d.DoT.copy(),
Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
ServerAddress: d.ServerAddress,
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DNS) mergeWith(other DNS) {
d.ServerAddress = helpers.MergeWithIP(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = helpers.MergeWithBool(d.KeepNameserver, other.KeepNameserver)
d.DoT.mergeWith(other.DoT)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (d *DNS) overrideWith(other DNS) { func (d *DNS) overrideWith(other DNS) {
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled) d.ServerAddress = helpers.OverrideWithIP(d.ServerAddress, other.ServerAddress)
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType) d.KeepNameserver = helpers.OverrideWithBool(d.KeepNameserver, other.KeepNameserver)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod) d.DoT.overrideWith(other.DoT)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist)
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
} }
func (d *DNS) setDefaults() { func (d *DNS) setDefaults() {
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true) localhost := net.IPv4(127, 0, 0, 1) //nolint:gomnd
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot") d.ServerAddress = helpers.DefaultIP(d.ServerAddress, localhost)
const defaultUpdatePeriod = 24 * time.Hour d.KeepNameserver = helpers.DefaultBool(d.KeepNameserver, false)
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod) d.DoT.setDefaults()
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
provider.Cloudflare().Name,
})
d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults()
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
}
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
return d.ServerAddress
}
providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0])
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
return provider.Plain.IPv4[0].Addr()
} }
func (d DNS) String() string { func (d DNS) String() string {
@@ -155,77 +75,8 @@ func (d DNS) String() string {
func (d DNS) toLinesNode() (node *gotree.Node) { func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:") node = gotree.New("DNS settings:")
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
if *d.KeepNameserver {
return node
}
node.Appendf("DNS server address to use: %s", d.ServerAddress) node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.Appendf("Keep existing nameserver(s): %s", helpers.BoolPtrToYesNo(d.KeepNameserver))
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled)) node.AppendNode(d.DoT.toLinesNode())
if !*d.ServerEnabled {
return node
}
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
upstreamResolvers := node.Append("Upstream resolvers:")
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
update := "disabled"
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
node.AppendNode(d.Blacklist.toLinesNode())
return node return node
} }
func (d *DNS) read(r *reader.Reader) (err error) {
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
if err != nil {
return err
}
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return err
}
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
if err != nil {
return err
}
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
if err != nil {
return err
}
err = d.Blacklist.read(r)
if err != nil {
return err
}
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
if err != nil {
return err
}
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
if err != nil {
return err
}
return nil
}

View File

@@ -3,14 +3,12 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/netip"
"regexp" "regexp"
"github.com/qdm12/dns/v2/pkg/blockbuilder" "github.com/qdm12/dns/pkg/blacklist"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"inet.af/netaddr"
) )
// DNSBlacklist is settings for the DNS blacklist building. // DNSBlacklist is settings for the DNS blacklist building.
@@ -20,25 +18,21 @@ type DNSBlacklist struct {
BlockSurveillance *bool BlockSurveillance *bool
AllowedHosts []string AllowedHosts []string
AddBlockedHosts []string AddBlockedHosts []string
AddBlockedIPs []netip.Addr AddBlockedIPs []netaddr.IP
AddBlockedIPPrefixes []netip.Prefix AddBlockedIPPrefixes []netaddr.IPPrefix
// RebindingProtectionExemptHostnames is a list of hostnames
// exempt from DNS rebinding protection.
RebindingProtectionExemptHostnames []string
} }
func (b *DNSBlacklist) setDefaults() { func (b *DNSBlacklist) setDefaults() {
b.BlockMalicious = gosettings.DefaultPointer(b.BlockMalicious, true) b.BlockMalicious = helpers.DefaultBool(b.BlockMalicious, true)
b.BlockAds = gosettings.DefaultPointer(b.BlockAds, false) b.BlockAds = helpers.DefaultBool(b.BlockAds, false)
b.BlockSurveillance = gosettings.DefaultPointer(b.BlockSurveillance, true) b.BlockSurveillance = helpers.DefaultBool(b.BlockSurveillance, true)
} }
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
var ( var (
ErrAllowedHostNotValid = errors.New("allowed host is not valid") ErrAllowedHostNotValid = errors.New("allowed host is not valid")
ErrBlockedHostNotValid = errors.New("blocked host is not valid") ErrBlockedHostNotValid = errors.New("blocked host is not valid")
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt host is not valid")
) )
func (b DNSBlacklist) validate() (err error) { func (b DNSBlacklist) validate() (err error) {
@@ -54,53 +48,51 @@ func (b DNSBlacklist) validate() (err error) {
} }
} }
for _, host := range b.RebindingProtectionExemptHostnames {
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
}
}
return nil return nil
} }
func (b DNSBlacklist) copy() (copied DNSBlacklist) { func (b DNSBlacklist) copy() (copied DNSBlacklist) {
return DNSBlacklist{ return DNSBlacklist{
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious), BlockMalicious: helpers.CopyBoolPtr(b.BlockMalicious),
BlockAds: gosettings.CopyPointer(b.BlockAds), BlockAds: helpers.CopyBoolPtr(b.BlockAds),
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance), BlockSurveillance: helpers.CopyBoolPtr(b.BlockSurveillance),
AllowedHosts: gosettings.CopySlice(b.AllowedHosts), AllowedHosts: helpers.CopyStringSlice(b.AllowedHosts),
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts), AddBlockedHosts: helpers.CopyStringSlice(b.AddBlockedHosts),
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs), AddBlockedIPs: helpers.CopyNetaddrIPsSlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes), AddBlockedIPPrefixes: helpers.CopyIPPrefixSlice(b.AddBlockedIPPrefixes),
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
} }
} }
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) { func (b *DNSBlacklist) mergeWith(other DNSBlacklist) {
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious) b.BlockMalicious = helpers.MergeWithBool(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds) b.BlockAds = helpers.MergeWithBool(b.BlockAds, other.BlockAds)
b.BlockSurveillance = gosettings.OverrideWithPointer(b.BlockSurveillance, other.BlockSurveillance) b.BlockSurveillance = helpers.MergeWithBool(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts) b.AllowedHosts = helpers.MergeStringSlices(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts) b.AddBlockedHosts = helpers.MergeStringSlices(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs) b.AddBlockedIPs = helpers.MergeNetaddrIPsSlices(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes) b.AddBlockedIPPrefixes = helpers.MergeIPPrefixesSlices(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
other.RebindingProtectionExemptHostnames)
} }
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) ( func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
settings blockbuilder.Settings, b.BlockMalicious = helpers.OverrideWithBool(b.BlockMalicious, other.BlockMalicious)
) { b.BlockAds = helpers.OverrideWithBool(b.BlockAds, other.BlockAds)
return blockbuilder.Settings{ b.BlockSurveillance = helpers.OverrideWithBool(b.BlockSurveillance, other.BlockSurveillance)
Client: client, b.AllowedHosts = helpers.OverrideWithStringSlice(b.AllowedHosts, other.AllowedHosts)
BlockMalicious: b.BlockMalicious, b.AddBlockedHosts = helpers.OverrideWithStringSlice(b.AddBlockedHosts, other.AddBlockedHosts)
BlockAds: b.BlockAds, b.AddBlockedIPs = helpers.OverrideWithNetaddrIPsSlice(b.AddBlockedIPs, other.AddBlockedIPs)
BlockSurveillance: b.BlockSurveillance, b.AddBlockedIPPrefixes = helpers.OverrideWithIPPrefixesSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
}
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
return blacklist.BuilderSettings{
BlockMalicious: *b.BlockMalicious,
BlockAds: *b.BlockAds,
BlockSurveillance: *b.BlockSurveillance,
AllowedHosts: b.AllowedHosts, AllowedHosts: b.AllowedHosts,
AddBlockedHosts: b.AddBlockedHosts, AddBlockedHosts: b.AddBlockedHosts,
AddBlockedIPs: b.AddBlockedIPs, AddBlockedIPs: b.AddBlockedIPs,
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes, AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
} }, nil
} }
func (b DNSBlacklist) String() string { func (b DNSBlacklist) String() string {
@@ -110,130 +102,37 @@ func (b DNSBlacklist) String() string {
func (b DNSBlacklist) toLinesNode() (node *gotree.Node) { func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS filtering settings:") node = gotree.New("DNS filtering settings:")
node.Appendf("Block malicious: %s", gosettings.BoolToYesNo(b.BlockMalicious)) node.Appendf("Block malicious: %s", helpers.BoolPtrToYesNo(b.BlockMalicious))
node.Appendf("Block ads: %s", gosettings.BoolToYesNo(b.BlockAds)) node.Appendf("Block ads: %s", helpers.BoolPtrToYesNo(b.BlockAds))
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance)) node.Appendf("Block surveillance: %s", helpers.BoolPtrToYesNo(b.BlockSurveillance))
if len(b.AllowedHosts) > 0 { if len(b.AllowedHosts) > 0 {
allowedHostsNode := node.Append("Allowed hosts:") allowedHostsNode := node.Appendf("Allowed hosts:")
for _, host := range b.AllowedHosts { for _, host := range b.AllowedHosts {
allowedHostsNode.Append(host) allowedHostsNode.Appendf(host)
} }
} }
if len(b.AddBlockedHosts) > 0 { if len(b.AddBlockedHosts) > 0 {
blockedHostsNode := node.Append("Blocked hosts:") blockedHostsNode := node.Appendf("Blocked hosts:")
for _, host := range b.AddBlockedHosts { for _, host := range b.AddBlockedHosts {
blockedHostsNode.Append(host) blockedHostsNode.Appendf(host)
} }
} }
if len(b.AddBlockedIPs) > 0 { if len(b.AddBlockedIPs) > 0 {
blockedIPsNode := node.Append("Blocked IP addresses:") blockedIPsNode := node.Appendf("Blocked IP addresses:")
for _, ip := range b.AddBlockedIPs { for _, ip := range b.AddBlockedIPs {
blockedIPsNode.Append(ip.String()) blockedIPsNode.Appendf(ip.String())
} }
} }
if len(b.AddBlockedIPPrefixes) > 0 { if len(b.AddBlockedIPPrefixes) > 0 {
blockedIPPrefixesNode := node.Append("Blocked IP networks:") blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
for _, ipNetwork := range b.AddBlockedIPPrefixes { for _, ipNetwork := range b.AddBlockedIPPrefixes {
blockedIPPrefixesNode.Append(ipNetwork.String()) blockedIPPrefixesNode.Appendf(ipNetwork.String())
}
}
if len(b.RebindingProtectionExemptHostnames) > 0 {
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
for _, host := range b.RebindingProtectionExemptHostnames {
exemptHostsNode.Append(host)
} }
} }
return node return node
} }
func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
b.BlockMalicious, err = r.BoolPtr("BLOCK_MALICIOUS")
if err != nil {
return err
}
b.BlockSurveillance, err = r.BoolPtr("BLOCK_SURVEILLANCE",
reader.RetroKeys("BLOCK_NSA"))
if err != nil {
return err
}
b.BlockAds, err = r.BoolPtr("BLOCK_ADS")
if err != nil {
return err
}
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
if err != nil {
return err
}
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
return nil
}
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
if err != nil {
return nil, nil, err
}
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
if err != nil {
return nil, nil, err
}
// TODO v4 remove this block below
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
if err != nil {
return nil, nil, err
}
ips = append(ips, privateIPs...)
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
return ips, ipPrefixes, nil
}
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
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"))
if len(privateAddresses) == 0 {
return nil, nil, nil
}
ips = make([]netip.Addr, 0, len(privateAddresses))
ipPrefixes = make([]netip.Prefix, 0, len(privateAddresses))
for _, privateAddress := range privateAddresses {
ip, err := netip.ParseAddr(privateAddress)
if err == nil {
ips = append(ips, ip)
continue
}
ipPrefix, err := netip.ParsePrefix(privateAddress)
if err == nil {
ipPrefixes = append(ipPrefixes, ipPrefix)
continue
}
return nil, nil, fmt.Errorf(
"environment variable DOT_PRIVATE_ADDRESS: %w: %s",
ErrPrivateAddressNotValid, privateAddress)
}
return ips, ipPrefixes, nil
}

View File

@@ -0,0 +1,113 @@
package settings
import (
"errors"
"fmt"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"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: helpers.CopyBoolPtr(d.Enabled),
UpdatePeriod: helpers.CopyDurationPtr(d.UpdatePeriod),
Unbound: d.Unbound.copy(),
Blacklist: d.Blacklist.copy(),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DoT) mergeWith(other DoT) {
d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.mergeWith(other.Unbound)
d.Blacklist.mergeWith(other.Blacklist)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DoT) overrideWith(other DoT) {
d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.overrideWith(other.Unbound)
d.Blacklist.overrideWith(other.Blacklist)
}
func (d *DoT) setDefaults() {
d.Enabled = helpers.DefaultBool(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = helpers.DefaultDuration(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", helpers.BoolPtrToYesNo(d.Enabled))
if !*d.Enabled {
return node
}
update := "disabled"
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
}

View File

@@ -3,24 +3,19 @@ package settings
import "errors" import "errors"
var ( var (
ErrValueUnknown = errors.New("value is unknown")
ErrCityNotValid = errors.New("the city specified is not valid") ErrCityNotValid = errors.New("the city specified is not valid")
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root") ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
ErrCategoryNotValid = errors.New("the category specified is not valid")
ErrCountryNotValid = errors.New("the country specified is not valid") ErrCountryNotValid = errors.New("the country specified is not valid")
ErrFilepathMissing = errors.New("filepath is missing") ErrFilepathMissing = errors.New("filepath is missing")
ErrFirewallZeroPort = errors.New("cannot have a zero port") ErrFirewallZeroPort = errors.New("cannot have a zero port to block")
ErrFirewallPublicOutboundSubnet = errors.New("outbound subnet has an unspecified address")
ErrHostnameNotValid = errors.New("the hostname specified is not valid") ErrHostnameNotValid = errors.New("the hostname specified is not valid")
ErrISPNotValid = errors.New("the ISP specified is not valid") ErrISPNotValid = errors.New("the ISP specified is not valid")
ErrMinRatioNotValid = errors.New("minimum ratio is not valid")
ErrMissingValue = errors.New("missing value") ErrMissingValue = errors.New("missing value")
ErrNameNotValid = errors.New("the server name specified is not valid") ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing") ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed") ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid") ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid") ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
ErrOpenVPNKeyPassphraseIsEmpty = errors.New("key passphrase is empty")
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high") ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty") ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported") ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
@@ -28,31 +23,22 @@ var (
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds") ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid") ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled") ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty") ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
ErrRegionNotValid = errors.New("the region specified is not valid") ErrRegionNotValid = errors.New("the region specified is not valid")
ErrServerAddressNotValid = errors.New("server listening address is not valid") ErrServerAddressNotValid = errors.New("server listening address is not valid")
ErrSystemPGIDNotValid = errors.New("process group id is not valid") ErrSystemPGIDNotValid = errors.New("process group id is not valid")
ErrSystemPUIDNotValid = errors.New("process user id is not valid") ErrSystemPUIDNotValid = errors.New("process user id is not valid")
ErrSystemTimezoneNotValid = errors.New("timezone is not valid") ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small") ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
ErrUpdaterProtonEmailMissing = errors.New("proton email is missing")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid") ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid") ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set") ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed") ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set") ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
ErrWireguardEndpointPortSet = errors.New("endpoint port is set")
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set") ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
ErrWireguardInterfaceAddressIPv6 = errors.New("interface address is IPv6 but IPv6 is not supported")
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid") ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set") ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set") ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPublicKeyNotSet = errors.New("public key is not set") ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid") ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
ErrWireguardKeepAliveNegative = errors.New("persistent keep alive interval is negative")
ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
) )

View File

@@ -2,10 +2,9 @@ package settings
import ( import (
"fmt" "fmt"
"net/netip" "net"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -13,7 +12,7 @@ import (
type Firewall struct { type Firewall struct {
VPNInputPorts []uint16 VPNInputPorts []uint16
InputPorts []uint16 InputPorts []uint16
OutboundSubnets []netip.Prefix OutboundSubnets []net.IPNet
Enabled *bool Enabled *bool
Debug *bool Debug *bool
} }
@@ -27,12 +26,6 @@ func (f Firewall) validate() (err error) {
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort) return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
} }
for _, subnet := range f.OutboundSubnets {
if subnet.Addr().IsUnspecified() {
return fmt.Errorf("%w: %s", ErrFirewallPublicOutboundSubnet, subnet)
}
}
return nil return nil
} }
@@ -47,28 +40,40 @@ func hasZeroPort(ports []uint16) (has bool) {
func (f *Firewall) copy() (copied Firewall) { func (f *Firewall) copy() (copied Firewall) {
return Firewall{ return Firewall{
VPNInputPorts: gosettings.CopySlice(f.VPNInputPorts), VPNInputPorts: helpers.CopyUint16Slice(f.VPNInputPorts),
InputPorts: gosettings.CopySlice(f.InputPorts), InputPorts: helpers.CopyUint16Slice(f.InputPorts),
OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets), OutboundSubnets: helpers.CopyIPNetSlice(f.OutboundSubnets),
Enabled: gosettings.CopyPointer(f.Enabled), Enabled: helpers.CopyBoolPtr(f.Enabled),
Debug: gosettings.CopyPointer(f.Debug), Debug: helpers.CopyBoolPtr(f.Debug),
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
// It merges values of slices together, even if they
// are set in the receiver settings.
func (f *Firewall) mergeWith(other Firewall) {
f.VPNInputPorts = helpers.MergeUint16Slices(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = helpers.MergeUint16Slices(f.InputPorts, other.InputPorts)
f.OutboundSubnets = helpers.MergeIPNetsSlices(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = helpers.MergeWithBool(f.Enabled, other.Enabled)
f.Debug = helpers.MergeWithBool(f.Debug, other.Debug)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (f *Firewall) overrideWith(other Firewall) { func (f *Firewall) overrideWith(other Firewall) {
f.VPNInputPorts = gosettings.OverrideWithSlice(f.VPNInputPorts, other.VPNInputPorts) f.VPNInputPorts = helpers.OverrideWithUint16Slice(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts) f.InputPorts = helpers.OverrideWithUint16Slice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets) f.OutboundSubnets = helpers.OverrideWithIPNetsSlice(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled) f.Enabled = helpers.OverrideWithBool(f.Enabled, other.Enabled)
f.Debug = gosettings.OverrideWithPointer(f.Debug, other.Debug) f.Debug = helpers.OverrideWithBool(f.Debug, other.Debug)
} }
func (f *Firewall) setDefaults() { func (f *Firewall) setDefaults() {
f.Enabled = gosettings.DefaultPointer(f.Enabled, true) f.Enabled = helpers.DefaultBool(f.Enabled, true)
f.Debug = gosettings.DefaultPointer(f.Debug, false) f.Debug = helpers.DefaultBool(f.Debug, false)
} }
func (f Firewall) String() string { func (f Firewall) String() string {
@@ -78,7 +83,7 @@ func (f Firewall) String() string {
func (f Firewall) toLinesNode() (node *gotree.Node) { func (f Firewall) toLinesNode() (node *gotree.Node) {
node = gotree.New("Firewall settings:") node = gotree.New("Firewall settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(f.Enabled)) node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(f.Enabled))
if !*f.Enabled { if !*f.Enabled {
return node return node
} }
@@ -104,39 +109,9 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
if len(f.OutboundSubnets) > 0 { if len(f.OutboundSubnets) > 0 {
outboundSubnets := node.Appendf("Outbound subnets:") outboundSubnets := node.Appendf("Outbound subnets:")
for _, subnet := range f.OutboundSubnets { for _, subnet := range f.OutboundSubnets {
outboundSubnets.Appendf("%s", &subnet) outboundSubnets.Appendf("%s", subnet)
} }
} }
return node return node
} }
func (f *Firewall) read(r *reader.Reader) (err error) {
f.VPNInputPorts, err = r.CSVUint16("FIREWALL_VPN_INPUT_PORTS")
if err != nil {
return err
}
f.InputPorts, err = r.CSVUint16("FIREWALL_INPUT_PORTS")
if err != nil {
return err
}
f.OutboundSubnets, err = r.CSVNetipPrefixes(
"FIREWALL_OUTBOUND_SUBNETS", reader.RetroKeys("EXTRA_SUBNETS"))
if err != nil {
return err
}
f.Enabled, err = r.BoolPtr("FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT")
if err != nil {
return err
}
f.Debug, err = r.BoolPtr("FIREWALL_DEBUG")
if err != nil {
return err
}
return nil
}

View File

@@ -1,73 +0,0 @@
package settings
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Firewall_validate(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
firewall Firewall
errWrapped error
errMessage string
}{
"empty": {},
"zero_vpn_input_port": {
firewall: Firewall{
VPNInputPorts: []uint16{0},
},
errWrapped: ErrFirewallZeroPort,
errMessage: "VPN input ports: cannot have a zero port",
},
"zero_input_port": {
firewall: Firewall{
InputPorts: []uint16{0},
},
errWrapped: ErrFirewallZeroPort,
errMessage: "input ports: cannot have a zero port",
},
"unspecified_outbound_subnet": {
firewall: Firewall{
OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
},
},
errWrapped: ErrFirewallPublicOutboundSubnet,
errMessage: "outbound subnet has an unspecified address: 0.0.0.0/0",
},
"public_outbound_subnet": {
firewall: Firewall{
OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("1.2.3.4/32"),
},
},
},
"valid_settings": {
firewall: Firewall{
VPNInputPorts: []uint16{100, 101},
InputPorts: []uint16{200, 201},
OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("192.168.1.0/24"),
netip.MustParsePrefix("10.10.1.1/32"),
},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
err := testCase.firewall.validate()
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}

View File

@@ -1,15 +1,12 @@
package settings package settings
import ( import (
"errors"
"fmt" "fmt"
"net/netip"
"os" "os"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/govalid/address"
) )
// Health contains settings for the healthcheck and health server. // Health contains settings for the healthcheck and health server.
@@ -18,51 +15,24 @@ type Health struct {
// for the health check server. // for the health check server.
// It cannot be the empty string in the internal state. // It cannot be the empty string in the internal state.
ServerAddress string ServerAddress string
// TargetAddresses are the addresses (host or host:port) // 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.
// Addresses after the first one are used as fallbacks for retries. // It cannot be the empty string in the internal state.
// It cannot be empty in the internal state. TargetAddress string
TargetAddresses []string VPN HealthyWait
// ICMPTargetIPs are the IP addresses to use for ICMP echo requests
// in the health checker. The slice can be set to a single
// unspecified address (0.0.0.0) such that the VPN server IP is used,
// although this can be less reliable. It defaults to [1.1.1.1,8.8.8.8],
// and cannot be left empty in the internal state.
ICMPTargetIPs []netip.Addr
// SmallCheckType is the type of small health check to perform.
// It can be "icmp" or "dns", and defaults to "icmp".
// Note it changes automatically to dns if icmp is not supported.
SmallCheckType string
// RestartVPN indicates whether to restart the VPN connection
// when the healthcheck fails.
RestartVPN *bool
} }
var (
ErrICMPTargetIPNotValid = errors.New("ICMP target IP address is not valid")
ErrICMPTargetIPsNotCompatible = errors.New("ICMP target IP addresses are not compatible")
ErrSmallCheckTypeNotValid = errors.New("small check type is not valid")
)
func (h Health) Validate() (err error) { func (h Health) Validate() (err error) {
err = validate.ListeningAddress(h.ServerAddress, os.Getuid()) uid := os.Getuid()
_, err = address.Validate(h.ServerAddress,
address.OptionListening(uid))
if err != nil { if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err) return fmt.Errorf("server listening address is not valid: %w", err)
} }
for _, ip := range h.ICMPTargetIPs { err = h.VPN.validate()
switch {
case !ip.IsValid():
return fmt.Errorf("%w: %s", ErrICMPTargetIPNotValid, ip)
case ip.IsUnspecified() && len(h.ICMPTargetIPs) > 1:
return fmt.Errorf("%w: only a single IP address must be set if it is to be unspecified",
ErrICMPTargetIPsNotCompatible)
}
}
err = validate.IsOneOf(h.SmallCheckType, "icmp", "dns")
if err != nil { if err != nil {
return fmt.Errorf("%w: %s", ErrSmallCheckTypeNotValid, err) return fmt.Errorf("health VPN settings: %w", err)
} }
return nil return nil
@@ -70,34 +40,33 @@ func (h Health) Validate() (err error) {
func (h *Health) copy() (copied Health) { func (h *Health) copy() (copied Health) {
return Health{ return Health{
ServerAddress: h.ServerAddress, ServerAddress: h.ServerAddress,
TargetAddresses: h.TargetAddresses, TargetAddress: h.TargetAddress,
ICMPTargetIPs: gosettings.CopySlice(h.ICMPTargetIPs), VPN: h.VPN.copy(),
SmallCheckType: h.SmallCheckType,
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
} }
} }
// MergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *Health) MergeWith(other Health) {
h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress)
h.TargetAddress = helpers.MergeWithString(h.TargetAddress, other.TargetAddress)
h.VPN.mergeWith(other.VPN)
}
// OverrideWith overrides fields of the receiver // OverrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (h *Health) OverrideWith(other Health) { func (h *Health) OverrideWith(other Health) {
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress) h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress)
h.TargetAddresses = gosettings.OverrideWithSlice(h.TargetAddresses, other.TargetAddresses) h.TargetAddress = helpers.OverrideWithString(h.TargetAddress, other.TargetAddress)
h.ICMPTargetIPs = gosettings.OverrideWithSlice(h.ICMPTargetIPs, other.ICMPTargetIPs) h.VPN.overrideWith(other.VPN)
h.SmallCheckType = gosettings.OverrideWithComparable(h.SmallCheckType, other.SmallCheckType)
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
} }
func (h *Health) SetDefaults() { func (h *Health) SetDefaults() {
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999") h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999")
h.TargetAddresses = gosettings.DefaultSlice(h.TargetAddresses, []string{"cloudflare.com:443", "github.com:443"}) h.TargetAddress = helpers.DefaultString(h.TargetAddress, "github.com:443")
h.ICMPTargetIPs = gosettings.DefaultSlice(h.ICMPTargetIPs, []netip.Addr{ h.VPN.setDefaults()
netip.AddrFrom4([4]byte{1, 1, 1, 1}),
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
})
h.SmallCheckType = gosettings.DefaultComparable(h.SmallCheckType, "icmp")
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
} }
func (h Health) String() string { func (h Health) String() string {
@@ -107,40 +76,7 @@ func (h Health) String() string {
func (h Health) toLinesNode() (node *gotree.Node) { func (h Health) toLinesNode() (node *gotree.Node) {
node = gotree.New("Health settings:") node = gotree.New("Health settings:")
node.Appendf("Server listening address: %s", h.ServerAddress) node.Appendf("Server listening address: %s", h.ServerAddress)
targetAddrs := node.Appendf("Target addresses:") node.Appendf("Target address: %s", h.TargetAddress)
for _, targetAddr := range h.TargetAddresses { node.AppendNode(h.VPN.toLinesNode("VPN"))
targetAddrs.Append(targetAddr)
}
switch h.SmallCheckType {
case "icmp":
icmpNode := node.Appendf("Small health check type: ICMP echo request")
if len(h.ICMPTargetIPs) == 1 && h.ICMPTargetIPs[0].IsUnspecified() {
icmpNode.Appendf("ICMP target IP: VPN server IP address")
} else {
icmpIPs := icmpNode.Appendf("ICMP target IPs:")
for _, ip := range h.ICMPTargetIPs {
icmpIPs.Append(ip.String())
}
}
case "dns":
node.Appendf("Small health check type: Plain DNS lookup over UDP")
}
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
return node return node
} }
func (h *Health) Read(r *reader.Reader) (err error) {
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
h.TargetAddresses = r.CSV("HEALTH_TARGET_ADDRESSES",
reader.RetroKeys("HEALTH_ADDRESS_TO_PING", "HEALTH_TARGET_ADDRESS"))
h.ICMPTargetIPs, err = r.CSVNetipAddresses("HEALTH_ICMP_TARGET_IPS", reader.RetroKeys("HEALTH_ICMP_TARGET_IP"))
if err != nil {
return err
}
h.SmallCheckType = r.String("HEALTH_SMALL_CHECK_TYPE")
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,66 @@
package settings
import (
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"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
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{
Initial: helpers.CopyDurationPtr(h.Initial),
Addition: helpers.CopyDurationPtr(h.Addition),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) mergeWith(other HealthyWait) {
h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial)
h.Addition = helpers.MergeWithDuration(h.Addition, other.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 = helpers.OverrideWithDuration(h.Initial, other.Initial)
h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition)
}
func (h *HealthyWait) setDefaults() {
const initialDurationDefault = 6 * time.Second
const additionDurationDefault = 5 * time.Second
h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault)
h.Addition = helpers.DefaultDuration(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
}

View File

@@ -1,5 +0,0 @@
package settings
func ptrTo[T any](value T) *T {
return &value
}

View File

@@ -1,6 +1,12 @@
package helpers package helpers
func IsOneOf[T comparable](value T, choices ...T) (ok bool) { import (
"errors"
"fmt"
"strings"
)
func IsOneOf(value string, choices ...string) (ok bool) {
for _, choice := range choices { for _, choice := range choices {
if value == choice { if value == choice {
return true return true
@@ -8,3 +14,39 @@ func IsOneOf[T comparable](value T, choices ...T) (ok bool) {
} }
return false return false
} }
var (
ErrNoChoice = errors.New("one or more values is set but there is no possible value available")
ErrValueNotOneOf = errors.New("value is not one of the possible choices")
)
func AreAllOneOf(values, choices []string) (err error) {
if len(values) > 0 && len(choices) == 0 {
return ErrNoChoice
}
set := make(map[string]struct{}, len(choices))
for _, choice := range choices {
choice = strings.ToLower(choice)
set[choice] = struct{}{}
}
for _, value := range values {
_, ok := set[value]
if !ok {
return fmt.Errorf("%w: value %q, choices available are %s",
ErrValueNotOneOf, value, strings.Join(choices, ", "))
}
}
return nil
}
func Uint16IsOneOf(port uint16, choices []uint16) (ok bool) {
for _, choice := range choices {
if port == choice {
return true
}
}
return false
}

View File

@@ -0,0 +1,190 @@
package helpers
import (
"net"
"time"
"github.com/qdm12/golibs/logging"
"inet.af/netaddr"
)
func CopyStringPtr(original *string) (copied *string) {
if original == nil {
return nil
}
copied = new(string)
*copied = *original
return copied
}
func CopyBoolPtr(original *bool) (copied *bool) {
if original == nil {
return nil
}
copied = new(bool)
*copied = *original
return copied
}
func CopyUint8Ptr(original *uint8) (copied *uint8) {
if original == nil {
return nil
}
copied = new(uint8)
*copied = *original
return copied
}
func CopyUint16Ptr(original *uint16) (copied *uint16) {
if original == nil {
return nil
}
copied = new(uint16)
*copied = *original
return copied
}
func CopyIntPtr(original *int) (copied *int) {
if original == nil {
return nil
}
copied = new(int)
*copied = *original
return copied
}
func CopyDurationPtr(original *time.Duration) (copied *time.Duration) {
if original == nil {
return nil
}
copied = new(time.Duration)
*copied = *original
return copied
}
func CopyLogLevelPtr(original *logging.Level) (copied *logging.Level) {
if original == nil {
return nil
}
copied = new(logging.Level)
*copied = *original
return copied
}
func CopyIP(original net.IP) (copied net.IP) {
if original == nil {
return nil
}
copied = make(net.IP, len(original))
copy(copied, original)
return copied
}
func CopyIPNet(original net.IPNet) (copied net.IPNet) {
if original.IP != nil {
copied.IP = make(net.IP, len(original.IP))
copy(copied.IP, original.IP)
}
if original.Mask != nil {
copied.Mask = make(net.IPMask, len(original.Mask))
copy(copied.Mask, original.Mask)
}
return copied
}
func CopyIPNetPtr(original *net.IPNet) (copied *net.IPNet) {
if original == nil {
return nil
}
copied = new(net.IPNet)
*copied = CopyIPNet(*original)
return copied
}
func CopyNetaddrIP(original netaddr.IP) (copied netaddr.IP) {
b, err := original.MarshalBinary()
if err != nil {
panic(err)
}
err = copied.UnmarshalBinary(b)
if err != nil {
panic(err)
}
return copied
}
func CopyIPPrefix(original netaddr.IPPrefix) (copied netaddr.IPPrefix) {
b, err := original.MarshalText()
if err != nil {
panic(err)
}
err = copied.UnmarshalText(b)
if err != nil {
panic(err)
}
return copied
}
func CopyStringSlice(original []string) (copied []string) {
if original == nil {
return nil
}
copied = make([]string, len(original))
copy(copied, original)
return copied
}
func CopyUint16Slice(original []uint16) (copied []uint16) {
if original == nil {
return nil
}
copied = make([]uint16, len(original))
copy(copied, original)
return copied
}
func CopyIPNetSlice(original []net.IPNet) (copied []net.IPNet) {
if original == nil {
return nil
}
copied = make([]net.IPNet, len(original))
for i := range original {
copied[i] = CopyIPNet(original[i])
}
return copied
}
func CopyIPPrefixSlice(original []netaddr.IPPrefix) (copied []netaddr.IPPrefix) {
if original == nil {
return nil
}
copied = make([]netaddr.IPPrefix, len(original))
for i := range original {
copied[i] = CopyIPPrefix(original[i])
}
return copied
}
func CopyNetaddrIPsSlice(original []netaddr.IP) (copied []netaddr.IP) {
if original == nil {
return nil
}
copied = make([]netaddr.IP, len(original))
for i := range original {
copied[i] = CopyNetaddrIP(original[i])
}
return copied
}

View File

@@ -0,0 +1,93 @@
package helpers
import (
"net"
"time"
"github.com/qdm12/golibs/logging"
)
func DefaultInt(existing *int, defaultValue int) (
result *int) {
if existing != nil {
return existing
}
result = new(int)
*result = defaultValue
return result
}
func DefaultUint8(existing *uint8, defaultValue uint8) (
result *uint8) {
if existing != nil {
return existing
}
result = new(uint8)
*result = defaultValue
return result
}
func DefaultUint16(existing *uint16, defaultValue uint16) (
result *uint16) {
if existing != nil {
return existing
}
result = new(uint16)
*result = defaultValue
return result
}
func DefaultBool(existing *bool, defaultValue bool) (
result *bool) {
if existing != nil {
return existing
}
result = new(bool)
*result = defaultValue
return result
}
func DefaultString(existing string, defaultValue string) (
result string) {
if existing != "" {
return existing
}
return defaultValue
}
func DefaultStringPtr(existing *string, defaultValue string) (result *string) {
if existing != nil {
return existing
}
result = new(string)
*result = defaultValue
return result
}
func DefaultDuration(existing *time.Duration,
defaultValue time.Duration) (result *time.Duration) {
if existing != nil {
return existing
}
result = new(time.Duration)
*result = defaultValue
return result
}
func DefaultLogLevel(existing *logging.Level,
defaultValue logging.Level) (result *logging.Level) {
if existing != nil {
return existing
}
result = new(logging.Level)
*result = defaultValue
return result
}
func DefaultIP(existing net.IP, defaultValue net.IP) (
result net.IP) {
if existing != nil {
return existing
}
return defaultValue
}

View File

@@ -0,0 +1,31 @@
package helpers
import (
"errors"
"fmt"
"os"
"path/filepath"
)
var (
ErrFileDoesNotExist = errors.New("file does not exist")
ErrFileRead = errors.New("cannot read file")
ErrFileClose = errors.New("cannot close file")
)
func FileExists(path string) (err error) {
path = filepath.Clean(path)
f, err := os.Open(path)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("%w: %s", ErrFileDoesNotExist, path)
} else if err != nil {
return fmt.Errorf("%w: %s", ErrFileRead, err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("%w: %s", ErrFileClose, err)
}
return nil
}

View File

@@ -0,0 +1,241 @@
package helpers
import (
"net"
"net/http"
"time"
"github.com/qdm12/golibs/logging"
"inet.af/netaddr"
)
func MergeWithBool(existing, other *bool) (result *bool) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(bool)
*result = *other
return result
}
func MergeWithString(existing, other string) (result string) {
if existing != "" {
return existing
}
return other
}
func MergeWithInt(existing, other int) (result int) {
if existing != 0 {
return existing
}
return other
}
func MergeWithStringPtr(existing, other *string) (result *string) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(string)
*result = *other
return result
}
func MergeWithIntPtr(existing, other *int) (result *int) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(int)
*result = *other
return result
}
func MergeWithUint8(existing, other *uint8) (result *uint8) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(uint8)
*result = *other
return result
}
func MergeWithUint16(existing, other *uint16) (result *uint16) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(uint16)
*result = *other
return result
}
func MergeWithIP(existing, other net.IP) (result net.IP) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = make(net.IP, len(other))
copy(result, other)
return result
}
func MergeWithDuration(existing, other *time.Duration) (result *time.Duration) {
if existing != nil {
return existing
}
return other
}
func MergeWithLogLevel(existing, other *logging.Level) (result *logging.Level) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(logging.Level)
*result = *other
return result
}
func MergeWithHTTPHandler(existing, other http.Handler) (result http.Handler) {
if existing != nil {
return existing
}
return other
}
func MergeStringSlices(a, b []string) (result []string) {
if a == nil && b == nil {
return nil
}
seen := make(map[string]struct{}, len(a)+len(b))
result = make([]string, 0, len(a)+len(b))
for _, s := range a {
if _, ok := seen[s]; ok {
continue // duplicate
}
result = append(result, s)
seen[s] = struct{}{}
}
for _, s := range b {
if _, ok := seen[s]; ok {
continue // duplicate
}
result = append(result, s)
seen[s] = struct{}{}
}
return result
}
func MergeUint16Slices(a, b []uint16) (result []uint16) {
if a == nil && b == nil {
return nil
}
seen := make(map[uint16]struct{}, len(a)+len(b))
result = make([]uint16, 0, len(a)+len(b))
for _, n := range a {
if _, ok := seen[n]; ok {
continue // duplicate
}
result = append(result, n)
seen[n] = struct{}{}
}
for _, n := range b {
if _, ok := seen[n]; ok {
continue // duplicate
}
result = append(result, n)
seen[n] = struct{}{}
}
return result
}
func MergeIPNetsSlices(a, b []net.IPNet) (result []net.IPNet) {
if a == nil && b == nil {
return nil
}
seen := make(map[string]struct{}, len(a)+len(b))
result = make([]net.IPNet, 0, len(a)+len(b))
for _, ipNet := range a {
key := ipNet.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ipNet)
seen[key] = struct{}{}
}
for _, ipNet := range b {
key := ipNet.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ipNet)
seen[key] = struct{}{}
}
return result
}
func MergeNetaddrIPsSlices(a, b []netaddr.IP) (result []netaddr.IP) {
if a == nil && b == nil {
return nil
}
seen := make(map[string]struct{}, len(a)+len(b))
result = make([]netaddr.IP, 0, len(a)+len(b))
for _, ip := range a {
key := ip.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ip)
seen[key] = struct{}{}
}
for _, ip := range b {
key := ip.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ip)
seen[key] = struct{}{}
}
return result
}
func MergeIPPrefixesSlices(a, b []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
if a == nil && b == nil {
return nil
}
seen := make(map[string]struct{}, len(a)+len(b))
result = make([]netaddr.IPPrefix, 0, len(a)+len(b))
for _, ipPrefix := range a {
key := ipPrefix.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ipPrefix)
seen[key] = struct{}{}
}
for _, ipPrefix := range b {
key := ipPrefix.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ipPrefix)
seen[key] = struct{}{}
}
return result
}

View File

@@ -0,0 +1,29 @@
package helpers
import (
"fmt"
"strings"
)
func ChoicesOrString(choices []string) string {
return strings.Join(
choices[:len(choices)-1], ", ") +
" or " + choices[len(choices)-1]
}
func PortChoicesOrString(ports []uint16) (s string) {
switch len(ports) {
case 0:
return "there is no allowed port"
case 1:
return "allowed port is " + fmt.Sprint(ports[0])
}
s = "allowed ports are "
portStrings := make([]string, len(ports))
for i := range ports {
portStrings[i] = fmt.Sprint(ports[i])
}
s += ChoicesOrString(portStrings)
return s
}

View File

@@ -0,0 +1,25 @@
package helpers
func ObfuscateWireguardKey(fullKey string) (obfuscatedKey string) {
const minKeyLength = 10
if len(fullKey) < minKeyLength {
return "(too short)"
}
lastIndex := len(fullKey) - 1
return fullKey[0:2] + "..." + fullKey[lastIndex-2:]
}
func ObfuscatePassword(password string) (obfuscatedPassword string) {
if password != "" {
return "[set]"
}
return "[not set]"
}
func ObfuscateData(data string) (obfuscated string) {
if data != "" {
return "[set]"
}
return "[not set]"
}

View File

@@ -0,0 +1,148 @@
package helpers
import (
"net"
"net/http"
"time"
"github.com/qdm12/golibs/logging"
"inet.af/netaddr"
)
func OverrideWithBool(existing, other *bool) (result *bool) {
if other == nil {
return existing
}
result = new(bool)
*result = *other
return result
}
func OverrideWithString(existing, other string) (result string) {
if other == "" {
return existing
}
return other
}
func OverrideWithInt(existing, other int) (result int) {
if other == 0 {
return existing
}
return other
}
func OverrideWithStringPtr(existing, other *string) (result *string) {
if other == nil {
return existing
}
result = new(string)
*result = *other
return result
}
func OverrideWithIntPtr(existing, other *int) (result *int) {
if other == nil {
return existing
}
result = new(int)
*result = *other
return result
}
func OverrideWithUint8(existing, other *uint8) (result *uint8) {
if other == nil {
return existing
}
result = new(uint8)
*result = *other
return result
}
func OverrideWithUint16(existing, other *uint16) (result *uint16) {
if other == nil {
return existing
}
result = new(uint16)
*result = *other
return result
}
func OverrideWithIP(existing, other net.IP) (result net.IP) {
if other == nil {
return existing
}
result = make(net.IP, len(other))
copy(result, other)
return result
}
func OverrideWithDuration(existing, other *time.Duration) (result *time.Duration) {
if other == nil {
return existing
}
result = new(time.Duration)
*result = *other
return result
}
func OverrideWithLogLevel(existing, other *logging.Level) (result *logging.Level) {
if other == nil {
return existing
}
result = new(logging.Level)
*result = *other
return result
}
func OverrideWithHTTPHandler(existing, other http.Handler) (result http.Handler) {
if other != nil {
return other
}
return existing
}
func OverrideWithStringSlice(existing, other []string) (result []string) {
if other == nil {
return existing
}
result = make([]string, len(other))
copy(result, other)
return result
}
func OverrideWithUint16Slice(existing, other []uint16) (result []uint16) {
if other == nil {
return existing
}
result = make([]uint16, len(other))
copy(result, other)
return result
}
func OverrideWithIPNetsSlice(existing, other []net.IPNet) (result []net.IPNet) {
if other == nil {
return existing
}
result = make([]net.IPNet, len(other))
copy(result, other)
return result
}
func OverrideWithNetaddrIPsSlice(existing, other []netaddr.IP) (result []netaddr.IP) {
if other == nil {
return existing
}
result = make([]netaddr.IP, len(other))
copy(result, other)
return result
}
func OverrideWithIPPrefixesSlice(existing, other []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
if other == nil {
return existing
}
result = make([]netaddr.IPPrefix, len(other))
copy(result, other)
return result
}

View File

@@ -0,0 +1,11 @@
package helpers
import "time"
// StringPtr returns a pointer to the string value
// passed as argument.
func StringPtr(s string) *string { return &s }
// DurationPtr returns a pointer to the duration value
// passed as argument.
func DurationPtr(d time.Duration) *time.Duration { return &d }

View File

@@ -0,0 +1,15 @@
package helpers
func BoolPtrToYesNo(b *bool) string {
if *b {
return "yes"
}
return "no"
}
func TCPPtrToString(tcp *bool) string {
if *tcp {
return "TCP"
}
return "UDP"
}

View File

@@ -1,30 +1,4 @@
package settings package settings
import gomock "github.com/golang/mock/gomock" func boolPtr(b bool) *bool { return &b }
func uint8Ptr(n uint8) *uint8 { return &n }
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
}

View File

@@ -3,13 +3,10 @@ package settings
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"time"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/govalid/address"
) )
// HTTPProxy contains settings to configure the HTTP proxy. // HTTPProxy contains settings to configure the HTTP proxy.
@@ -36,17 +33,13 @@ type HTTPProxy struct {
// each request/response. It cannot be nil in the // each request/response. It cannot be nil in the
// internal state. // internal state.
Log *bool Log *bool
// ReadHeaderTimeout is the HTTP header read timeout duration
// of the HTTP server. It defaults to 1 second if left unset.
ReadHeaderTimeout time.Duration
// ReadTimeout is the HTTP read timeout duration
// of the HTTP server. It defaults to 3 seconds if left unset.
ReadTimeout time.Duration
} }
func (h HTTPProxy) validate() (err error) { func (h HTTPProxy) validate() (err error) {
// Do not validate user and password // Do not validate user and password
err = validate.ListeningAddress(h.ListeningAddress, os.Getuid())
uid := os.Getuid()
_, err = address.Validate(h.ListeningAddress, address.OptionListening(uid))
if err != nil { if err != nil {
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress) return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
} }
@@ -56,42 +49,45 @@ func (h HTTPProxy) validate() (err error) {
func (h *HTTPProxy) copy() (copied HTTPProxy) { func (h *HTTPProxy) copy() (copied HTTPProxy) {
return HTTPProxy{ return HTTPProxy{
User: gosettings.CopyPointer(h.User), User: helpers.CopyStringPtr(h.User),
Password: gosettings.CopyPointer(h.Password), Password: helpers.CopyStringPtr(h.Password),
ListeningAddress: h.ListeningAddress, ListeningAddress: h.ListeningAddress,
Enabled: gosettings.CopyPointer(h.Enabled), Enabled: helpers.CopyBoolPtr(h.Enabled),
Stealth: gosettings.CopyPointer(h.Stealth), Stealth: helpers.CopyBoolPtr(h.Stealth),
Log: gosettings.CopyPointer(h.Log), Log: helpers.CopyBoolPtr(h.Log),
ReadHeaderTimeout: h.ReadHeaderTimeout,
ReadTimeout: h.ReadTimeout,
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HTTPProxy) mergeWith(other HTTPProxy) {
h.User = helpers.MergeWithStringPtr(h.User, other.User)
h.Password = helpers.MergeWithStringPtr(h.Password, other.Password)
h.ListeningAddress = helpers.MergeWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled)
h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth)
h.Log = helpers.MergeWithBool(h.Log, other.Log)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (h *HTTPProxy) overrideWith(other HTTPProxy) { func (h *HTTPProxy) overrideWith(other HTTPProxy) {
h.User = gosettings.OverrideWithPointer(h.User, other.User) h.User = helpers.OverrideWithStringPtr(h.User, other.User)
h.Password = gosettings.OverrideWithPointer(h.Password, other.Password) h.Password = helpers.OverrideWithStringPtr(h.Password, other.Password)
h.ListeningAddress = gosettings.OverrideWithComparable(h.ListeningAddress, other.ListeningAddress) h.ListeningAddress = helpers.OverrideWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = gosettings.OverrideWithPointer(h.Enabled, other.Enabled) h.Enabled = helpers.OverrideWithBool(h.Enabled, other.Enabled)
h.Stealth = gosettings.OverrideWithPointer(h.Stealth, other.Stealth) h.Stealth = helpers.OverrideWithBool(h.Stealth, other.Stealth)
h.Log = gosettings.OverrideWithPointer(h.Log, other.Log) h.Log = helpers.OverrideWithBool(h.Log, other.Log)
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
} }
func (h *HTTPProxy) setDefaults() { func (h *HTTPProxy) setDefaults() {
h.User = gosettings.DefaultPointer(h.User, "") h.User = helpers.DefaultStringPtr(h.User, "")
h.Password = gosettings.DefaultPointer(h.Password, "") h.Password = helpers.DefaultStringPtr(h.Password, "")
h.ListeningAddress = gosettings.DefaultComparable(h.ListeningAddress, ":8888") h.ListeningAddress = helpers.DefaultString(h.ListeningAddress, ":8888")
h.Enabled = gosettings.DefaultPointer(h.Enabled, false) h.Enabled = helpers.DefaultBool(h.Enabled, false)
h.Stealth = gosettings.DefaultPointer(h.Stealth, false) h.Stealth = helpers.DefaultBool(h.Stealth, false)
h.Log = gosettings.DefaultPointer(h.Log, false) h.Log = helpers.DefaultBool(h.Log, false)
const defaultReadHeaderTimeout = time.Second
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 3 * time.Second
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
} }
func (h HTTPProxy) String() string { func (h HTTPProxy) String() string {
@@ -100,83 +96,16 @@ func (h HTTPProxy) String() string {
func (h HTTPProxy) toLinesNode() (node *gotree.Node) { func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
node = gotree.New("HTTP proxy settings:") node = gotree.New("HTTP proxy settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(h.Enabled)) node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(h.Enabled))
if !*h.Enabled { if !*h.Enabled {
return node return node
} }
node.Appendf("Listening address: %s", h.ListeningAddress) node.Appendf("Listening address: %s", h.ListeningAddress)
node.Appendf("User: %s", *h.User) node.Appendf("User: %s", *h.User)
node.Appendf("Password: %s", gosettings.ObfuscateKey(*h.Password)) node.Appendf("Password: %s", helpers.ObfuscatePassword(*h.Password))
node.Appendf("Stealth mode: %s", gosettings.BoolToYesNo(h.Stealth)) node.Appendf("Stealth mode: %s", helpers.BoolPtrToYesNo(h.Stealth))
node.Appendf("Log: %s", gosettings.BoolToYesNo(h.Log)) node.Appendf("Log: %s", helpers.BoolPtrToYesNo(h.Log))
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
node.Appendf("Read timeout: %s", h.ReadTimeout)
return node return node
} }
func (h *HTTPProxy) read(r *reader.Reader) (err error) {
h.User = r.Get("HTTPPROXY_USER",
reader.RetroKeys("PROXY_USER", "TINYPROXY_USER"),
reader.ForceLowercase(false))
h.Password = r.Get("HTTPPROXY_PASSWORD",
reader.RetroKeys("PROXY_PASSWORD", "TINYPROXY_PASSWORD"),
reader.ForceLowercase(false))
h.ListeningAddress, err = readHTTProxyListeningAddress(r)
if err != nil {
return err
}
h.Enabled, err = r.BoolPtr("HTTPPROXY", reader.RetroKeys("PROXY", "TINYPROXY"))
if err != nil {
return err
}
h.Stealth, err = r.BoolPtr("HTTPPROXY_STEALTH")
if err != nil {
return err
}
h.Log, err = readHTTProxyLog(r)
if err != nil {
return err
}
return nil
}
func readHTTProxyListeningAddress(r *reader.Reader) (listeningAddress string, err error) {
// Retro-compatible keys using a port only
port, err := r.Uint16Ptr("",
reader.RetroKeys("HTTPPROXY_PORT", "TINYPROXY_PORT", "PROXY_PORT"),
reader.IsRetro("HTTPPROXY_LISTENING_ADDRESS"))
if err != nil {
return "", err
} else if port != nil {
return fmt.Sprintf(":%d", *port), nil
}
const currentKey = "HTTPPROXY_LISTENING_ADDRESS"
return r.String(currentKey), nil
}
func readHTTProxyLog(r *reader.Reader) (enabled *bool, err error) {
const currentKey = "HTTPPROXY_LOG"
// Retro-compatible keys using different boolean verbs
value := r.String("",
reader.RetroKeys("PROXY_LOG", "TINYPROXY_LOG"),
reader.IsRetro(currentKey))
switch strings.ToLower(value) {
case "":
return r.BoolPtr(currentKey)
case "on", "info", "connect", "notice":
return ptrTo(true), nil
case "disabled", "no", "off":
return ptrTo(false), nil
default:
return nil, fmt.Errorf("HTTP retro-compatible proxy log setting: %w: %s",
ErrValueUnknown, value)
}
}

View File

@@ -1,5 +0,0 @@
package settings
type Warner interface {
Warn(message string)
}

View File

@@ -1,44 +1,43 @@
package settings package settings
import ( import (
"fmt" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/log"
) )
// Log contains settings to configure the logger. // Log contains settings to configure the logger.
type Log struct { type Log struct {
// Level is the log level of the logger. // Level is the log level of the logger.
// It cannot be empty in the internal state. // It cannot be nil in the internal state.
Level string Level *logging.Level
} }
func (l Log) validate() (err error) { func (l Log) validate() (err error) {
_, err = log.ParseLevel(l.Level)
if err != nil {
return fmt.Errorf("level: %w", err)
}
return nil return nil
} }
func (l *Log) copy() (copied Log) { func (l *Log) copy() (copied Log) {
return Log{ return Log{
Level: l.Level, Level: helpers.CopyLogLevelPtr(l.Level),
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (l *Log) mergeWith(other Log) {
l.Level = helpers.MergeWithLogLevel(l.Level, other.Level)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (l *Log) overrideWith(other Log) { func (l *Log) overrideWith(other Log) {
l.Level = gosettings.OverrideWithComparable(l.Level, other.Level) l.Level = helpers.OverrideWithLogLevel(l.Level, other.Level)
} }
func (l *Log) setDefaults() { func (l *Log) setDefaults() {
l.Level = gosettings.DefaultComparable(l.Level, log.LevelInfo.String()) l.Level = helpers.DefaultLogLevel(l.Level, logging.LevelInfo)
} }
func (l Log) String() string { func (l Log) String() string {
@@ -47,11 +46,6 @@ func (l Log) String() string {
func (l Log) toLinesNode() (node *gotree.Node) { func (l Log) toLinesNode() (node *gotree.Node) {
node = gotree.New("Log settings:") node = gotree.New("Log settings:")
node.Appendf("Log level: %s", l.Level) node.Appendf("Log level: %s", l.Level.String())
return node return node
} }
func (l *Log) read(r *reader.Reader) (err error) {
l.Level = r.String("LOG_LEVEL")
return nil
}

View File

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

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
package settings
// Retro-compatibility because SERVER_REGIONS changed to SERVER_COUNTRIES
// and SERVER_REGIONS is now the continent field for servers.
// TODO v4 remove.
func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) (
updatedSelection ServerSelection,
) {
validRegionsMap := stringSliceToMap(validRegions)
validCountriesMap := stringSliceToMap(validCountries)
updatedSelection = selection.copy()
updatedSelection.Regions = make([]string, 0, len(selection.Regions))
for _, region := range selection.Regions {
_, isValid := validRegionsMap[region]
if isValid {
updatedSelection.Regions = append(updatedSelection.Regions, region)
continue
}
_, isValid = validCountriesMap[region]
if !isValid {
// Region is not valid for the country or region
// just leave it to the validation to fail it later
continue
}
// Region is not valid for a region, but is a valid country
// Handle retro-compatibility and transfer the value to the
// country field.
updatedSelection.Countries = append(updatedSelection.Countries, region)
}
return updatedSelection
}
func stringSliceToMap(slice []string) (m map[string]struct{}) {
m = make(map[string]struct{}, len(slice))
for _, s := range slice {
m[s] = struct{}{}
}
return m
}

View File

@@ -1,114 +1,94 @@
package settings package settings
import ( import (
"encoding/base64"
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/qdm12/gluetun/internal/constants/openvpn" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/openvpn/extract" "github.com/qdm12/gluetun/internal/openvpn/parse"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// OpenVPN contains settings to configure the OpenVPN client. // OpenVPN contains settings to configure the OpenVPN client.
type OpenVPN struct { type OpenVPN struct {
// Version is the OpenVPN version to run. // Version is the OpenVPN version to run.
// It can only be "2.5" or "2.6". // It can only be "2.4" or "2.5".
Version string `json:"version"` Version string
// User is the OpenVPN authentication username. // User is the OpenVPN authentication username.
// It cannot be nil in the internal state if OpenVPN is used. // It cannot be an empty string in the internal state
// It is usually required but in some cases can be the empty string // if OpenVPN is used.
// to indicate no user+password authentication is needed. User string
User *string `json:"user"`
// Password is the OpenVPN authentication password. // Password is the OpenVPN authentication password.
// It cannot be nil in the internal state if OpenVPN is used. // It cannot be an empty string in the internal state
// It is usually required but in some cases can be the empty string // if OpenVPN is used.
// to indicate no user+password authentication is needed. Password string
Password *string `json:"password"`
// ConfFile is a custom OpenVPN configuration file path. // ConfFile is a custom OpenVPN configuration file path.
// It can be set to the empty string for it to be ignored. // It can be set to the empty string for it to be ignored.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
ConfFile *string `json:"config_file_path"` ConfFile *string
// Ciphers is a list of ciphers to use for OpenVPN, // Ciphers is a list of ciphers to use for OpenVPN,
// different from the ones specified by the VPN // different from the ones specified by the VPN
// service provider configuration files. // service provider configuration files.
Ciphers []string `json:"ciphers"` Ciphers []string
// Auth is an auth algorithm to use in OpenVPN instead // Auth is an auth algorithm to use in OpenVPN instead
// of the one specified by the VPN service provider. // of the one specified by the VPN service provider.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
// It is ignored if it is set to the empty string. // It is ignored if it is set to the empty string.
Auth *string `json:"auth"` Auth *string
// Cert is the base64 encoded DER of an OpenVPN certificate for the <cert> block. // ClientCrt is the OpenVPN client certificate.
// This is notably used by Cyberghost and VPN secure. // This is notably used by Cyberghost.
// It can be set to the empty string to be ignored. // It can be set to the empty string to be ignored.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
Cert *string `json:"cert"` ClientCrt *string
// Key is the base64 encoded DER of an OpenVPN key. // ClientKey is the OpenVPN client key.
// This is used by Cyberghost and VPN Unlimited. // This is used by Cyberghost and VPN Unlimited.
// It can be set to the empty string to be ignored. // It can be set to the empty string to be ignored.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
Key *string `json:"key"` ClientKey *string
// EncryptedKey is the base64 encoded DER of an encrypted key for OpenVPN.
// It is used by VPN secure.
// It defaults to the empty string meaning it is not
// to be used. KeyPassphrase must be set if this one is set.
EncryptedKey *string `json:"encrypted_key"`
// KeyPassphrase is the key passphrase to be used by OpenVPN
// to decrypt the EncryptedPrivateKey. It defaults to the
// empty string and must be set if EncryptedPrivateKey is set.
KeyPassphrase *string `json:"key_passphrase"`
// PIAEncPreset is the encryption preset for // PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an // Private Internet Access. It can be set to an
// empty string for other providers. // empty string for other providers.
PIAEncPreset *string `json:"pia_encryption_preset"` PIAEncPreset *string
// IPv6 is set to true if IPv6 routing should be
// set to be tunnel in OpenVPN, and false otherwise.
// It cannot be nil in the internal state.
IPv6 *bool // TODO automate like with Wireguard
// MSSFix is the value (1 to 10000) to set for the // MSSFix is the value (1 to 10000) to set for the
// mssfix option for OpenVPN. It is ignored if set to 0. // mssfix option for OpenVPN. It is ignored if set to 0.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
MSSFix *uint16 `json:"mssfix"` MSSFix *uint16
// Interface is the OpenVPN device interface name. // Interface is the OpenVPN device interface name.
// It cannot be an empty string in the internal state. // It cannot be an empty string in the internal state.
Interface string `json:"interface"` Interface string
// ProcessUser is the OpenVPN process OS username // ProcessUser is the OpenVPN process OS username
// to use. It cannot be empty in the internal state. // to use. It cannot be empty in the internal state.
// It defaults to 'root'. // It defaults to 'root'.
ProcessUser string `json:"process_user"` ProcessUser string
// Verbosity is the OpenVPN verbosity level from 0 to 6. // Verbosity is the OpenVPN verbosity level from 0 to 6.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
Verbosity *int `json:"verbosity"` Verbosity *int
// Flags is a slice of additional flags to be passed // Flags is a slice of additional flags to be passed
// to the OpenVPN program. // to the OpenVPN program.
Flags []string `json:"flags"` Flags []string
} }
var ivpnAccountID = regexp.MustCompile(`^(i|ivpn)\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}$`)
func (o OpenVPN) validate(vpnProvider string) (err error) { func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version // Validate version
validVersions := []string{openvpn.Openvpn25, openvpn.Openvpn26} validVersions := []string{constants.Openvpn24, constants.Openvpn25}
if err = validate.IsOneOf(o.Version, validVersions...); err != nil { if !helpers.IsOneOf(o.Version, validVersions...) {
return fmt.Errorf("%w: %w", ErrOpenVPNVersionIsNotValid, err) return fmt.Errorf("%w: %q can only be one of %s",
ErrOpenVPNVersionIsNotValid, o.Version, strings.Join(validVersions, ", "))
} }
isCustom := vpnProvider == providers.Custom isCustom := vpnProvider == constants.Custom
isUserRequired := !isCustom &&
vpnProvider != providers.Airvpn &&
vpnProvider != providers.VPNSecure
if isUserRequired && *o.User == "" { if !isCustom && o.User == "" {
return fmt.Errorf("%w", ErrOpenVPNUserIsEmpty) return ErrOpenVPNUserIsEmpty
} }
passwordRequired := isUserRequired && if !isCustom && o.Password == "" {
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(*o.User)) return ErrOpenVPNPasswordIsEmpty
if passwordRequired && *o.Password == "" {
return fmt.Errorf("%w", ErrOpenVPNPasswordIsEmpty)
} }
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile) err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
@@ -116,25 +96,16 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
return fmt.Errorf("custom configuration file: %w", err) return fmt.Errorf("custom configuration file: %w", err)
} }
err = validateOpenVPNClientCertificate(vpnProvider, *o.Cert) err = validateOpenVPNClientCertificate(vpnProvider, *o.ClientCrt)
if err != nil { if err != nil {
return fmt.Errorf("client certificate: %w", err) return fmt.Errorf("client certificate: %w", err)
} }
err = validateOpenVPNClientKey(vpnProvider, *o.Key) err = validateOpenVPNClientKey(vpnProvider, *o.ClientKey)
if err != nil { if err != nil {
return fmt.Errorf("client key: %w", err) return fmt.Errorf("client key: %w", err)
} }
err = validateOpenVPNEncryptedKey(vpnProvider, *o.EncryptedKey)
if err != nil {
return fmt.Errorf("encrypted key: %w", err)
}
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
}
const maxMSSFix = 10000 const maxMSSFix = 10000
if *o.MSSFix > maxMSSFix { if *o.MSSFix > maxMSSFix {
return fmt.Errorf("%w: %d is over the maximum value of %d", return fmt.Errorf("%w: %d is over the maximum value of %d",
@@ -155,41 +126,31 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
} }
func validateOpenVPNConfigFilepath(isCustom bool, func validateOpenVPNConfigFilepath(isCustom bool,
confFile string, confFile string) (err error) {
) (err error) {
if !isCustom { if !isCustom {
return nil return nil
} }
if confFile == "" { if confFile == "" {
return fmt.Errorf("%w", ErrFilepathMissing) return ErrFilepathMissing
} }
err = validate.FileExists(confFile) err = helpers.FileExists(confFile)
if err != nil { if err != nil {
return err return err
} }
extractor := extract.New()
_, _, err = extractor.Data(confFile)
if err != nil {
return fmt.Errorf("extracting information from custom configuration file: %w", err)
}
return nil return nil
} }
func validateOpenVPNClientCertificate(vpnProvider, func validateOpenVPNClientCertificate(vpnProvider,
clientCert string, clientCert string) (err error) {
) (err error) {
switch vpnProvider { switch vpnProvider {
case case
providers.Airvpn, constants.Cyberghost,
providers.Cyberghost, constants.VPNUnlimited:
providers.VPNSecure,
providers.VPNUnlimited:
if clientCert == "" { if clientCert == "" {
return fmt.Errorf("%w", ErrMissingValue) return ErrMissingValue
} }
} }
@@ -197,7 +158,7 @@ func validateOpenVPNClientCertificate(vpnProvider,
return nil return nil
} }
_, err = base64.StdEncoding.DecodeString(clientCert) _, err = parse.ExtractCert([]byte(clientCert))
if err != nil { if err != nil {
return err return err
} }
@@ -207,12 +168,11 @@ func validateOpenVPNClientCertificate(vpnProvider,
func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) { func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
switch vpnProvider { switch vpnProvider {
case case
providers.Airvpn, constants.Cyberghost,
providers.Cyberghost, constants.VPNUnlimited,
providers.VPNUnlimited, constants.Wevpn:
providers.Wevpn:
if clientKey == "" { if clientKey == "" {
return fmt.Errorf("%w", ErrMissingValue) return ErrMissingValue
} }
} }
@@ -220,25 +180,7 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
return nil return nil
} }
_, err = base64.StdEncoding.DecodeString(clientKey) _, err = parse.ExtractPrivateKey([]byte(clientKey))
if err != nil {
return err
}
return nil
}
func validateOpenVPNEncryptedKey(vpnProvider,
encryptedPrivateKey string,
) (err error) {
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
return fmt.Errorf("%w", ErrMissingValue)
}
if encryptedPrivateKey == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(encryptedPrivateKey)
if err != nil { if err != nil {
return err return err
} }
@@ -247,72 +189,87 @@ func validateOpenVPNEncryptedKey(vpnProvider,
func (o *OpenVPN) copy() (copied OpenVPN) { func (o *OpenVPN) copy() (copied OpenVPN) {
return OpenVPN{ return OpenVPN{
Version: o.Version, Version: o.Version,
User: gosettings.CopyPointer(o.User), User: o.User,
Password: gosettings.CopyPointer(o.Password), Password: o.Password,
ConfFile: gosettings.CopyPointer(o.ConfFile), ConfFile: helpers.CopyStringPtr(o.ConfFile),
Ciphers: gosettings.CopySlice(o.Ciphers), Ciphers: helpers.CopyStringSlice(o.Ciphers),
Auth: gosettings.CopyPointer(o.Auth), Auth: helpers.CopyStringPtr(o.Auth),
Cert: gosettings.CopyPointer(o.Cert), ClientCrt: helpers.CopyStringPtr(o.ClientCrt),
Key: gosettings.CopyPointer(o.Key), ClientKey: helpers.CopyStringPtr(o.ClientKey),
EncryptedKey: gosettings.CopyPointer(o.EncryptedKey), PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
KeyPassphrase: gosettings.CopyPointer(o.KeyPassphrase), IPv6: helpers.CopyBoolPtr(o.IPv6),
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset), MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
MSSFix: gosettings.CopyPointer(o.MSSFix), Interface: o.Interface,
Interface: o.Interface, ProcessUser: o.ProcessUser,
ProcessUser: o.ProcessUser, Verbosity: helpers.CopyIntPtr(o.Verbosity),
Verbosity: gosettings.CopyPointer(o.Verbosity), Flags: helpers.CopyStringSlice(o.Flags),
Flags: gosettings.CopySlice(o.Flags),
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (o *OpenVPN) mergeWith(other OpenVPN) {
o.Version = helpers.MergeWithString(o.Version, other.Version)
o.User = helpers.MergeWithString(o.User, other.User)
o.Password = helpers.MergeWithString(o.Password, other.Password)
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers)
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
o.Interface = helpers.MergeWithString(o.Interface, other.Interface)
o.ProcessUser = helpers.MergeWithString(o.ProcessUser, other.ProcessUser)
o.Verbosity = helpers.MergeWithIntPtr(o.Verbosity, other.Verbosity)
o.Flags = helpers.MergeStringSlices(o.Flags, other.Flags)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (o *OpenVPN) overrideWith(other OpenVPN) { func (o *OpenVPN) overrideWith(other OpenVPN) {
o.Version = gosettings.OverrideWithComparable(o.Version, other.Version) o.Version = helpers.OverrideWithString(o.Version, other.Version)
o.User = gosettings.OverrideWithPointer(o.User, other.User) o.User = helpers.OverrideWithString(o.User, other.User)
o.Password = gosettings.OverrideWithPointer(o.Password, other.Password) o.Password = helpers.OverrideWithString(o.Password, other.Password)
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile) o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
o.Ciphers = gosettings.OverrideWithSlice(o.Ciphers, other.Ciphers) o.Ciphers = helpers.OverrideWithStringSlice(o.Ciphers, other.Ciphers)
o.Auth = gosettings.OverrideWithPointer(o.Auth, other.Auth) o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
o.Cert = gosettings.OverrideWithPointer(o.Cert, other.Cert) o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt)
o.Key = gosettings.OverrideWithPointer(o.Key, other.Key) o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey)
o.EncryptedKey = gosettings.OverrideWithPointer(o.EncryptedKey, other.EncryptedKey) o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.KeyPassphrase = gosettings.OverrideWithPointer(o.KeyPassphrase, other.KeyPassphrase) o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset) o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
o.MSSFix = gosettings.OverrideWithPointer(o.MSSFix, other.MSSFix) o.Interface = helpers.OverrideWithString(o.Interface, other.Interface)
o.Interface = gosettings.OverrideWithComparable(o.Interface, other.Interface) o.ProcessUser = helpers.OverrideWithString(o.ProcessUser, other.ProcessUser)
o.ProcessUser = gosettings.OverrideWithComparable(o.ProcessUser, other.ProcessUser) o.Verbosity = helpers.OverrideWithIntPtr(o.Verbosity, other.Verbosity)
o.Verbosity = gosettings.OverrideWithPointer(o.Verbosity, other.Verbosity) o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags)
o.Flags = gosettings.OverrideWithSlice(o.Flags, other.Flags)
} }
func (o *OpenVPN) setDefaults(vpnProvider string) { func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = gosettings.DefaultComparable(o.Version, openvpn.Openvpn26) o.Version = helpers.DefaultString(o.Version, constants.Openvpn25)
o.User = gosettings.DefaultPointer(o.User, "") if vpnProvider == constants.Mullvad {
if vpnProvider == providers.Mullvad { o.Password = "m"
o.Password = gosettings.DefaultPointer(o.Password, "m")
} else {
o.Password = gosettings.DefaultPointer(o.Password, "")
} }
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "") o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
o.Auth = gosettings.DefaultPointer(o.Auth, "") o.Auth = helpers.DefaultStringPtr(o.Auth, "")
o.Cert = gosettings.DefaultPointer(o.Cert, "") o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "")
o.Key = gosettings.DefaultPointer(o.Key, "") o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "")
o.EncryptedKey = gosettings.DefaultPointer(o.EncryptedKey, "")
o.KeyPassphrase = gosettings.DefaultPointer(o.KeyPassphrase, "")
var defaultEncPreset string var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess { if vpnProvider == constants.PrivateInternetAccess {
defaultEncPreset = presets.Strong defaultEncPreset = constants.PIAEncryptionPresetStrong
} }
o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset) o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
o.MSSFix = gosettings.DefaultPointer(o.MSSFix, 0)
o.Interface = gosettings.DefaultComparable(o.Interface, "tun0") o.IPv6 = helpers.DefaultBool(o.IPv6, false)
o.ProcessUser = gosettings.DefaultComparable(o.ProcessUser, "root") o.MSSFix = helpers.DefaultUint16(o.MSSFix, 0)
o.Verbosity = gosettings.DefaultPointer(o.Verbosity, 1) o.Interface = helpers.DefaultString(o.Interface, "tun0")
o.ProcessUser = helpers.DefaultString(o.ProcessUser, "root")
o.Verbosity = helpers.DefaultInt(o.Verbosity, 1)
} }
func (o OpenVPN) String() string { func (o OpenVPN) String() string {
@@ -322,8 +279,8 @@ func (o OpenVPN) String() string {
func (o OpenVPN) toLinesNode() (node *gotree.Node) { func (o OpenVPN) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN settings:") node = gotree.New("OpenVPN settings:")
node.Appendf("OpenVPN version: %s", o.Version) node.Appendf("OpenVPN version: %s", o.Version)
node.Appendf("User: %s", gosettings.ObfuscateKey(*o.User)) node.Appendf("User: %s", helpers.ObfuscatePassword(o.User))
node.Appendf("Password: %s", gosettings.ObfuscateKey(*o.Password)) node.Appendf("Password: %s", helpers.ObfuscatePassword(o.Password))
if *o.ConfFile != "" { if *o.ConfFile != "" {
node.Appendf("Custom configuration file: %s", *o.ConfFile) node.Appendf("Custom configuration file: %s", *o.ConfFile)
@@ -337,23 +294,20 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
node.Appendf("Auth: %s", *o.Auth) node.Appendf("Auth: %s", *o.Auth)
} }
if *o.Cert != "" { if *o.ClientCrt != "" {
node.Appendf("Client crt: %s", gosettings.ObfuscateKey(*o.Cert)) node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.ClientCrt))
} }
if *o.Key != "" { if *o.ClientKey != "" {
node.Appendf("Client key: %s", gosettings.ObfuscateKey(*o.Key)) node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey))
}
if *o.EncryptedKey != "" {
node.Appendf("Encrypted key: %s (key passhrapse %s)",
gosettings.ObfuscateKey(*o.EncryptedKey), gosettings.ObfuscateKey(*o.KeyPassphrase))
} }
if *o.PIAEncPreset != "" { if *o.PIAEncPreset != "" {
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset) node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
} }
node.Appendf("Tunnel IPv6: %s", helpers.BoolPtrToYesNo(o.IPv6))
if *o.MSSFix > 0 { if *o.MSSFix > 0 {
node.Appendf("MSS Fix: %d", *o.MSSFix) node.Appendf("MSS Fix: %d", *o.MSSFix)
} }
@@ -379,58 +333,3 @@ func (o OpenVPN) WithDefaults(provider string) OpenVPN {
o.setDefaults(provider) o.setDefaults(provider)
return o return o
} }
func (o *OpenVPN) read(r *reader.Reader) (err error) {
o.Version = r.String("OPENVPN_VERSION")
o.User = r.Get("OPENVPN_USER", reader.RetroKeys("USER"), reader.ForceLowercase(false))
o.Password = r.Get("OPENVPN_PASSWORD", reader.RetroKeys("PASSWORD"), reader.ForceLowercase(false))
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
o.Ciphers = r.CSV("OPENVPN_CIPHERS", reader.RetroKeys("OPENVPN_CIPHER"))
o.Auth = r.Get("OPENVPN_AUTH")
o.Cert = r.Get("OPENVPN_CERT", reader.ForceLowercase(false))
o.Key = r.Get("OPENVPN_KEY", reader.ForceLowercase(false))
o.EncryptedKey = r.Get("OPENVPN_ENCRYPTED_KEY", reader.ForceLowercase(false))
o.KeyPassphrase = r.Get("OPENVPN_KEY_PASSPHRASE", reader.ForceLowercase(false))
o.PIAEncPreset = r.Get("PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
reader.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
o.MSSFix, err = r.Uint16Ptr("OPENVPN_MSSFIX")
if err != nil {
return err
}
o.Interface = r.String("VPN_INTERFACE",
reader.RetroKeys("OPENVPN_INTERFACE"), reader.ForceLowercase(false))
o.ProcessUser, err = readOpenVPNProcessUser(r)
if err != nil {
return err
}
o.Verbosity, err = r.IntPtr("OPENVPN_VERBOSITY")
if err != nil {
return err
}
flagsPtr := r.Get("OPENVPN_FLAGS", reader.ForceLowercase(false))
if flagsPtr != nil {
o.Flags = strings.Fields(*flagsPtr)
}
return nil
}
func readOpenVPNProcessUser(r *reader.Reader) (processUser string, err error) {
value, err := r.BoolPtr("OPENVPN_ROOT") // Retro-compatibility
if err != nil {
return "", err
} else if value != nil {
if *value {
return "root", nil
}
const defaultNonRootUser = "nonrootuser"
return defaultNonRootUser, nil
}
return r.String("OPENVPN_PROCESS_USER"), nil
}

View File

@@ -1,43 +0,0 @@
package settings
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_ivpnAccountID(t *testing.T) {
t.Parallel()
testCases := []struct {
s string
match bool
}{
{},
{s: "abc"},
{s: "i"},
{s: "ivpn"},
{s: "ivpn-aaaa"},
{s: "ivpn-aaaa-aaaa"},
{s: "ivpn-aaaa-aaaa-aaa"},
{s: "ivpn-aaaa-aaaa-aaaa", match: true},
{s: "ivpn-aaaa-aaaa-aaaaa"},
{s: "ivpn-a6B7-fP91-Zh6Y", match: true},
{s: "i-aaaa"},
{s: "i-aaaa-aaaa"},
{s: "i-aaaa-aaaa-aaa"},
{s: "i-aaaa-aaaa-aaaa", match: true},
{s: "i-aaaa-aaaa-aaaaa"},
{s: "i-a6B7-fP91-Zh6Y", match: true},
}
for _, testCase := range testCases {
t.Run(testCase.s, func(t *testing.T) {
t.Parallel()
match := ivpnAccountID.MatchString(testCase.s)
assert.Equal(t, testCase.match, match)
})
}
}

View File

@@ -2,16 +2,9 @@ package settings
import ( import (
"fmt" "fmt"
"net/netip"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -20,48 +13,37 @@ type OpenVPNSelection struct {
// It can be set to an empty string to indicate to // It can be set to an empty string to indicate to
// NOT use a custom configuration file. // NOT use a custom configuration file.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
ConfFile *string `json:"config_file_path"` ConfFile *string
// Protocol is the OpenVPN network protocol to use, // TCP is true if the OpenVPN protocol is TCP,
// and can be udp or tcp. It cannot be the empty string // and false for UDP.
// in the internal state. // It cannot be nil in the internal state.
Protocol string `json:"protocol"` TCP *bool
// EndpointIP is the server endpoint IP address.
// If set, it overrides any IP address from the picked
// built-in server connection. To indicate it should
// not be used, it should be set to [netip.IPv4Unspecified].
// It can never be the zero value in the internal state.
EndpointIP netip.Addr `json:"endpoint_ip"`
// CustomPort is the OpenVPN server endpoint port. // CustomPort is the OpenVPN server endpoint port.
// It can be set to 0 to indicate no custom port should // It can be set to 0 to indicate no custom port should
// be used. It cannot be nil in the internal state. // be used. It cannot be nil in the internal state.
CustomPort *uint16 `json:"custom_port"` CustomPort *uint16 // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe
// PIAEncPreset is the encryption preset for // PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an // Private Internet Access. It can be set to an
// empty string for other providers. // empty string for other providers.
PIAEncPreset *string `json:"pia_encryption_preset"` PIAEncPreset *string
} }
func (o OpenVPNSelection) validate(vpnProvider string) (err error) { func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate ConfFile // Validate ConfFile
if confFile := *o.ConfFile; confFile != "" { if confFile := *o.ConfFile; confFile != "" {
err := validate.FileExists(confFile) err := helpers.FileExists(confFile)
if err != nil { if err != nil {
return fmt.Errorf("configuration file: %w", err) return fmt.Errorf("configuration file: %w", err)
} }
} }
err = validate.IsOneOf(o.Protocol, constants.UDP, constants.TCP)
if err != nil {
return fmt.Errorf("network protocol: %w", err)
}
// Validate TCP // Validate TCP
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider, if *o.TCP && helpers.IsOneOf(vpnProvider,
providers.Giganews, constants.Ipvanish,
providers.Ipvanish, constants.Perfectprivacy,
providers.Perfectprivacy, constants.Privado,
providers.Privado, constants.VPNUnlimited,
providers.Vyprvpn, constants.Vyprvpn,
) { ) {
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNTCPNotSupported, vpnProvider) ErrOpenVPNTCPNotSupported, vpnProvider)
@@ -71,74 +53,60 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
if *o.CustomPort != 0 { if *o.CustomPort != 0 {
switch vpnProvider { switch vpnProvider {
// no restriction on port // no restriction on port
case providers.Custom, providers.Cyberghost, providers.HideMyAss, case constants.Cyberghost, constants.HideMyAss,
providers.Privatevpn, providers.Torguard: constants.PrivateInternetAccess, constants.Privatevpn,
constants.Protonvpn, constants.Torguard:
// no custom port allowed // no custom port allowed
case providers.Expressvpn, providers.Fastestvpn, case constants.Expressvpn, constants.Fastestvpn,
providers.Giganews, providers.Ipvanish, providers.Nordvpn, constants.Ipvanish, constants.Nordvpn,
providers.Privado, providers.Purevpn, constants.Privado, constants.Purevpn,
providers.Surfshark, providers.VPNSecure, constants.Surfshark, constants.VPNUnlimited,
providers.VPNUnlimited, providers.Vyprvpn: constants.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider) ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default: default:
var allowedTCP, allowedUDP []uint16 var allowedTCP, allowedUDP []uint16
switch vpnProvider { switch vpnProvider {
case providers.Airvpn: case constants.Ivpn:
allowedTCP = []uint16{
53, 80, 443, // IP in 1, 3
1194, 2018, 41185, // IP in 1, 2, 3, 4
}
allowedUDP = []uint16{53, 80, 443, 1194, 2018, 41185}
case providers.Ivpn:
allowedTCP = []uint16{80, 443, 1143} allowedTCP = []uint16{80, 443, 1143}
allowedUDP = []uint16{53, 1194, 2049, 2050} allowedUDP = []uint16{53, 1194, 2049, 2050}
case providers.Mullvad: case constants.Mullvad:
allowedTCP = []uint16{80, 443, 1401} allowedTCP = []uint16{80, 443, 1401}
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400} allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
case providers.Perfectprivacy: case constants.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433} allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433} allowedUDP = []uint16{44, 443, 4433}
case providers.PrivateInternetAccess: case constants.Wevpn:
allowedTCP = []uint16{80, 110, 443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
case providers.SlickVPN:
allowedTCP = []uint16{443, 8080, 8888}
allowedUDP = []uint16{443, 8080, 8888}
case providers.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018} allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198} allowedUDP = []uint16{80, 1194, 1198}
case providers.Windscribe: case constants.Windscribe:
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783} allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783} allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
default:
panic(fmt.Sprintf("VPN provider %s has no registered allowed ports", vpnProvider))
} }
allowedPorts := allowedUDP if *o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedTCP) {
if o.Protocol == constants.TCP { return fmt.Errorf("%w: %d for VPN service provider %s; %s",
allowedPorts = allowedTCP ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
} helpers.PortChoicesOrString(allowedTCP))
err = validate.IsOneOf(*o.CustomPort, allowedPorts...) } else if !*o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedUDP) {
if err != nil { return fmt.Errorf("%w: %d for VPN service provider %s; %s",
return fmt.Errorf("%w: for VPN service provider %s: %w", ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
ErrOpenVPNCustomPortNotAllowed, vpnProvider, err) helpers.PortChoicesOrString(allowedUDP))
} }
} }
} }
// Validate EncPreset // Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess { if vpnProvider == constants.PrivateInternetAccess {
validEncryptionPresets := []string{ validEncryptionPresets := []string{
presets.None, constants.PIAEncryptionPresetNone,
presets.Normal, constants.PIAEncryptionPresetNormal,
presets.Strong, constants.PIAEncryptionPresetStrong,
} }
if err = validate.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...); err != nil { if !helpers.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...) {
return fmt.Errorf("%w: %w", ErrOpenVPNEncryptionPresetNotValid, err) return fmt.Errorf("%w: %s; valid presets are %s",
ErrOpenVPNEncryptionPresetNotValid, *o.PIAEncPreset,
helpers.ChoicesOrString(validEncryptionPresets))
} }
} }
@@ -147,33 +115,37 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) { func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
return OpenVPNSelection{ return OpenVPNSelection{
ConfFile: gosettings.CopyPointer(o.ConfFile), ConfFile: helpers.CopyStringPtr(o.ConfFile),
Protocol: o.Protocol, TCP: helpers.CopyBoolPtr(o.TCP),
EndpointIP: o.EndpointIP, CustomPort: helpers.CopyUint16Ptr(o.CustomPort),
CustomPort: gosettings.CopyPointer(o.CustomPort), PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
} }
} }
func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) {
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.TCP = helpers.MergeWithBool(o.TCP, other.TCP)
o.CustomPort = helpers.MergeWithUint16(o.CustomPort, other.CustomPort)
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) { func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile) o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol) o.TCP = helpers.OverrideWithBool(o.TCP, other.TCP)
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort) o.CustomPort = helpers.OverrideWithUint16(o.CustomPort, other.CustomPort)
o.EndpointIP = gosettings.OverrideWithValidator(o.EndpointIP, other.EndpointIP) o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
} }
func (o *OpenVPNSelection) setDefaults(vpnProvider string) { func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "") o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP) o.TCP = helpers.DefaultBool(o.TCP, false)
o.EndpointIP = gosettings.DefaultValidator(o.EndpointIP, netip.IPv4Unspecified()) o.CustomPort = helpers.DefaultUint16(o.CustomPort, 0)
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
var defaultEncPreset string var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess { if vpnProvider == constants.PrivateInternetAccess {
defaultEncPreset = presets.Strong defaultEncPreset = constants.PIAEncryptionPresetStrong
} }
o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset) o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
} }
func (o OpenVPNSelection) String() string { func (o OpenVPNSelection) String() string {
@@ -182,11 +154,7 @@ func (o OpenVPNSelection) String() string {
func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) { func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN server selection settings:") node = gotree.New("OpenVPN server selection settings:")
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol)) node.Appendf("Protocol: %s", helpers.TCPPtrToString(o.TCP))
if !o.EndpointIP.IsUnspecified() {
node.Appendf("Endpoint IP address: %s", o.EndpointIP)
}
if *o.CustomPort != 0 { if *o.CustomPort != 0 {
node.Appendf("Custom port: %d", *o.CustomPort) node.Appendf("Custom port: %d", *o.CustomPort)
@@ -202,26 +170,3 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
return node return node
} }
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"))
o.EndpointIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
if err != nil {
return err
}
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
if err != nil {
return err
}
o.PIAEncPreset = r.Get("PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
reader.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
return nil
}

View File

@@ -3,11 +3,10 @@ package settings
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -15,56 +14,24 @@ import (
type PortForwarding struct { type PortForwarding struct {
// Enabled is true if port forwarding should be activated. // Enabled is true if port forwarding should be activated.
// It cannot be nil for the internal state. // It cannot be nil for the internal state.
Enabled *bool `json:"enabled"` Enabled *bool
// Provider is set to specify which custom port forwarding code
// should be used. This is especially necessary for the custom
// provider using Wireguard for a provider where Wireguard is not
// natively supported but custom port forwarding code is available.
// It defaults to the empty string, meaning the current provider
// should be the one used for port forwarding.
// It cannot be nil for the internal state.
Provider *string `json:"provider"`
// Filepath is the port forwarding status file path // Filepath is the port forwarding status file path
// to use. It can be the empty string to indicate not // to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the // to write to a file. It cannot be nil for the
// internal state // internal state
Filepath *string `json:"status_file_path"` Filepath *string
// 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.
ListeningPort *uint16 `json:"listening_port"`
// Username is only used for Private Internet Access port forwarding.
Username string `json:"username"`
// Password is only used for Private Internet Access port forwarding.
Password string `json:"password"`
} }
func (p PortForwarding) Validate(vpnProvider string) (err error) { func (p PortForwarding) validate(vpnProvider string) (err error) {
if !*p.Enabled { if !*p.Enabled {
return nil return nil
} }
// Validate current provider or custom provider specified // Validate Enabled
providerSelected := vpnProvider validProviders := []string{constants.PrivateInternetAccess}
if *p.Provider != "" { if !helpers.IsOneOf(vpnProvider, validProviders...) {
providerSelected = *p.Provider return fmt.Errorf("%w: for provider %s, it is only available for %s",
} ErrPortForwardingEnabled, vpnProvider, strings.Join(validProviders, ", "))
validProviders := []string{
providers.Perfectprivacy,
providers.PrivateInternetAccess,
providers.Privatevpn,
providers.Protonvpn,
}
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
} }
// Validate Filepath // Validate Filepath
@@ -75,49 +42,29 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
} }
} }
if providerSelected == providers.PrivateInternetAccess {
switch {
case p.Username == "":
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
case p.Password == "":
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
}
}
return nil return nil
} }
func (p *PortForwarding) Copy() (copied PortForwarding) { func (p *PortForwarding) copy() (copied PortForwarding) {
return PortForwarding{ return PortForwarding{
Enabled: gosettings.CopyPointer(p.Enabled), Enabled: helpers.CopyBoolPtr(p.Enabled),
Provider: gosettings.CopyPointer(p.Provider), Filepath: helpers.CopyStringPtr(p.Filepath),
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,
} }
} }
func (p *PortForwarding) OverrideWith(other PortForwarding) { func (p *PortForwarding) mergeWith(other PortForwarding) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled) p.Enabled = helpers.MergeWithBool(p.Enabled, other.Enabled)
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider) p.Filepath = helpers.MergeWithStringPtr(p.Filepath, other.Filepath)
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath) }
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand) func (p *PortForwarding) overrideWith(other PortForwarding) {
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort) p.Enabled = helpers.OverrideWithBool(p.Enabled, other.Enabled)
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username) p.Filepath = helpers.OverrideWithStringPtr(p.Filepath, other.Filepath)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
} }
func (p *PortForwarding) setDefaults() { func (p *PortForwarding) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, false) p.Enabled = helpers.DefaultBool(p.Enabled, false)
p.Provider = gosettings.DefaultPointer(p.Provider, "") p.Filepath = helpers.DefaultStringPtr(p.Filepath, "/tmp/gluetun/forwarded_port")
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
} }
func (p PortForwarding) String() string { func (p PortForwarding) String() string {
@@ -130,18 +77,7 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
} }
node = gotree.New("Automatic port forwarding settings:") node = gotree.New("Automatic port forwarding settings:")
node.Appendf("Enabled: yes")
listeningPort := "disabled"
if *p.ListeningPort != 0 {
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
}
node.Appendf("Redirection listening port: %s", listeningPort)
if *p.Provider == "" {
node.Appendf("Use port forwarding code for current provider")
} else {
node.Appendf("Use code for provider: %s", *p.Provider)
}
filepath := *p.Filepath filepath := *p.Filepath
if filepath == "" { if filepath == "" {
@@ -149,67 +85,5 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
} }
node.Appendf("Forwarded port file path: %s", filepath) node.Appendf("Forwarded port file path: %s", filepath)
if *p.UpCommand != "" {
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
}
if *p.DownCommand != "" {
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
}
if p.Username != "" {
credentialsNode := node.Appendf("Credentials:")
credentialsNode.Appendf("Username: %s", p.Username)
credentialsNode.Appendf("Password: %s", gosettings.ObfuscateKey(p.Password))
}
return node return node
} }
func (p *PortForwarding) read(r *reader.Reader) (err error) {
p.Enabled, err = r.BoolPtr("VPN_PORT_FORWARDING",
reader.RetroKeys(
"PORT_FORWARDING",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING",
))
if err != nil {
return err
}
p.Provider = r.Get("VPN_PORT_FORWARDING_PROVIDER")
p.Filepath = r.Get("VPN_PORT_FORWARDING_STATUS_FILE",
reader.ForceLowercase(false),
reader.RetroKeys(
"PORT_FORWARDING_STATUS_FILE",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
))
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
reader.ForceLowercase(false))
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
reader.ForceLowercase(false))
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
if err != nil {
return err
}
usernameKeys := []string{"VPN_PORT_FORWARDING_USERNAME", "OPENVPN_USER", "USER"}
for _, key := range usernameKeys {
p.Username = r.String(key, reader.ForceLowercase(false))
if p.Username != "" {
break
}
}
passwordKeys := []string{"VPN_PORT_FORWARDING_PASSWORD", "OPENVPN_PASSWORD", "PASSWORD"}
for _, key := range passwordKeys {
p.Password = r.String(key, reader.ForceLowercase(false))
if p.Password != "" {
break
}
}
return nil
}

View File

@@ -10,7 +10,7 @@ func Test_PortForwarding_String(t *testing.T) {
t.Parallel() t.Parallel()
settings := PortForwarding{ settings := PortForwarding{
Enabled: ptrTo(false), Enabled: boolPtr(false),
} }
s := settings.String() s := settings.String()

View File

@@ -2,58 +2,51 @@ package settings
import ( import (
"fmt" "fmt"
"strings"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/vpn" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// Provider contains settings specific to a VPN provider. // Provider contains settings specific to a VPN provider.
type Provider struct { type Provider struct {
// Name is the VPN service provider name. // Name is the VPN service provider name.
// It cannot be the empty string in the internal state. // It cannot be nil in the internal state.
Name string `json:"name"` Name *string
// ServerSelection is the settings to // ServerSelection is the settings to
// select the VPN server. // select the VPN server.
ServerSelection ServerSelection `json:"server_selection"` ServerSelection ServerSelection
// PortForwarding is the settings about port forwarding. // PortForwarding is the settings about port forwarding.
PortForwarding PortForwarding `json:"port_forwarding"` PortForwarding PortForwarding
} }
// TODO v4 remove pointer for receiver (because of Surfshark). // 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, allServers models.AllServers) (err error) {
// Validate Name // Validate Name
var validNames []string var validNames []string
if vpnType == vpn.OpenVPN { if vpnType == constants.OpenVPN {
validNames = providers.AllWithCustom() validNames = constants.AllProviders()
validNames = append(validNames, "pia") // Retro-compatibility validNames = append(validNames, "pia") // Retro-compatibility
} else { // Wireguard } else { // Wireguard
validNames = []string{ validNames = []string{
providers.Airvpn, constants.Custom,
providers.Custom, constants.Ivpn,
providers.Fastestvpn, constants.Mullvad,
providers.Ivpn, constants.Windscribe,
providers.Mullvad,
providers.Nordvpn,
providers.Protonvpn,
providers.Surfshark,
providers.Windscribe,
} }
} }
if err = validate.IsOneOf(p.Name, validNames...); err != nil { if !helpers.IsOneOf(*p.Name, validNames...) {
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err) return fmt.Errorf("%w: %q can only be one of %s",
ErrVPNProviderNameNotValid, *p.Name, helpers.ChoicesOrString(validNames))
} }
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner) err = p.ServerSelection.validate(*p.Name, allServers)
if err != nil { if err != nil {
return fmt.Errorf("server selection: %w", err) return fmt.Errorf("server selection: %w", err)
} }
err = p.PortForwarding.Validate(p.Name) err = p.PortForwarding.validate(*p.Name)
if err != nil { if err != nil {
return fmt.Errorf("port forwarding: %w", err) return fmt.Errorf("port forwarding: %w", err)
} }
@@ -63,22 +56,28 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
func (p *Provider) copy() (copied Provider) { func (p *Provider) copy() (copied Provider) {
return Provider{ return Provider{
Name: p.Name, Name: helpers.CopyStringPtr(p.Name),
ServerSelection: p.ServerSelection.copy(), ServerSelection: p.ServerSelection.copy(),
PortForwarding: p.PortForwarding.Copy(), PortForwarding: p.PortForwarding.copy(),
} }
} }
func (p *Provider) mergeWith(other Provider) {
p.Name = helpers.MergeWithStringPtr(p.Name, other.Name)
p.ServerSelection.mergeWith(other.ServerSelection)
p.PortForwarding.mergeWith(other.PortForwarding)
}
func (p *Provider) overrideWith(other Provider) { func (p *Provider) overrideWith(other Provider) {
p.Name = gosettings.OverrideWithComparable(p.Name, other.Name) p.Name = helpers.OverrideWithStringPtr(p.Name, other.Name)
p.ServerSelection.overrideWith(other.ServerSelection) p.ServerSelection.overrideWith(other.ServerSelection)
p.PortForwarding.OverrideWith(other.PortForwarding) p.PortForwarding.overrideWith(other.PortForwarding)
} }
func (p *Provider) setDefaults() { func (p *Provider) setDefaults() {
p.Name = gosettings.DefaultComparable(p.Name, providers.PrivateInternetAccess) p.Name = helpers.DefaultStringPtr(p.Name, constants.PrivateInternetAccess)
p.ServerSelection.setDefaults(*p.Name)
p.PortForwarding.setDefaults() p.PortForwarding.setDefaults()
p.ServerSelection.setDefaults(p.Name, *p.PortForwarding.Enabled)
} }
func (p Provider) String() string { func (p Provider) String() string {
@@ -87,42 +86,8 @@ func (p Provider) String() string {
func (p Provider) toLinesNode() (node *gotree.Node) { func (p Provider) toLinesNode() (node *gotree.Node) {
node = gotree.New("VPN provider settings:") node = gotree.New("VPN provider settings:")
node.Appendf("Name: %s", p.Name) node.Appendf("Name: %s", *p.Name)
node.AppendNode(p.ServerSelection.toLinesNode()) node.AppendNode(p.ServerSelection.toLinesNode())
node.AppendNode(p.PortForwarding.toLinesNode()) node.AppendNode(p.PortForwarding.toLinesNode())
return node return node
} }
func (p *Provider) read(r *reader.Reader, vpnType string) (err error) {
p.Name = readVPNServiceProvider(r, vpnType)
err = p.ServerSelection.read(r, p.Name, vpnType)
if err != nil {
return fmt.Errorf("server selection: %w", err)
}
err = p.PortForwarding.read(r)
if err != nil {
return fmt.Errorf("port forwarding: %w", err)
}
return nil
}
func readVPNServiceProvider(r *reader.Reader, vpnType string) (vpnProvider string) {
vpnProvider = r.String("VPN_SERVICE_PROVIDER", reader.RetroKeys("VPNSP"))
if vpnProvider == "" {
if vpnType != vpn.Wireguard && r.Get("OPENVPN_CUSTOM_CONFIG") != nil {
// retro compatibility
return providers.Custom
}
return ""
}
vpnProvider = strings.ToLower(vpnProvider)
if vpnProvider == "pia" { // retro compatibility
return providers.PrivateInternetAccess
}
return vpnProvider
}

View File

@@ -3,54 +3,33 @@ package settings
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"time"
"github.com/qdm12/gluetun/internal/publicip/api" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// PublicIP contains settings for port forwarding. // PublicIP contains settings for port forwarding.
type PublicIP struct { type PublicIP struct {
// Enabled is set to true to fetch the public ip address // Period is the period to get the public IP address.
// information on VPN connection. It defaults to true. // It can be set to 0 to disable periodic checking.
Enabled *bool // 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 // IPFilepath is the public IP address status file path
// to use. It can be the empty string to indicate not // to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the // to write to a file. It cannot be nil for the
// internal state // internal state
IPFilepath *string 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
}
// UpdateWith deep copies the receiving settings, overrides the copy with
// fields set in the partialUpdate argument, validates the new settings
// and returns them if they are valid, or returns an error otherwise.
// In all cases, the receiving settings are unmodified.
func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP, err error) {
updatedSettings = p.copy()
updatedSettings.overrideWith(partialUpdate)
err = updatedSettings.validate()
if err != nil {
return updatedSettings, fmt.Errorf("validating updated settings: %w", err)
}
return updatedSettings, nil
} }
func (p PublicIP) validate() (err error) { 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 if *p.IPFilepath != "" { // optional
_, err := filepath.Abs(*p.IPFilepath) _, err := filepath.Abs(*p.IPFilepath)
if err != nil { if err != nil {
@@ -58,39 +37,30 @@ 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)
}
}
return nil return nil
} }
func (p *PublicIP) copy() (copied PublicIP) { func (p *PublicIP) copy() (copied PublicIP) {
return PublicIP{ return PublicIP{
Enabled: gosettings.CopyPointer(p.Enabled), Period: helpers.CopyDurationPtr(p.Period),
IPFilepath: gosettings.CopyPointer(p.IPFilepath), IPFilepath: helpers.CopyStringPtr(p.IPFilepath),
APIs: gosettings.CopySlice(p.APIs),
} }
} }
func (p *PublicIP) mergeWith(other PublicIP) {
p.Period = helpers.MergeWithDuration(p.Period, other.Period)
p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath)
}
func (p *PublicIP) overrideWith(other PublicIP) { func (p *PublicIP) overrideWith(other PublicIP) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled) p.Period = helpers.OverrideWithDuration(p.Period, other.Period)
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath) p.IPFilepath = helpers.OverrideWithStringPtr(p.IPFilepath, other.IPFilepath)
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
} }
func (p *PublicIP) setDefaults() { func (p *PublicIP) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, true) const defaultPeriod = 12 * time.Hour
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip") p.Period = helpers.DefaultDuration(p.Period, defaultPeriod)
p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{ p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip")
{Name: string(api.IPInfo)},
{Name: string(api.Cloudflare)},
{Name: string(api.IfConfigCo)},
{Name: string(api.IP2Location)},
})
} }
func (p PublicIP) String() string { func (p PublicIP) String() string {
@@ -98,78 +68,22 @@ func (p PublicIP) String() string {
} }
func (p PublicIP) toLinesNode() (node *gotree.Node) { func (p PublicIP) toLinesNode() (node *gotree.Node) {
if !*p.Enabled { node = gotree.New("Public IP settings:")
return gotree.New("Public IP settings: disabled")
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 != "" { if *p.IPFilepath != "" {
node.Appendf("IP file path: %s", *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)
}
}
return node return node
} }
func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
p.Enabled, err = readPublicIPEnabled(r, warner)
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
}
}
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
}

View File

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

View File

@@ -1,16 +1,12 @@
package settings package settings
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"net" "net"
"os" "os"
"strconv" "strconv"
"github.com/qdm12/gluetun/internal/server/middlewares/auth" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -22,14 +18,6 @@ type ControlServer struct {
// Log can be true or false to enable logging on requests. // Log can be true or false to enable logging on requests.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
Log *bool Log *bool
// AuthFilePath is the path to the file containing the authentication
// configuration for the middleware.
// 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) { func (c ControlServer) validate() (err error) {
@@ -50,55 +38,34 @@ func (c ControlServer) validate() (err error) {
ErrControlServerPrivilegedPort, port, uid) ErrControlServerPrivilegedPort, port, uid)
} }
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
jsonDecoder.DisallowUnknownFields()
var role auth.Role
err = jsonDecoder.Decode(&role)
if err != nil {
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
}
if role.Auth != "" {
err = role.Validate()
if err != nil {
return fmt.Errorf("default authentication role is not valid: %w", err)
}
}
return nil return nil
} }
func (c *ControlServer) copy() (copied ControlServer) { func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{ return ControlServer{
Address: gosettings.CopyPointer(c.Address), Address: helpers.CopyStringPtr(c.Address),
Log: gosettings.CopyPointer(c.Log), Log: helpers.CopyBoolPtr(c.Log),
AuthFilePath: c.AuthFilePath,
AuthDefaultRole: c.AuthDefaultRole,
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (c *ControlServer) mergeWith(other ControlServer) {
c.Address = helpers.MergeWithStringPtr(c.Address, other.Address)
c.Log = helpers.MergeWithBool(c.Log, other.Log)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (c *ControlServer) overrideWith(other ControlServer) { func (c *ControlServer) overrideWith(other ControlServer) {
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address) c.Address = helpers.OverrideWithStringPtr(c.Address, other.Address)
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log) c.Log = helpers.OverrideWithBool(c.Log, other.Log)
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
} }
func (c *ControlServer) setDefaults() { func (c *ControlServer) setDefaults() {
c.Address = gosettings.DefaultPointer(c.Address, ":8000") c.Address = helpers.DefaultStringPtr(c.Address, ":8000")
c.Log = gosettings.DefaultPointer(c.Log, true) c.Log = helpers.DefaultBool(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 { func (c ControlServer) String() string {
@@ -108,26 +75,6 @@ func (c ControlServer) String() string {
func (c ControlServer) toLinesNode() (node *gotree.Node) { func (c ControlServer) toLinesNode() (node *gotree.Node) {
node = gotree.New("Control server settings:") node = gotree.New("Control server settings:")
node.Appendf("Listening address: %s", *c.Address) node.Appendf("Listening address: %s", *c.Address)
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log)) node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(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 return node
} }
func (c *ControlServer) read(r *reader.Reader) (err error) {
c.Log, err = r.BoolPtr("HTTP_CONTROL_SERVER_LOG")
if err != nil {
return err
}
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
}

View File

@@ -3,124 +3,123 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/configuration/settings/validation" "github.com/qdm12/gluetun/internal/configuration/settings/validation"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
type ServerSelection struct { type ServerSelection struct { //nolint:maligned
// VPN is the VPN type which can be 'openvpn' // VPN is the VPN type which can be 'openvpn'
// or 'wireguard'. It cannot be the empty string // or 'wireguard'. It cannot be the empty string
// in the internal state. // in the internal state.
VPN string `json:"vpn"` VPN string
// Countries is the list of countries to filter VPN servers with. // TargetIP is the server endpoint IP address to use.
Countries []string `json:"countries"` // It will override any IP address from the picked
// Categories is the list of categories to filter VPN servers with. // built-in server. It cannot be nil in the internal
Categories []string `json:"categories"` // state, and can be set to an empty net.IP{} to indicate
// there is not target IP address to use.
TargetIP net.IP
// Counties is the list of countries to filter VPN servers with.
Countries []string
// Regions is the list of regions to filter VPN servers with. // Regions is the list of regions to filter VPN servers with.
Regions []string `json:"regions"` Regions []string
// Cities is the list of cities to filter VPN servers with. // Cities is the list of cities to filter VPN servers with.
Cities []string `json:"cities"` Cities []string
// ISPs is the list of ISP names to filter VPN servers with. // ISPs is the list of ISP names to filter VPN servers with.
ISPs []string `json:"isps"` ISPs []string
// Names is the list of server names to filter VPN servers with. // Names is the list of server names to filter VPN servers with.
Names []string `json:"names"` Names []string
// Numbers is the list of server numbers to filter VPN servers with. // Numbers is the list of server numbers to filter VPN servers with.
Numbers []uint16 `json:"numbers"` Numbers []uint16
// Hostnames is the list of hostnames to filter VPN servers with. // Hostnames is the list of hostnames to filter VPN servers with.
Hostnames []string `json:"hostnames"` Hostnames []string
// OwnedOnly is true if VPN provider servers that are not owned // OwnedOnly is true if VPN provider servers that are not owned
// should be filtered. This is used with Mullvad. // should be filtered. This is used with Mullvad.
OwnedOnly *bool `json:"owned_only"` OwnedOnly *bool
// FreeOnly is true if VPN servers that are not free should // FreeOnly is true if VPN servers that are not free should
// be filtered. This is used with ProtonVPN and VPN Unlimited. // be filtered. This is used with ProtonVPN and VPN Unlimited.
FreeOnly *bool `json:"free_only"` FreeOnly *bool
// PremiumOnly is true if VPN servers that are not premium should
// be filtered. This is used with VPN Secure.
// TODO extend to providers using FreeOnly.
PremiumOnly *bool `json:"premium_only"`
// StreamOnly is true if VPN servers not for streaming should // StreamOnly is true if VPN servers not for streaming should
// be filtered. This is used with ProtonVPN and VPNUnlimited. // be filtered. This is used with VPNUnlimited.
StreamOnly *bool `json:"stream_only"` StreamOnly *bool
// MultiHopOnly is true if VPN servers that are not multihop // MultiHopOnly is true if VPN servers that are not multihop
// should be filtered. This is used with Surfshark. // should be filtered. This is used with Surfshark.
MultiHopOnly *bool `json:"multi_hop_only"` MultiHopOnly *bool
// PortForwardOnly is true if VPN servers that don't support
// port forwarding should be filtered. This is used with PIA
// and ProtonVPN.
PortForwardOnly *bool `json:"port_forward_only"`
// SecureCoreOnly is true if VPN servers without secure core should
// be filtered. This is used with ProtonVPN.
SecureCoreOnly *bool `json:"secure_core_only"`
// TorOnly is true if VPN servers without tor should
// be filtered. This is used with ProtonVPN.
TorOnly *bool `json:"tor_only"`
// OpenVPN contains settings to select OpenVPN servers // OpenVPN contains settings to select OpenVPN servers
// and the final connection. // and the final connection.
OpenVPN OpenVPNSelection `json:"openvpn"` OpenVPN OpenVPNSelection
// Wireguard contains settings to select Wireguard servers // Wireguard contains settings to select Wireguard servers
// and the final connection. // and the final connection.
Wireguard WireguardSelection `json:"wireguard"` Wireguard WireguardSelection
} }
var ( var (
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported") ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported") ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported") ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported") ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
ErrPortForwardOnlyNotSupported = errors.New("port forwarding only filter is not supported")
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set")
ErrSecureCoreOnlyNotSupported = errors.New("secure core only filter is not supported")
ErrTorOnlyNotSupported = errors.New("tor only filter is not supported")
) )
func (ss *ServerSelection) validate(vpnServiceProvider string, func (ss *ServerSelection) validate(vpnServiceProvider string,
filterChoicesGetter FilterChoicesGetter, warner Warner, allServers models.AllServers) (err error) {
) (err error) {
switch ss.VPN { switch ss.VPN {
case vpn.OpenVPN, vpn.Wireguard: case constants.OpenVPN, constants.Wireguard:
default: default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN) return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
} }
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner) countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, allServers)
if err != nil { if err != nil {
return err // already wrapped error return err // already wrapped error
} }
// Retro-compatibility err = validateServerFilters(*ss, countryChoices, regionChoices, cityChoices,
switch vpnServiceProvider { ispChoices, nameChoices, hostnameChoices)
case providers.Nordvpn:
*ss = nordvpnRetroRegion(*ss, filterChoices.Regions, filterChoices.Countries)
case providers.Surfshark:
*ss = surfsharkRetroRegion(*ss)
}
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)
if err != nil { if err != nil {
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err) if errors.Is(err, helpers.ErrNoChoice) {
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
}
return err // already wrapped error
} }
err = validateSubscriptionTierFilters(*ss, vpnServiceProvider) if *ss.OwnedOnly &&
if err != nil { vpnServiceProvider != constants.Mullvad {
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err) return fmt.Errorf("%w: for VPN service provider %s",
ErrOwnedOnlyNotSupported, vpnServiceProvider)
} }
err = validateFeatureFilters(*ss, vpnServiceProvider) if *ss.FreeOnly &&
if err != nil { !helpers.IsOneOf(vpnServiceProvider,
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err) constants.Protonvpn,
constants.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrFreeOnlyNotSupported, vpnServiceProvider)
} }
if ss.VPN == vpn.OpenVPN { if *ss.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider,
constants.Protonvpn,
constants.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrStreamOnlyNotSupported, vpnServiceProvider)
}
if *ss.MultiHopOnly &&
vpnServiceProvider != constants.Surfshark {
return fmt.Errorf("%w: for VPN service provider %s",
ErrMultiHopOnlyNotSupported, vpnServiceProvider)
}
if ss.VPN == constants.OpenVPN {
err = ss.OpenVPN.validate(vpnServiceProvider) err = ss.OpenVPN.validate(vpnServiceProvider)
if err != nil { if err != nil {
return fmt.Errorf("OpenVPN server selection settings: %w", err) return fmt.Errorf("OpenVPN server selection settings: %w", err)
@@ -135,218 +134,225 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
return nil return nil
} }
func getLocationFilterChoices(vpnServiceProvider string, func getLocationFilterChoices(vpnServiceProvider string, ss *ServerSelection,
ss *ServerSelection, filterChoicesGetter FilterChoicesGetter, warner Warner) ( allServers models.AllServers) (
filterChoices models.FilterChoices, err error, countryChoices, regionChoices, cityChoices,
) { ispChoices, nameChoices, hostnameChoices []string,
filterChoices = filterChoicesGetter.GetFilterChoices(vpnServiceProvider) err error) {
switch vpnServiceProvider {
if vpnServiceProvider == providers.Surfshark { case constants.Custom:
// // Retro compatibility case constants.Cyberghost:
servers := allServers.GetCyberghost()
countryChoices = validation.CyberghostCountryChoices(servers)
hostnameChoices = validation.CyberghostHostnameChoices(servers)
case constants.Expressvpn:
servers := allServers.GetExpressvpn()
countryChoices = validation.ExpressvpnCountriesChoices(servers)
cityChoices = validation.ExpressvpnCityChoices(servers)
hostnameChoices = validation.ExpressvpnHostnameChoices(servers)
case constants.Fastestvpn:
servers := allServers.GetFastestvpn()
countryChoices = validation.FastestvpnCountriesChoices(servers)
hostnameChoices = validation.FastestvpnHostnameChoices(servers)
case constants.HideMyAss:
servers := allServers.GetHideMyAss()
countryChoices = validation.HideMyAssCountryChoices(servers)
regionChoices = validation.HideMyAssRegionChoices(servers)
cityChoices = validation.HideMyAssCityChoices(servers)
hostnameChoices = validation.HideMyAssHostnameChoices(servers)
case constants.Ipvanish:
servers := allServers.GetIpvanish()
countryChoices = validation.IpvanishCountryChoices(servers)
cityChoices = validation.IpvanishCityChoices(servers)
hostnameChoices = validation.IpvanishHostnameChoices(servers)
case constants.Ivpn:
servers := allServers.GetIvpn()
countryChoices = validation.IvpnCountryChoices(servers)
cityChoices = validation.IvpnCityChoices(servers)
ispChoices = validation.IvpnISPChoices(servers)
hostnameChoices = validation.IvpnHostnameChoices(servers)
case constants.Mullvad:
servers := allServers.GetMullvad()
countryChoices = validation.MullvadCountryChoices(servers)
cityChoices = validation.MullvadCityChoices(servers)
ispChoices = validation.MullvadISPChoices(servers)
hostnameChoices = validation.MullvadHostnameChoices(servers)
case constants.Nordvpn:
servers := allServers.GetNordvpn()
regionChoices = validation.NordvpnRegionChoices(servers)
hostnameChoices = validation.NordvpnHostnameChoices(servers)
case constants.Perfectprivacy:
servers := allServers.GetPerfectprivacy()
cityChoices = validation.PerfectprivacyCityChoices(servers)
case constants.Privado:
servers := allServers.GetPrivado()
countryChoices = validation.PrivadoCountryChoices(servers)
regionChoices = validation.PrivadoRegionChoices(servers)
cityChoices = validation.PrivadoCityChoices(servers)
hostnameChoices = validation.PrivadoHostnameChoices(servers)
case constants.PrivateInternetAccess:
servers := allServers.GetPia()
regionChoices = validation.PIAGeoChoices(servers)
hostnameChoices = validation.PIAHostnameChoices(servers)
nameChoices = validation.PIANameChoices(servers)
case constants.Privatevpn:
servers := allServers.GetPrivatevpn()
countryChoices = validation.PrivatevpnCountryChoices(servers)
cityChoices = validation.PrivatevpnCityChoices(servers)
hostnameChoices = validation.PrivatevpnHostnameChoices(servers)
case constants.Protonvpn:
servers := allServers.GetProtonvpn()
countryChoices = validation.ProtonvpnCountryChoices(servers)
regionChoices = validation.ProtonvpnRegionChoices(servers)
cityChoices = validation.ProtonvpnCityChoices(servers)
nameChoices = validation.ProtonvpnNameChoices(servers)
hostnameChoices = validation.ProtonvpnHostnameChoices(servers)
case constants.Purevpn:
servers := allServers.GetPurevpn()
countryChoices = validation.PurevpnCountryChoices(servers)
regionChoices = validation.PurevpnRegionChoices(servers)
cityChoices = validation.PurevpnCityChoices(servers)
hostnameChoices = validation.PurevpnHostnameChoices(servers)
case constants.Surfshark:
servers := allServers.GetSurfshark()
countryChoices = validation.SurfsharkCountryChoices(servers)
cityChoices = validation.SurfsharkCityChoices(servers)
hostnameChoices = validation.SurfsharkHostnameChoices(servers)
regionChoices = validation.SurfsharkRegionChoices(servers)
// TODO v4 remove // TODO v4 remove
newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic regionChoices = append(regionChoices, validation.SurfsharkRetroLocChoices()...)
err := atLeastOneIsOneOfCaseInsensitive(ss.Regions, newAndRetroRegions, warner) if err := helpers.AreAllOneOf(ss.Regions, regionChoices); err != nil {
if err != nil { return nil, nil, nil, nil, nil, nil, fmt.Errorf("%w: %s", ErrRegionNotValid, err)
// 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)
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
// Retro compatibility
// TODO remove in v4
*ss = surfsharkRetroRegion(*ss)
case constants.Torguard:
servers := allServers.GetTorguard()
countryChoices = validation.TorguardCountryChoices(servers)
cityChoices = validation.TorguardCityChoices(servers)
hostnameChoices = validation.TorguardHostnameChoices(servers)
case constants.VPNUnlimited:
servers := allServers.GetVPNUnlimited()
countryChoices = validation.VPNUnlimitedCountryChoices(servers)
cityChoices = validation.VPNUnlimitedCityChoices(servers)
hostnameChoices = validation.VPNUnlimitedHostnameChoices(servers)
case constants.Vyprvpn:
servers := allServers.GetVyprvpn()
regionChoices = validation.VyprvpnRegionChoices(servers)
case constants.Wevpn:
servers := allServers.GetWevpn()
cityChoices = validation.WevpnCityChoices(servers)
hostnameChoices = validation.WevpnHostnameChoices(servers)
case constants.Windscribe:
servers := allServers.GetWindscribe()
regionChoices = validation.WindscribeRegionChoices(servers)
cityChoices = validation.WindscribeCityChoices(servers)
hostnameChoices = validation.WindscribeHostnameChoices(servers)
default:
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%w: %s", ErrVPNProviderNameNotValid, vpnServiceProvider)
} }
return filterChoices, nil return countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices, nil
} }
// validateServerFilters validates filters against the choices given as arguments. // validateServerFilters validates filters against the choices given as arguments.
// Set an argument to nil to pass the check for a particular filter. // Set an argument to nil to pass the check for a particular filter.
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices, func validateServerFilters(settings ServerSelection,
vpnServiceProvider string, warner Warner, countryChoices, regionChoices, cityChoices, ispChoices,
) (err error) { nameChoices, hostnameChoices []string) (err error) {
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner) if err := helpers.AreAllOneOf(settings.Countries, countryChoices); err != nil {
if err != nil { return fmt.Errorf("%w: %s", ErrCountryNotValid, err)
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner) if err := helpers.AreAllOneOf(settings.Regions, regionChoices); err != nil {
if err != nil { return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner) if err := helpers.AreAllOneOf(settings.Cities, cityChoices); err != nil {
if err != nil { return fmt.Errorf("%w: %s", ErrCityNotValid, err)
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner) if err := helpers.AreAllOneOf(settings.ISPs, ispChoices); err != nil {
if err != nil { return fmt.Errorf("%w: %s", ErrISPNotValid, err)
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner) if err := helpers.AreAllOneOf(settings.Hostnames, hostnameChoices); err != nil {
if err != nil { return fmt.Errorf("%w: %s", ErrHostnameNotValid, err)
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
} }
if vpnServiceProvider == providers.Custom { if err := helpers.AreAllOneOf(settings.Names, nameChoices); err != nil {
switch len(settings.Names) { return fmt.Errorf("%w: %s", ErrNameNotValid, err)
case 0:
case 1:
// Allow a single name to be specified for the custom provider in case
// the user wants to use VPN server side port forwarding with PIA
// which requires a server name for TLS verification.
filterChoices.Names = settings.Names
default:
return fmt.Errorf("%w: %d names specified instead of "+
"0 or 1 for the custom provider",
ErrNameNotValid, len(settings.Names))
}
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
} }
return nil 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 &&
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
return fmt.Errorf("%w", ErrFreeOnlyNotSupported)
case *settings.PremiumOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.VPNSecure):
return fmt.Errorf("%w", ErrPremiumOnlyNotSupported)
case *settings.FreeOnly && *settings.PremiumOnly:
return fmt.Errorf("%w", ErrFreePremiumBothSet)
default:
return nil
}
}
func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) error {
switch {
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
case *settings.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
case *settings.MultiHopOnly && vpnServiceProvider != providers.Surfshark:
return fmt.Errorf("%w", ErrMultiHopOnlyNotSupported)
case *settings.PortForwardOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.PrivateInternetAccess, providers.Protonvpn):
return fmt.Errorf("%w", ErrPortForwardOnlyNotSupported)
case *settings.SecureCoreOnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrSecureCoreOnlyNotSupported)
case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrTorOnlyNotSupported)
default:
return nil
}
}
func (ss *ServerSelection) copy() (copied ServerSelection) { func (ss *ServerSelection) copy() (copied ServerSelection) {
return ServerSelection{ return ServerSelection{
VPN: ss.VPN, VPN: ss.VPN,
Countries: gosettings.CopySlice(ss.Countries), TargetIP: helpers.CopyIP(ss.TargetIP),
Categories: gosettings.CopySlice(ss.Categories), Countries: helpers.CopyStringSlice(ss.Countries),
Regions: gosettings.CopySlice(ss.Regions), Regions: helpers.CopyStringSlice(ss.Regions),
Cities: gosettings.CopySlice(ss.Cities), Cities: helpers.CopyStringSlice(ss.Cities),
ISPs: gosettings.CopySlice(ss.ISPs), ISPs: helpers.CopyStringSlice(ss.ISPs),
Hostnames: gosettings.CopySlice(ss.Hostnames), Hostnames: helpers.CopyStringSlice(ss.Hostnames),
Names: gosettings.CopySlice(ss.Names), Names: helpers.CopyStringSlice(ss.Names),
Numbers: gosettings.CopySlice(ss.Numbers), Numbers: helpers.CopyUint16Slice(ss.Numbers),
OwnedOnly: gosettings.CopyPointer(ss.OwnedOnly), OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
FreeOnly: gosettings.CopyPointer(ss.FreeOnly), FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly), StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
StreamOnly: gosettings.CopyPointer(ss.StreamOnly), MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
SecureCoreOnly: gosettings.CopyPointer(ss.SecureCoreOnly), OpenVPN: ss.OpenVPN.copy(),
TorOnly: gosettings.CopyPointer(ss.TorOnly), Wireguard: ss.Wireguard.copy(),
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(),
Wireguard: ss.Wireguard.copy(),
} }
} }
func (ss *ServerSelection) mergeWith(other ServerSelection) {
ss.VPN = helpers.MergeWithString(ss.VPN, other.VPN)
ss.TargetIP = helpers.MergeWithIP(ss.TargetIP, other.TargetIP)
ss.Countries = helpers.MergeStringSlices(ss.Countries, other.Countries)
ss.Regions = helpers.MergeStringSlices(ss.Regions, other.Regions)
ss.Cities = helpers.MergeStringSlices(ss.Cities, other.Cities)
ss.ISPs = helpers.MergeStringSlices(ss.ISPs, other.ISPs)
ss.Hostnames = helpers.MergeStringSlices(ss.Hostnames, other.Hostnames)
ss.Names = helpers.MergeStringSlices(ss.Names, other.Names)
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.OpenVPN.mergeWith(other.OpenVPN)
ss.Wireguard.mergeWith(other.Wireguard)
}
func (ss *ServerSelection) overrideWith(other ServerSelection) { func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN) ss.VPN = helpers.OverrideWithString(ss.VPN, other.VPN)
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries) ss.TargetIP = helpers.OverrideWithIP(ss.TargetIP, other.TargetIP)
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories) ss.Countries = helpers.OverrideWithStringSlice(ss.Countries, other.Countries)
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions) ss.Regions = helpers.OverrideWithStringSlice(ss.Regions, other.Regions)
ss.Cities = gosettings.OverrideWithSlice(ss.Cities, other.Cities) ss.Cities = helpers.OverrideWithStringSlice(ss.Cities, other.Cities)
ss.ISPs = gosettings.OverrideWithSlice(ss.ISPs, other.ISPs) ss.ISPs = helpers.OverrideWithStringSlice(ss.ISPs, other.ISPs)
ss.Hostnames = gosettings.OverrideWithSlice(ss.Hostnames, other.Hostnames) ss.Hostnames = helpers.OverrideWithStringSlice(ss.Hostnames, other.Hostnames)
ss.Names = gosettings.OverrideWithSlice(ss.Names, other.Names) ss.Names = helpers.OverrideWithStringSlice(ss.Names, other.Names)
ss.Numbers = gosettings.OverrideWithSlice(ss.Numbers, other.Numbers) ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
ss.OwnedOnly = gosettings.OverrideWithPointer(ss.OwnedOnly, other.OwnedOnly) ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly) ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly) ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
ss.StreamOnly = gosettings.OverrideWithPointer(ss.StreamOnly, other.StreamOnly) ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.SecureCoreOnly = gosettings.OverrideWithPointer(ss.SecureCoreOnly, other.SecureCoreOnly)
ss.TorOnly = gosettings.OverrideWithPointer(ss.TorOnly, other.TorOnly)
ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
ss.OpenVPN.overrideWith(other.OpenVPN) ss.OpenVPN.overrideWith(other.OpenVPN)
ss.Wireguard.overrideWith(other.Wireguard) ss.Wireguard.overrideWith(other.Wireguard)
} }
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) { func (ss *ServerSelection) setDefaults(vpnProvider string) {
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN) ss.VPN = helpers.DefaultString(ss.VPN, constants.OpenVPN)
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false) ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false) ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false) ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
ss.StreamOnly = gosettings.DefaultPointer(ss.StreamOnly, false) ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false) ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
defaultPortForwardOnly := portForwardingEnabled &&
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
ss.OpenVPN.setDefaults(vpnProvider) ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults() ss.Wireguard.setDefaults()
} }
@@ -358,15 +364,14 @@ func (ss ServerSelection) String() string {
func (ss ServerSelection) toLinesNode() (node *gotree.Node) { func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server selection settings:") node = gotree.New("Server selection settings:")
node.Appendf("VPN type: %s", ss.VPN) node.Appendf("VPN type: %s", ss.VPN)
if len(ss.TargetIP) > 0 {
node.Appendf("Target IP address: %s", ss.TargetIP)
}
if len(ss.Countries) > 0 { if len(ss.Countries) > 0 {
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", ")) node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
} }
if len(ss.Categories) > 0 {
node.Appendf("Categories: %s", strings.Join(ss.Categories, ", "))
}
if len(ss.Regions) > 0 { if len(ss.Regions) > 0 {
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", ")) node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
} }
@@ -402,31 +407,15 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node.Appendf("Free only servers: yes") node.Appendf("Free only servers: yes")
} }
if *ss.PremiumOnly {
node.Appendf("Premium only servers: yes")
}
if *ss.StreamOnly { if *ss.StreamOnly {
node.Appendf("Stream only servers: yes") node.Appendf("Stream only servers: yes")
} }
if *ss.SecureCoreOnly {
node.Appendf("Secure Core only servers: yes")
}
if *ss.TorOnly {
node.Appendf("Tor only servers: yes")
}
if *ss.MultiHopOnly { if *ss.MultiHopOnly {
node.Appendf("Multi-hop only servers: yes") node.Appendf("Multi-hop only servers: yes")
} }
if *ss.PortForwardOnly { if ss.VPN == constants.OpenVPN {
node.Appendf("Port forwarding only servers: yes")
}
if ss.VPN == vpn.OpenVPN {
node.AppendNode(ss.OpenVPN.toLinesNode()) node.AppendNode(ss.OpenVPN.toLinesNode())
} else { } else {
node.AppendNode(ss.Wireguard.toLinesNode()) node.AppendNode(ss.Wireguard.toLinesNode())
@@ -438,90 +427,6 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
// WithDefaults is a shorthand using setDefaults. // WithDefaults is a shorthand using setDefaults.
// It's used in unit tests in other packages. // It's used in unit tests in other packages.
func (ss ServerSelection) WithDefaults(provider string) ServerSelection { func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
const portForwardingEnabled = false ss.setDefaults(provider)
ss.setDefaults(provider, portForwardingEnabled)
return ss return ss
} }
func (ss *ServerSelection) read(r *reader.Reader,
vpnProvider, vpnType string,
) (err error) {
ss.VPN = vpnType
countriesRetroKeys := []string{"COUNTRY"}
if vpnProvider == providers.Cyberghost {
countriesRetroKeys = append(countriesRetroKeys, "REGION")
}
ss.Countries = r.CSV("SERVER_COUNTRIES", reader.RetroKeys(countriesRetroKeys...))
ss.Regions = r.CSV("SERVER_REGIONS", reader.RetroKeys("REGION"))
ss.Cities = r.CSV("SERVER_CITIES", reader.RetroKeys("CITY"))
ss.ISPs = r.CSV("ISP")
ss.Hostnames = r.CSV("SERVER_HOSTNAMES", reader.RetroKeys("SERVER_HOSTNAME"))
ss.Names = r.CSV("SERVER_NAMES", reader.RetroKeys("SERVER_NAME"))
ss.Numbers, err = r.CSVUint16("SERVER_NUMBER")
ss.Categories = r.CSV("SERVER_CATEGORIES")
if err != nil {
return err
}
// Mullvad only
ss.OwnedOnly, err = r.BoolPtr("OWNED_ONLY", reader.RetroKeys("OWNED"))
if err != nil {
return err
}
// VPNUnlimited and ProtonVPN only
ss.FreeOnly, err = r.BoolPtr("FREE_ONLY")
if err != nil {
return err
}
// VPNSecure only
ss.PremiumOnly, err = r.BoolPtr("PREMIUM_ONLY")
if err != nil {
return err
}
// Surfshark only
ss.MultiHopOnly, err = r.BoolPtr("MULTIHOP_ONLY")
if err != nil {
return err
}
// VPNUnlimited and ProtonVPN only
ss.StreamOnly, err = r.BoolPtr("STREAM_ONLY")
if err != nil {
return err
}
// ProtonVPN only
ss.SecureCoreOnly, err = r.BoolPtr("SECURE_CORE_ONLY")
if err != nil {
return err
}
// ProtonVPN only
ss.TorOnly, err = r.BoolPtr("TOR_ONLY")
if err != nil {
return err
}
// PIA and ProtonVPN only
ss.PortForwardOnly, err = r.BoolPtr("PORT_FORWARD_ONLY")
if err != nil {
return err
}
err = ss.OpenVPN.read(r)
if err != nil {
return err
}
err = ss.Wireguard.read(r)
if err != nil {
return err
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More